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
|