File size: 22,529 Bytes
119e586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
-- ─────────────────────────────────────────────────────────────────────────────
-- SnapKitty Sovereign β€” Haskell Trust Deed Validator v2.0
-- LISP DSL β†’ Haskell enforcement β†’ Rust runtime
--
-- Rules are loaded from deed-rules.lisp (Funtan DSL) at startup.
-- Haskell enforces them. Rust calls this binary as a subprocess.
--
-- LiquidHaskell refinement annotations are present throughout.
-- Activate with: ghc -fplugin=LiquidHaskell (requires liquid-fixpoint + Z3)
-- Without LH installed, annotations are comments β€” smart constructors
-- provide equivalent runtime guarantees.
--
-- stdin protocol (9 lines):
--   1: agent_id
--   2: allowed_actions   (comma-separated)
--   3: restricted_actions (comma-separated)
--   4: trust_score       (float)
--   5: provided_seal     (64-char hex string)
--   6: expires_at        (unix timestamp seconds, integer)
--   7: status            (active|revoked|expired|pending-renewal)
--   8: escalation_authority (agent_id or "none")
--   9: spec_path         (path to deed-rules.lisp, or "default")
--
-- stdout: key=value pairs
-- ─────────────────────────────────────────────────────────────────────────────

{-# LANGUAGE LinearTypes #-}

module DeedValidator where

import Data.Char    (toLower, isHexDigit, isSpace, isDigit)
import Data.List    (intersect, nub, isPrefixOf, isSuffixOf)
import Data.Maybe   (mapMaybe, fromMaybe)
import System.IO    (hPutStrLn, stderr, hSetBuffering, stdout, BufferMode(..))
import Control.Exception (try, SomeException)

-- ─────────────────────────────────────────────────────────────────────────────
-- Minimal S-expression parser for deed-rules.lisp
-- ─────────────────────────────────────────────────────────────────────────────

data SExpr
    = Atom  String
    | SList [SExpr]
    deriving (Show, Eq)

parseSExprs :: String -> [SExpr]
parseSExprs input = fst (parseMany (stripComments input))
  where
    stripComments [] = []
    stripComments (';':rest) = stripComments (dropWhile (/= '\n') rest)
    stripComments (c:cs)     = c : stripComments cs

    parseMany :: String -> ([SExpr], String)
    parseMany s =
        let s' = dropWhile isSpace s
        in case s' of
            []       -> ([], [])
            (')':_)  -> ([], s')
            _        -> let (e, rest)  = parseOne s'
                            (es, rem') = parseMany rest
                        in (e:es, rem')

    parseOne :: String -> (SExpr, String)
    parseOne [] = (Atom "", [])
    parseOne ('(':rest) =
        let (children, after) = parseMany rest
            after' = case dropWhile isSpace after of
                       (')':xs) -> xs
                       xs       -> xs
        in (SList children, after')
    parseOne ('"':rest) =
        let (str, after) = break (== '"') rest
        in (Atom str, drop 1 after)
    parseOne s =
        let (tok, rest) = break (\c -> isSpace c || c == '(' || c == ')') s
        in (Atom tok, rest)

-- Extract a named list from a deed-spec s-expr
lookupList :: String -> SExpr -> Maybe [SExpr]
lookupList key (SList (Atom "deed-spec" : children)) =
    let find [] = Nothing
        find (SList (Atom k : vals) : rest)
            | k == key  = Just vals
            | otherwise = find rest
        find (_ : rest) = find rest
    in find children
lookupList key (SList children) =
    let find [] = Nothing
        find (SList (Atom k : vals) : rest)
            | k == key  = Just vals
            | otherwise = find rest
        find (_ : rest) = find rest
    in find children
lookupList _ _ = Nothing

lookupAtom :: String -> SExpr -> Maybe String
lookupAtom key spec = case lookupList key spec of
    Just (Atom v : _) -> Just v
    _                 -> Nothing

lookupFloat :: String -> SExpr -> Maybe Double
lookupFloat key spec = case lookupAtom key spec of
    Just s -> case reads s of { [(v,"")] -> Just v; _ -> Nothing }
    Nothing -> Nothing

lookupAtoms :: String -> SExpr -> [String]
lookupAtoms key spec = case lookupList key spec of
    Just atoms -> [v | Atom v <- atoms]
    Nothing    -> []

-- ─────────────────────────────────────────────────────────────────────────────
-- DeedSpec β€” loaded from deed-rules.lisp
-- ─────────────────────────────────────────────────────────────────────────────

data DeedSpec = DeedSpec
    { specTrustMin        :: Double    -- default 0.01
    , specTrustMax        :: Double    -- default 1.0
    , specSealMinLen      :: Int       -- default 64
    , specExpiryRequired  :: Bool      -- default True
    , specAuthorityModel  :: String    -- "role-based"
    , specAuthorityMinTrust :: Double  -- default 1.0
    , specStatusValues    :: [String]  -- valid status strings
    , specBlockedActions  :: [String]  -- globally forbidden
    , specSealMustCover   :: [String]  -- fields seal must cover
    } deriving (Show)

defaultDeedSpec :: DeedSpec
defaultDeedSpec = DeedSpec
    { specTrustMin         = 0.01
    , specTrustMax         = 1.0
    , specSealMinLen       = 64
    , specExpiryRequired   = True
    , specAuthorityModel   = "role-based"
    , specAuthorityMinTrust = 1.0
    , specStatusValues     = ["active", "expired", "revoked", "pending-renewal"]
    , specBlockedActions   = ["delete_ledger","modify_deed","override_all",
                               "jailbreak","ignore_previous"]
    , specSealMustCover    = ["agent-id","trust-score","expires-at","status",
                               "escalation-authority","allowed-actions","restricted-actions"]
    }

loadDeedSpec :: FilePath -> IO DeedSpec
loadDeedSpec path = do
    result <- try (readFile path) :: IO (Either SomeException String)
    case result of
        Left err -> do
            hPutStrLn stderr $ "[deed-validator] spec file not found: " ++ path
                             ++ " β€” using defaults. Error: " ++ show err
            return defaultDeedSpec
        Right content -> do
            let exprs = parseSExprs content
            case exprs of
                (spec:_) -> do
                    let ds = DeedSpec
                              { specTrustMin        = fromMaybe 0.01  (lookupFloat "trust-score-min"  spec)
                              , specTrustMax        = fromMaybe 1.0   (lookupFloat "trust-score-max"  spec)
                              , specSealMinLen      = fromMaybe 64    (fmap round (lookupFloat "seal-min-length" spec))
                              , specExpiryRequired  = fromMaybe True  (fmap (== "true") (lookupAtom "expiry-required" spec))
                              , specAuthorityModel  = fromMaybe "role-based" (lookupAtom "authority-model" spec)
                              , specAuthorityMinTrust = fromMaybe 1.0 (lookupFloat "authority-min-trust" spec)
                              , specStatusValues    = let vs = lookupAtoms "status-values" spec
                                                      in if null vs then specStatusValues defaultDeedSpec else vs
                              , specBlockedActions  = let bs = lookupAtoms "globally-blocked-actions" spec
                                                      in if null bs then specBlockedActions defaultDeedSpec else bs
                              , specSealMustCover   = let cs = lookupAtoms "seal-must-cover" spec
                                                      in if null cs then specSealMustCover defaultDeedSpec else cs
                              }
                    hPutStrLn stderr $ "[deed-validator] loaded spec v" ++
                                       fromMaybe "?" (lookupAtom "version" spec)
                    return ds
                [] -> do
                    hPutStrLn stderr "[deed-validator] empty spec file β€” using defaults"
                    return defaultDeedSpec

-- ─────────────────────────────────────────────────────────────────────────────
-- TrustDeed β€” full field set (matches Rust struct)
-- ─────────────────────────────────────────────────────────────────────────────

-- {-@ type TrustScore = {v:Double | v >= 0.01 && v <= 1.0} @-}
-- {-@ type ValidSeal  = {v:String | len v >= 64}           @-}
-- {-@ type FutureTime = {v:Int    | v > 0}                 @-}

newtype TrustScore = TrustScore Double deriving (Show)
newtype ValidSeal  = ValidSeal  String deriving (Show)

-- Smart constructor β€” invalid trust scores cannot be constructed
-- {-@ mkTrustScore :: DeedSpec -> Double -> Either String TrustScore @-}
mkTrustScore :: DeedSpec -> Double -> Either String TrustScore
mkTrustScore spec s
    | s < specTrustMin spec = Left $ "V3:TRUST_BELOW_MIN score=" ++ show s
    | s > specTrustMax spec = Left $ "V3:TRUST_ABOVE_MAX score=" ++ show s
    | otherwise             = Right (TrustScore s)

-- Smart constructor β€” short or non-hex seals cannot be constructed
-- {-@ mkValidSeal :: DeedSpec -> String -> Either String ValidSeal @-}
mkValidSeal :: DeedSpec -> String -> Either String ValidSeal
mkValidSeal spec seal
    | null seal                          = Left "V4:NO_SEAL"
    | length seal < specSealMinLen spec  = Left $ "V4:SEAL_TOO_SHORT len=" ++ show (length seal)
    | not (all isHexDigit seal)          = Left "V4:SEAL_NOT_HEX"
    | otherwise                          = Right (ValidSeal seal)

data TrustDeed = TrustDeed
    { deedAgent      :: String
    , deedAllowed    :: [String]
    , deedRestricted :: [String]
    , deedScore      :: TrustScore
    , deedSeal       :: ValidSeal
    , deedExpiresAt  :: Integer      -- unix timestamp
    , deedStatus     :: String       -- active|revoked|expired|pending-renewal
    , deedEscalation :: String       -- agent_id or "none"
    } deriving (Show)

newtype DeedToken = DeedToken { deedContent :: TrustDeed }

mkDeedToken :: TrustDeed -> DeedToken
mkDeedToken = DeedToken

-- ─────────────────────────────────────────────────────────────────────────────
-- Validation checks β€” each returns (pass/fail, message)
-- ─────────────────────────────────────────────────────────────────────────────

-- V1: Structure β€” required non-empty fields
checkStructure :: TrustDeed -> (Bool, String)
checkStructure d
    | null (deedAgent d)   = (False, "V1:EMPTY_AGENT_ID")
    | null (deedAllowed d) = (False, "V1:NO_ALLOWED_ACTIONS β€” deed must permit at least one action")
    | otherwise            = (True,  "V1:STRUCTURAL_OK")

-- V2: Disjoint sets β€” allowed ∩ restricted = βˆ…
checkDisjoint :: TrustDeed -> (Bool, String)
checkDisjoint d =
    let overlap = deedAllowed d `intersect` deedRestricted d
    in if null overlap
       then (True,  "V2:SETS_DISJOINT")
       else (False, "V2:OVERLAP_DETECTED actions=" ++ show overlap)

-- V3: Trust score β€” validated at construction time by mkTrustScore
-- This check confirms the constructed score is in spec range
checkTrustScore :: DeedSpec -> TrustDeed -> (Bool, String)
checkTrustScore spec d =
    let TrustScore s = deedScore d
    in if s >= specTrustMin spec && s <= specTrustMax spec
       then (True,  "V3:TRUST_IN_RANGE score=" ++ show s)
       else (False, "V3:TRUST_OUT_OF_RANGE score=" ++ show s)

-- V4: Seal format β€” validated at construction time by mkValidSeal
checkSealFormat :: DeedSpec -> TrustDeed -> (Bool, String)
checkSealFormat spec d =
    let ValidSeal seal = deedSeal d
    in if length seal >= specSealMinLen spec && all isHexDigit seal
       then (True,  "V4:SEAL_FORMAT_OK len=" ++ show (length seal))
       else (False, "V4:SEAL_FORMAT_FAIL")

-- V5: Authority model β€” role-based, NOT name-based
-- Any agent with trust_score = 1.0 has authority. No names hardcoded.
checkAuthorityModel :: DeedSpec -> TrustDeed -> (Bool, String)
checkAuthorityModel spec d =
    let TrustScore s = deedScore d
        claimsAuth   = s >= specAuthorityMinTrust spec
        model        = specAuthorityModel spec
    in if claimsAuth && model /= "role-based"
       then (False, "V5:AUTHORITY_MODEL_MISCONFIGURED β€” spec requires role-based")
       else (True,  "V5:AUTHORITY_MODEL_OK model=" ++ model
                   ++ if claimsAuth then " (claims authority)" else "")

-- V6: Expiration β€” deed must not be expired, status must be active
checkExpiration :: DeedSpec -> TrustDeed -> Integer -> (Bool, String)
checkExpiration spec d nowTs
    | specExpiryRequired spec && deedExpiresAt d <= 0
        = (False, "V6:EXPIRES_AT_MISSING")
    | deedExpiresAt d > 0 && nowTs > deedExpiresAt d
        = (False, "V6:DEED_EXPIRED expires_at=" ++ show (deedExpiresAt d))
    | deedStatus d `notElem` specStatusValues spec
        = (False, "V6:INVALID_STATUS status=" ++ deedStatus d)
    | deedStatus d /= "active"
        = (False, "V6:DEED_NOT_ACTIVE status=" ++ deedStatus d)
    | otherwise
        = (True,  "V6:DEED_ACTIVE status=" ++ deedStatus d)

-- V7: Seal coverage β€” verify the seal covers all integrity fields
-- We cannot recompute SHA-256 without cryptonite, but we verify that the
-- seal FIELD SET declared in the spec matches what Rust computes.
-- Full content verification happens in Rust's verify_integrity().
checkSealCoverage :: DeedSpec -> TrustDeed -> (Bool, String)
checkSealCoverage spec _d =
    let required = specSealMustCover spec
        covered  = ["agent-id","trust-score","expires-at","status",
                     "escalation-authority","allowed-actions","restricted-actions"]
        missing  = filter (`notElem` covered) required
    in if null missing
       then (True,  "V7:SEAL_COVERAGE_OK covers=" ++ show (length covered) ++ " fields")
       else (False, "V7:SEAL_COVERAGE_INCOMPLETE missing=" ++ show missing)

-- V8: Blocked actions β€” globally forbidden actions must not be allowed
checkBlockedActions :: DeedSpec -> TrustDeed -> (Bool, String)
checkBlockedActions spec d =
    let blocked  = map (map toLower) (specBlockedActions spec)
        present  = filter (\a -> any (`isPrefixOf` map toLower a) blocked) (deedAllowed d)
    in if null present
       then (True,  "V8:NO_BLOCKED_ACTIONS")
       else (False, "V8:BLOCKED_ACTION_IN_ALLOWED actions=" ++ show present)

-- ─────────────────────────────────────────────────────────────────────────────
-- Deed validation result
-- ─────────────────────────────────────────────────────────────────────────────

data DeedValidation = DeedValidation
    { dv_agent   :: String
    , dv_v1      :: (Bool, String)
    , dv_v2      :: (Bool, String)
    , dv_v3      :: (Bool, String)
    , dv_v4      :: (Bool, String)
    , dv_v5      :: (Bool, String)
    , dv_v6      :: (Bool, String)
    , dv_v7      :: (Bool, String)
    , dv_v8      :: (Bool, String)
    , dv_valid   :: Bool
    , dv_reason  :: String
    } deriving (Show)

-- Linear: DeedToken %1-> ensures one validation per token
validateDeed :: DeedSpec -> Integer -> DeedToken %1-> DeedValidation
validateDeed spec nowTs (DeedToken deed) =
    let v1 = checkStructure             deed
        v2 = checkDisjoint              deed
        v3 = checkTrustScore   spec     deed
        v4 = checkSealFormat   spec     deed
        v5 = checkAuthorityModel spec   deed
        v6 = checkExpiration   spec deed nowTs
        v7 = checkSealCoverage spec     deed
        v8 = checkBlockedActions spec   deed

        allChecks = [v1, v2, v3, v4, v5, v6, v7, v8]
        allOk     = all fst allChecks
        firstFail = head $ filter (not . fst) allChecks ++ [(True, "ALL_CHECKS_PASSED")]

    in DeedValidation
        { dv_agent  = deedAgent deed
        , dv_v1     = v1
        , dv_v2     = v2
        , dv_v3     = v3
        , dv_v4     = v4
        , dv_v5     = v5
        , dv_v6     = v6
        , dv_v7     = v7
        , dv_v8     = v8
        , dv_valid  = allOk
        , dv_reason = snd firstFail
        }

-- ─────────────────────────────────────────────────────────────────────────────
-- Output
-- ─────────────────────────────────────────────────────────────────────────────

printDeedValidation :: DeedValidation -> IO ()
printDeedValidation dv = do
    putStrLn $ "agent="   ++ dv_agent dv
    putStrLn $ "valid="   ++ if dv_valid dv then "true" else "false"
    putStrLn $ "reason="  ++ dv_reason dv
    putStrLn $ "v1="      ++ checkLine (dv_v1 dv)
    putStrLn $ "v2="      ++ checkLine (dv_v2 dv)
    putStrLn $ "v3="      ++ checkLine (dv_v3 dv)
    putStrLn $ "v4="      ++ checkLine (dv_v4 dv)
    putStrLn $ "v5="      ++ checkLine (dv_v5 dv)
    putStrLn $ "v6="      ++ checkLine (dv_v6 dv)
    putStrLn $ "v7="      ++ checkLine (dv_v7 dv)
    putStrLn $ "v8="      ++ checkLine (dv_v8 dv)
    putStrLn   "engine=haskell-deed-validator-lisp-v2"
  where
    checkLine (True,  msg) = "pass:" ++ msg
    checkLine (False, msg) = "fail:" ++ msg

-- ─────────────────────────────────────────────────────────────────────────────
-- Main
-- ─────────────────────────────────────────────────────────────────────────────

main :: IO ()
main = do
    hSetBuffering stdout LineBuffering
    agentId       <- getLine
    allowedStr    <- getLine
    restrictedStr <- getLine
    scoreLine     <- getLine
    sealStr       <- getLine
    expiresLine   <- getLine
    statusStr     <- getLine
    escalationStr <- getLine
    specPathStr   <- getLine

    let specPath  = if specPathStr == "default" || null specPathStr
                    then "bridges/lisp/deed-rules.lisp"
                    else specPathStr

    spec <- loadDeedSpec specPath

    -- Unix "now" β€” approximate from expires_at sanity (subprocess has no clock access easily)
    -- Rust passes current timestamp as 10th line if needed; for now use 0 (expiry check is advisory)
    let nowTs     = 0 :: Integer   -- Rust's verify_integrity does authoritative expiry check

    let scoreD    = read scoreLine :: Double
        expiresD  = read expiresLine :: Integer
        allowed   = splitOn ',' allowedStr
        restricted = splitOn ',' restrictedStr

    case (mkTrustScore spec scoreD, mkValidSeal spec sealStr) of
        (Left err, _) -> do
            putStrLn $ "agent="  ++ agentId
            putStrLn   "valid=false"
            putStrLn $ "reason=" ++ err
            putStrLn   "engine=haskell-deed-validator-lisp-v2"
        (_, Left err) -> do
            putStrLn $ "agent="  ++ agentId
            putStrLn   "valid=false"
            putStrLn $ "reason=" ++ err
            putStrLn   "engine=haskell-deed-validator-lisp-v2"
        (Right score, Right seal) -> do
            let deed  = TrustDeed
                          { deedAgent      = agentId
                          , deedAllowed    = filter (not . null) allowed
                          , deedRestricted = filter (not . null) restricted
                          , deedScore      = score
                          , deedSeal       = seal
                          , deedExpiresAt  = expiresD
                          , deedStatus     = map toLower statusStr
                          , deedEscalation = escalationStr
                          }
                token  = mkDeedToken deed
                result = validateDeed spec nowTs token
            printDeedValidation result

-- ─────────────────────────────────────────────────────────────────────────────
-- Utilities
-- ─────────────────────────────────────────────────────────────────────────────

splitOn :: Char -> String -> [String]
splitOn _ "" = [""]
splitOn delim str =
    let (word, rest) = break (== delim) str
    in word : case rest of
        []     -> []
        (_:xs) -> splitOn delim xs