| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| {-# LANGUAGE DeriveGeneric #-} |
|
|
| module AgentMemory where |
|
|
| import Data.ByteString (ByteString) |
| import qualified Data.ByteString as BS |
| import qualified Data.ByteString.Char8 as BSC |
| import Data.List (intercalate) |
| import Data.Time (getCurrentTime, formatTime, defaultTimeLocale, UTCTime) |
| import GHC.Generics (Generic) |
| import Data.Hashable (hash) |
| import qualified Data.Map.Strict as Map |
| import Control.Exception (bracket) |
| import System.IO (withFile, IOMode(..), hPutStrLn, hGetContents) |
|
|
| |
| import SpacetimeAgent (Observation(..), AgentId, Frame, Agent(..)) |
|
|
| |
| |
|
|
| data WormMemoryEntry = WormMemoryEntry |
| { entryIndex :: Int |
| , entryTimestamp :: UTCTime |
| , entryAgentId :: AgentId |
| , entryData :: Observation |
| , entryPriorHash :: ByteString |
| , entryCurrentHash :: ByteString |
| } deriving (Show, Eq, Generic) |
|
|
| |
|
|
| data WormMemoryLog = WormMemoryLog |
| { logPath :: FilePath |
| , logEntries :: [WormMemoryEntry] |
| , logTailHash :: ByteString |
| , logSize :: Int |
| } deriving (Show, Eq, Generic) |
|
|
| |
|
|
| encodeMemoryEntry :: WormMemoryEntry -> ByteString |
| encodeMemoryEntry entry = |
| let idxStr = show (entryIndex entry) |
| aidStr = entryAgentId entry |
| timeStr = show (entryTimestamp entry) |
| obsStr = show (entryData entry) |
| priorStr = BSC.unpack (entryPriorHash entry) |
| parts = [idxStr, aidStr, timeStr, obsStr, priorStr] |
| in BSC.pack (intercalate "|" parts) |
|
|
| |
| |
| |
|
|
| computeWormSeal :: WormMemoryEntry -> ByteString -> ByteString |
| computeWormSeal entry priorHash = |
| let thisData = encodeMemoryEntry entry |
| chainData = thisData <> priorHash |
| in BSC.pack $ "worm" ++ show (hash chainData) |
|
|
| |
|
|
| createMemoryEntry :: Int -> UTCTime -> AgentId -> Observation -> ByteString -> WormMemoryEntry |
| createMemoryEntry idx ts aid obs priorHash = |
| let entry = WormMemoryEntry |
| { entryIndex = idx |
| , entryTimestamp = ts |
| , entryAgentId = aid |
| , entryData = obs |
| , entryPriorHash = priorHash |
| , entryCurrentHash = BS.empty |
| } |
| seal = computeWormSeal entry priorHash |
| in entry { entryCurrentHash = seal } |
|
|
| |
|
|
| initializeMemoryLog :: FilePath -> IO WormMemoryLog |
| initializeMemoryLog path = do |
| return WormMemoryLog |
| { logPath = path |
| , logEntries = [] |
| , logTailHash = BS.empty |
| , logSize = 0 |
| } |
|
|
| |
| |
|
|
| recordObservationWorm :: WormMemoryLog -> AgentId -> Observation -> IO WormMemoryLog |
| recordObservationWorm log aid obs = do |
| now <- getCurrentTime |
|
|
| let newIndex = logSize log |
| priorHash = logTailHash log |
| newEntry = createMemoryEntry newIndex now aid obs priorHash |
|
|
| |
| let updatedEntries = logEntries log ++ [newEntry] |
| updatedLog = log |
| { logEntries = updatedEntries |
| , logTailHash = entryCurrentHash newEntry |
| , logSize = newSize log + 1 |
| } |
|
|
| |
| persistEntry (logPath log) newEntry |
|
|
| return updatedLog |
|
|
| |
| persistEntry :: FilePath -> WormMemoryEntry -> IO () |
| persistEntry path entry = |
| let line = encodeWormEntryLine entry |
| in appendFile path (line ++ "\n") |
|
|
| |
| encodeWormEntryLine :: WormMemoryEntry -> String |
| encodeWormEntryLine entry = |
| let parts = |
| [ show (entryIndex entry) |
| , entryAgentId entry |
| , formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S%z" (entryTimestamp entry) |
| , show (entryData entry) |
| , BSC.unpack (entryPriorHash entry) |
| , BSC.unpack (entryCurrentHash entry) |
| ] |
| in intercalate "|" parts |
|
|
| |
| |
|
|
| verifyWormIntegrity :: WormMemoryLog -> Either String () |
| verifyWormIntegrity log = verifyChain (logEntries log) BS.empty |
| where |
| verifyChain [] _ = Right () |
| verifyChain (entry:rest) expectedPrior |
| | entryPriorHash entry /= expectedPrior = |
| Left $ "WORM chain broken at index " ++ show (entryIndex entry) |
| | otherwise = verifyChain rest (entryCurrentHash entry) |
|
|
| |
|
|
| |
| agentMemoryLog :: WormMemoryLog -> AgentId -> [Observation] |
| agentMemoryLog log aid = |
| [ entryData entry |
| | entry <- logEntries log |
| , entryAgentId entry == aid |
| ] |
|
|
| |
| observationsInRange :: WormMemoryLog -> UTCTime -> UTCTime -> [Observation] |
| observationsInRange log startTime endTime = |
| [ entryData entry |
| | entry <- logEntries log |
| , let ts = entryTimestamp entry |
| , ts >= startTime && ts <= endTime |
| ] |
|
|
| |
| getEntryAtIndex :: WormMemoryLog -> Int -> Maybe WormMemoryEntry |
| getEntryAtIndex log idx |
| | idx < 0 || idx >= length (logEntries log) = Nothing |
| | otherwise = Just (logEntries log !! idx) |
|
|
| |
|
|
| exportAuditTrail :: WormMemoryLog -> String |
| exportAuditTrail log = |
| let header = "βββ WORM Memory Audit Trail βββ\n" |
| stats = "Entries: " ++ show (logSize log) ++ " | Tail Hash: " ++ BSC.unpack (logTailHash log) ++ "\n\n" |
| entries = intercalate "\n" [ exportEntry e | e <- logEntries log ] |
| in header ++ stats ++ entries |
|
|
| exportEntry :: WormMemoryEntry -> String |
| exportEntry entry = |
| intercalate " | " |
| [ "[" ++ show (entryIndex entry) ++ "]" |
| , formatTime defaultTimeLocale "%H:%M:%S" (entryTimestamp entry) |
| , "Agent:" ++ entryAgentId entry |
| , "PriorHash:" ++ take 12 (BSC.unpack (entryPriorHash entry)) |
| , "CurrentHash:" ++ take 12 (BSC.unpack (entryCurrentHash entry)) |
| ] |
|
|
| |
|
|
| data MemoryStats = MemoryStats |
| { statsTotalEntries :: Int |
| , statsByAgent :: Map.Map AgentId Int |
| , statsOldestTime :: Maybe UTCTime |
| , statsNewestTime :: Maybe UTCTime |
| , statsChainIntegrity :: Bool |
| } deriving (Show, Eq, Generic) |
|
|
| computeMemoryStats :: WormMemoryLog -> MemoryStats |
| computeMemoryStats log = |
| let entries = logEntries log |
| byAgent = Map.fromListWith (+) [(entryAgentId e, 1) | e <- entries] |
| times = map entryTimestamp entries |
| oldest = if null times then Nothing else Just (minimum times) |
| newest = if null times then Nothing else Just (maximum times) |
| integrity = case verifyWormIntegrity log of |
| Right () -> True |
| Left _ -> False |
| in MemoryStats |
| { statsTotalEntries = length entries |
| , statsByAgent = byAgent |
| , statsOldestTime = oldest |
| , statsNewestTime = newest |
| , statsChainIntegrity = integrity |
| } |
|
|
| |
| |
|
|
| data MemorySnapshot = MemorySnapshot |
| { snapshotTimestamp :: UTCTime |
| , snapshotIndex :: Int |
| , snapshotHash :: ByteString |
| , snapshotAgent :: AgentId |
| } deriving (Show, Eq, Generic) |
|
|
| createSnapshot :: WormMemoryLog -> IO (Maybe MemorySnapshot) |
| createSnapshot log = do |
| now <- getCurrentTime |
| case logEntries log of |
| [] -> return Nothing |
| entries -> |
| let lastEntry = last entries |
| snapshot = MemorySnapshot |
| { snapshotTimestamp = now |
| , snapshotIndex = entryIndex lastEntry |
| , snapshotHash = entryCurrentHash lastEntry |
| , snapshotAgent = entryAgentId lastEntry |
| } |
| in return (Just snapshot) |
|
|
| |
| |
|
|
| verifyToCheckpoint :: WormMemoryLog -> MemorySnapshot -> Either String () |
| verifyToCheckpoint log snapshot = |
| let checkIndex = snapshotIndex snapshot |
| entries = logEntries log |
| headEntries = take (checkIndex + 1) entries |
| in if null entries |
| then Left "Empty log" |
| else if length entries < checkIndex + 1 |
| then Left "Checkpoint index out of range" |
| else verifyChain headEntries BS.empty |
| where |
| verifyChain [] _ = Right () |
| verifyChain (entry:rest) expectedPrior |
| | entryPriorHash entry /= expectedPrior = |
| Left $ "Chain broken at checkpoint index " ++ show (entryIndex entry) |
| | otherwise = verifyChain rest (entryCurrentHash entry) |
|
|
| |
|
|
| compactLog :: WormMemoryLog -> Int -> WormMemoryLog |
| compactLog log retentionDays = |
| |
| |
| log |
|
|
| |
| newSize :: WormMemoryLog -> Int |
| newSize log = logSize log |
|
|