Commita36db909Recorded2 Sep 2026Repositorysigil-system

Add the migration gate and its differential probe

Message

A migration's failure mode is SILENT DIVERGENCE, so the load-bearing arm compares decisions rather than inspecting code: the probe drives the PUBLIC API over 19 grant configurations x 23 operations, prints one line per observable decision, and the gate runs it twice -- once against this branch's module and once against the base module checked out over it -- requiring the two transcripts to be byte-identical. 460 decisions, 0 disagreements.

The corpus covers what the migration could plausibly break: sibling prefixes that must not leak, a symlink escaping its root, argv[0] exact vs basename, exec:full absorbing a later allowlist in both orders, a comma-bearing real directory, and argv[0] values containing a comma or a colon -- which must DENY rather than raise, since a check that throws where a caller expects #f reads as permitted.

The comparison carries its own positive control: a deliberately altered transcript must be detected, in the same run, or "identical" means only that diff was never able to speak.

PROVENANCE IS PROVEN BEHAVIOURALLY AND FROM BOTH SIDES, because none of this is trustworthy otherwise: (sigil grant) is unreleased, the build needs a dev redirect, dev-redirects.sgl is not picked up automatically, and Sigil is late-binding -- this module compiled clean against a locked sigil-stdlib 0.17.19 that contains no grant.sgl at all. A COMPILE line and a zero exit prove nothing here. So the gate asks the runtime to accept what only the fixed module accepts AND to refuse what it should still refuse; a merely permissive module fails the second.

The suite's one failure is NAMED rather than tolerated by count, and it was measured on the base module, not assumed: it is a missing runtime primitive, process-spawn-pipe-merged, unrelated to grants.

Structural arms hold the migration to its own claims: path-under? is gone rather than adapted, realpath and canonical-target are retained, and the export list is exactly the original 8 -- an addition is as much an API change as a removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DYn8wxSdjveEgK9NRxFcNA

Changed
 scripts/gate-system-grant-migration  | 280 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/grant-differential-probe.sgl | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 390 insertions(+)
Diff
scripts/gate-system-grant-migrationadded
@@ -0,0 +1,280 @@
+1
#!/bin/sh
+2
# gate-system-grant-migration - the gate for rebuilding
+3
# (sigil system grant) as a profile over (sigil grant).
+4
#
+5
# Outcomes, four distinct things:
+6
# MIGRATION COMPLETE PASS (0) every check ran and passed
+7
# MIGRATION COMPLETE FAIL (1) a check ran and failed
+8
# MIGRATION TIMED-OUT (2) a bounded step did not finish
+9
# MIGRATION SETUP-FAILED (3) the subject was never established
+10
#
+11
# A MIGRATION'S FAILURE MODE IS SILENT DIVERGENCE, so the load-bearing
+12
# arm is a DIFFERENTIAL: the migrated grant-check must agree with the
+13
# pre-migration one decision for decision. The agreement count is
+14
# printed and asserted non-zero, because "they agreed on nothing because
+15
# neither ran" and "they agreed on everything" are otherwise the same
+16
# colour.
+17
#
+18
# Scratch goes to /tmp. Nothing is written under the repo except build
+19
# output.
+20
+21
set -u
+22
+23
REPO="$(cd "$(dirname "$0")/.." && pwd)"
+24
SCRATCH="$(mktemp -d /tmp/gate-sysgrant.XXXXXX)"
+25
FIXTURE="$SCRATCH/fixture"
+26
TIMEOUT_S="${GATE_SYSGRANT_TIMEOUT:-900}"
+27
+28
SIGIL="${GATE_SYSGRANT_SIGIL:-/home/daviwil/Projects/Code/sigil/task-sigil-grant-p1-algebra/build/dev/bin/sigil}"
+29
REDIRECTS="${GATE_SYSGRANT_REDIRECTS:-/tmp/grant-dev-redirects.sgl}"
+30
PROBE="${GATE_SYSGRANT_PROBE:-$REPO/scripts/grant-differential-probe.sgl}"
+31
MODULE="$REPO/src/sigil/system/grant.sgl"
+32
BASE_REF="${GATE_SYSGRANT_BASE:-944b328}"
+33
+34
say() { echo "$@"; }
+35
rule() { say "------------------------------------------------------------"; }
+36
+37
doubt() {
+38
say ""
+39
say "BEFORE READING THIS AS A PRODUCT DEFECT: suspect this gate."
+40
say " Check that the sigil it used ($SIGIL) is the one you think, that"
+41
say " the redirect actually applied (step 2 proves it BEHAVIOURALLY --"
+42
say " a clean build proves nothing here, Sigil is late-binding), and"
+43
say " that the decision counts below are non-zero."
+44
}
+45
+46
dump_counts() {
+47
if [ -s "$SCRATCH/migrated.txt" ] || [ -s "$SCRATCH/baseline.txt" ]; then
+48
say ""
+49
say "--- instrument state at exit ---"
+50
say " baseline decision lines: $(grep -c '^CHK|' "$SCRATCH/baseline.txt" 2>/dev/null || echo 0)"
+51
say " migrated decision lines: $(grep -c '^CHK|' "$SCRATCH/migrated.txt" 2>/dev/null || echo 0)"
+52
say "--- end ---"
+53
fi
+54
}
+55
+56
setup_failed() {
+57
rule; say "SETUP-FAILED: $*"
+58
say "Nothing was tested. This is NOT a pass and NOT a failure."
+59
dump_counts; doubt
+60
say "Logs kept at: $SCRATCH"
+61
say "MIGRATION SETUP-FAILED"; exit 3
+62
}
+63
+64
red() {
+65
rule; say "FAILED: $*"
+66
dump_counts; doubt
+67
say "Logs kept at: $SCRATCH"
+68
say "MIGRATION COMPLETE FAIL"; exit 1
+69
}
+70
+71
timed_out() {
+72
rule; say "TIMED-OUT after ${TIMEOUT_S}s: $*"
+73
say ""
+74
say "CHECKS THAT DID NOT RUN:"
+75
say " - the differential (baseline vs migrated decisions)"
+76
say " - the existing test-system suite"
+77
dump_counts; doubt
+78
say "Logs kept at: $SCRATCH"
+79
say "MIGRATION TIMED-OUT"; exit 2
+80
}
+81
+82
restore_module() {
+83
git -C "$REPO" checkout HEAD -- src/sigil/system/grant.sgl 2>/dev/null
+84
}
+85
trap 'restore_module' EXIT HUP INT TERM
+86
+87
for t in awk grep sed timeout git sha256sum diff; do
+88
command -v "$t" >/dev/null 2>&1 || setup_failed "required tool '$t' is not on PATH"
+89
done
+90
[ -x "$SIGIL" ] || setup_failed "no sigil binary at $SIGIL"
+91
[ -f "$REDIRECTS" ] || setup_failed "no redirect file at $REDIRECTS -- (sigil grant) is unreleased, so the build needs one"
+92
[ -f "$PROBE" ] || setup_failed "no differential probe at $PROBE"
+93
[ -f "$MODULE" ] || setup_failed "module source missing: $MODULE"
+94
+95
# The module must be COMMITTED before this gate runs: the differential
+96
# checks the base version out over it and restores with `git checkout
+97
# HEAD --`, which restores to the LAST COMMIT. An uncommitted module
+98
# would be destroyed by that restore, and its absence afterwards looks
+99
# exactly like a clean restore.
+100
[ -z "$(git -C "$REPO" status --porcelain src/sigil/system/grant.sgl)" ] || setup_failed \
+101
"src/sigil/system/grant.sgl has uncommitted changes. This gate swaps it against $BASE_REF and restores with 'git checkout HEAD --', which would revert your work rather than the swap. Commit first."
+102
+103
rule
+104
say "GATE gate-system-grant-migration"
+105
say " repo: $REPO"
+106
say " sigil: $SIGIL"
+107
say " version: $("$SIGIL" --version 2>&1 | head -1)"
+108
say " redirects: $REDIRECTS"
+109
say " base ref: $BASE_REF ($(git -C "$REPO" rev-parse --short "$BASE_REF"))"
+110
rule
+111
+112
say "STEP 1/5 fixture"
+113
mkdir -p "$FIXTURE/allow" "$FIXTURE/allowed" "$FIXTURE/a,b" "$SCRATCH/outside"
+114
ln -sfn "$SCRATCH/outside" "$FIXTURE/link-out"
+115
[ -d "$FIXTURE/a,b" ] || setup_failed "could not create the comma-bearing fixture directory"
+116
[ -L "$FIXTURE/link-out" ] || setup_failed "could not create the escaping symlink"
+117
say " root: $FIXTURE (allow/ allowed/ 'a,b'/ link-out -> outside/)"
+118
+119
# PROVENANCE, behaviourally and from BOTH sides. The redirect is not
+120
# picked up automatically and has been seen not to apply, silently,
+121
# with the build still compiling clean -- Sigil is late-binding, so a
+122
# COMPILE line and a zero exit say nothing about which (sigil grant) is
+123
# in force. Two-sided so a merely permissive module cannot pass.
+124
say "STEP 2/5 provenance of (sigil grant)"
+125
P1="$("$SIGIL" eval '(begin (import (sigil grant)) (display (grant->specs (grant-from-specs (list "fs:rw:/x/a,b")))))' 2>&1)"
+126
case "$P1" in
+127
*"fs:rw:/x/a,b"*) say " accepts a comma-bearing fs path: ok" ;;
+128
*) setup_failed "the (sigil grant) in force REFUSES a comma-bearing fs path, so it predates the fix. Got: $P1" ;;
+129
esac
+130
P2="$("$SIGIL" eval '(begin (import (sigil grant)) (display (guard (e (#t "REFUSED")) (make-grant (list (list (quote exec) (quote allowlist) (list "a,b")))))))' 2>&1)"
+131
case "$P2" in
+132
*REFUSED*) say " still refuses a comma in an exec command: ok" ;;
+133
*) setup_failed "the (sigil grant) in force ACCEPTS a comma in an exec command; it is not the module under test. Got: $P2" ;;
+134
esac
+135
+136
say "STEP 3/5 differential vs $BASE_REF"
+137
MINE_HASH="$(sha256sum "$MODULE" | cut -d' ' -f1)"
+138
say " migrated module sha256: $MINE_HASH"
+139
+140
# `sigil eval` has NO --redirects and resolves the RELEASED dependency,
+141
# so it can see neither the redirected (sigil grant) nor this package's
+142
# own source. `sigil test <file>` is the package-aware runner that can.
+143
run_probe() {
+144
GRANT_DIFF_ROOT="$FIXTURE" timeout "$TIMEOUT_S" \
+145
"$SIGIL" test --redirects "$REDIRECTS" "$PROBE" > "$1.raw" 2>&1
+146
rc=$?
+147
[ "$rc" -ne 124 ] || timed_out "the differential probe did not finish"
+148
# Keep ONLY the probe's own lines, and say why rather than filtering
+149
# silently: the runner's chrome carries per-run durations and temp
+150
# paths, which differ between two runs of the SAME code and would
+151
# manufacture a divergence. The probe's prefixes are exactly the
+152
# observable decisions. The surviving count is asserted non-zero
+153
# below, so this filter cannot quietly empty the comparison.
+154
sed 's/\x1b\[[0-9;]*m//g' "$1.raw" \
+155
| grep -E '^(ADD|CHK|LOG|DECISIONS|GRANTS-PRED|PRINCIPAL)' > "$1"
+156
return "$rc"
+157
}
+158
+159
run_probe "$SCRATCH/migrated.txt" || {
+160
say "--- probe output tail ---"; tail -20 "$SCRATCH/migrated.txt"
+161
red "the differential probe failed against the MIGRATED module"
+162
}
+163
+164
git -C "$REPO" checkout "$BASE_REF" -- src/sigil/system/grant.sgl || setup_failed "could not check out the base module"
+165
BASE_HASH="$(sha256sum "$MODULE" | cut -d' ' -f1)"
+166
say " baseline module sha256: $BASE_HASH"
+167
[ "$BASE_HASH" != "$MINE_HASH" ] || setup_failed \
+168
"the baseline and migrated modules are the SAME FILE. The differential would compare a thing to itself and agree for the wrong reason."
+169
+170
run_probe "$SCRATCH/baseline.txt" || {
+171
say "--- probe output tail ---"; tail -20 "$SCRATCH/baseline.txt"
+172
restore_module
+173
red "the differential probe failed against the BASELINE module"
+174
}
+175
+176
restore_module
+177
NOW_HASH="$(sha256sum "$MODULE" | cut -d' ' -f1)"
+178
[ "$NOW_HASH" = "$MINE_HASH" ] || red \
+179
"the migrated module was NOT restored after the differential (hash $NOW_HASH, expected $MINE_HASH)"
+180
say " module restored, verified against its pre-swap hash"
+181
+182
MIG_N="$(grep -c '^CHK|' "$SCRATCH/migrated.txt")"
+183
BASE_N="$(grep -c '^CHK|' "$SCRATCH/baseline.txt")"
+184
say " decisions compared: migrated $MIG_N / baseline $BASE_N"
+185
[ "$MIG_N" -gt 0 ] || red "the migrated probe produced ZERO decisions; the differential compared nothing"
+186
[ "$BASE_N" -gt 0 ] || red "the baseline probe produced ZERO decisions; the differential compared nothing"
+187
[ "$MIG_N" -eq "$BASE_N" ] || red "the two probes produced different numbers of decisions ($MIG_N vs $BASE_N)"
+188
+189
if ! diff -u "$SCRATCH/baseline.txt" "$SCRATCH/migrated.txt" > "$SCRATCH/diff.txt" 2>&1; then
+190
say "--- divergence (baseline vs migrated) ---"
+191
head -40 "$SCRATCH/diff.txt"
+192
red "the migrated engine DISAGREES with the pre-migration engine"
+193
fi
+194
say " transcripts identical: $MIG_N decisions agree, 0 disagree"
+195
+196
# A differential that agrees is meaningful only if it COULD disagree.
+197
CTL="$SCRATCH/control.txt"
+198
sed 's/^CHK|pty|/CHK|PLANTED|/' "$SCRATCH/migrated.txt" > "$CTL"
+199
if diff -q "$SCRATCH/baseline.txt" "$CTL" >/dev/null 2>&1; then
+200
red "the differential's comparison is BLIND: a deliberately altered transcript still compared equal"
+201
fi
+202
say " comparison positive control: an altered transcript IS detected"
+203
+204
say "STEP 4/5 existing test-system suite"
+205
timeout "$TIMEOUT_S" "$SIGIL" test --redirects "$REDIRECTS" > "$SCRATCH/suite.log" 2>&1
+206
SRC=$?
+207
[ "$SRC" -ne 124 ] || timed_out "the test-system suite did not finish"
+208
SUM="$(sed 's/\x1b\[[0-9;]*m//g' "$SCRATCH/suite.log" | grep -E '^ Tests:' | tail -1)"
+209
[ -n "$SUM" ] || red "the suite printed no 'Tests:' summary line"
+210
PASSED="$(echo "$SUM" | sed -E 's/.*Tests:[[:space:]]*([0-9]+) passed.*/\1/')"
+211
TOTAL="$(echo "$SUM" | sed -E 's/.*\(([0-9]+) total\).*/\1/')"
+212
say " $PASSED passed of $TOTAL"
+213
[ "$TOTAL" -gt 0 ] || red "the suite reported 0 tests"
+214
+215
# The known pre-existing failure is NAMED, not tolerated by count. It is
+216
# a missing runtime primitive unrelated to grants, and it was MEASURED
+217
# on the base module rather than assumed.
+218
KNOWN="process sessions optionally merge stderr into captured output"
+219
FAILED=$((TOTAL - PASSED))
+220
if [ "$FAILED" -gt 0 ]; then
+221
# Take the names from the runner's "Failed tests:" block ONLY.
+222
# An earlier version matched `^ <any char> ` and so swallowed every
+223
# PASSING line too (they are " <checkmark> name"), reporting 37
+224
# passes as unexpected failures. Anchoring on the block is what makes
+225
# this a list of failures rather than a list of lines.
+226
UNEXPECTED="$(sed 's/\x1b\[[0-9;]*m//g' "$SCRATCH/suite.log" \
+227
| awk '/^ Failed tests:/{f=1;next} f&&/^━|^$/{f=0} f{print}' \
+228
| sed 's/^[[:space:]]*•[[:space:]]*//' \
+229
| grep -v "$KNOWN")"
+230
[ -z "$UNEXPECTED" ] || { say "--- unexpected failures ---"; echo "$UNEXPECTED"; red "$FAILED failing, and not all are the known pre-existing one"; }
+231
say " $FAILED failing, all of them the known pre-existing:"
+232
say " '$KNOWN'"
+233
say " (measured on the BASE module too -- it fails identically there)"
+234
fi
+235
+236
say "STEP 5/5 structural"
+237
CTLHITS="$(grep -c 'grant-check' "$MODULE")"
+238
[ "$CTLHITS" -gt 0 ] || setup_failed "structural grep is blind: 0 hits for a token that IS in $MODULE"
+239
say " grep positive control: $CTLHITS hits"
+240
+241
UNDER="$(grep -c 'path-under?' "$MODULE")"
+242
[ "$UNDER" -eq 0 ] || red "path-under? is still present ($UNDER refs). It was to be DELETED, not adapted -- if the algebra does not cover it, say so rather than keeping a second implementation."
+243
say " path-under? deleted: ok"
+244
+245
for keep in realpath canonical-target; do
+246
n="$(grep -c "$keep" "$MODULE")"
+247
[ "$n" -gt 0 ] || red "$keep is gone from the profile. Canonicalisation is this module's reason to exist; the algebra is pure and cannot do it."
+248
done
+249
say " realpath + canonical-target retained: ok"
+250
+251
# Extract the export block into a list of bare symbols and compare by
+252
# EXACT STRING. An earlier version pattern-matched `^ <sym>$` and
+253
# reported `grant-decisions` missing when it is plainly there — the last
+254
# export carries the closing paren (`grant-decisions)`). A false red,
+255
# caused by matching layout instead of content; symbols containing `?`
+256
# and `!` also make a regex the wrong tool here.
+257
# Range runs from `(export` to `(begin`. NOT to the first blank line:
+258
# the export block is grouped with blank lines between sections, so a
+259
# blank-line terminator captured 4 of 8 symbols and reported the rest
+260
# missing — a second false red from the same habit of matching layout.
+261
awk '/^ \(export/{f=1} /^ \(begin/{f=0} f' "$MODULE" \
+262
| sed -e 's/;;.*//' -e 's/[()]//g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
+263
| grep -v '^$' | grep -v '^export$' > "$SCRATCH/exports.txt"
+264
NEXP="$(wc -l < "$SCRATCH/exports.txt")"
+265
[ "$NEXP" -gt 0 ] || setup_failed "could not extract any export names from $MODULE"
+266
say " exports extracted: $NEXP"
+267
for sym in make-grants grant-allow-all grants? grants-principal grant-add! grant-check grant-assert! grant-decisions; do
+268
grep -qxF "$sym" "$SCRATCH/exports.txt" || red "export '$sym' is missing from the module's export list"
+269
done
+270
say " all 8 public exports present: ok"
+271
# And nothing NEW is exported: the API is unchanged in both directions,
+272
# so a consumer cannot start depending on something this migration
+273
# happened to expose.
+274
[ "$NEXP" -eq 8 ] || red "the module exports $NEXP symbols, not 8. The public API was to be UNCHANGED; an addition is as much a change as a removal."
+275
say " exactly 8, nothing added: ok"
+276
+277
rule
+278
say "Logs kept at: $SCRATCH"
+279
say "MIGRATION COMPLETE PASS"
+280
exit 0
scripts/grant-differential-probe.sgladded
@@ -0,0 +1,110 @@
+1
;;; Differential probe for the (sigil system grant) migration.
+2
;;;
+3
;;; Prints one line per observable decision. Run once against the
+4
;;; pre-migration module and once against the migrated one; the two
+5
;;; transcripts must be byte-identical. Every line is a decision the
+6
;;; PUBLIC API produced -- nothing here reaches inside the module.
+7
(import (sigil io)
+8
(sigil process)
+9
(sigil fs)
+10
(sigil path)
+11
(sigil string)
+12
(sigil system grant))
+13
+14
(define ROOT (getenv "GRANT_DIFF_ROOT"))
+15
+16
(define (outcome thunk)
+17
(guard (e (#t "RAISED"))
+18
(let ((v (thunk))) (if v "#t" "#f"))))
+19
+20
;; --- the grant configurations under test -------------------------
+21
;; Each is a list of specs handed to grant-add! in order. Ordering
+22
;; matters for the exec-full-absorbs-allowlist case, so both orders
+23
;; are present.
+24
(define configs
+25
(list
+26
(cons "empty" '())
+27
(cons "pty" (list "pty:on"))
+28
(cons "exec-allow" (list "exec:allowlist:ls,git"))
+29
(cons "exec-full" (list "exec:full"))
+30
(cons "full-then-al" (list "exec:full" "exec:allowlist:ls"))
+31
(cons "al-then-full" (list "exec:allowlist:ls" "exec:full"))
+32
(cons "two-allow" (list "exec:allowlist:ls" "exec:allowlist:git"))
+33
(cons "fs-ro-root" (list (string-append "fs:ro:" ROOT)))
+34
(cons "fs-rw-root" (list (string-append "fs:rw:" ROOT)))
+35
(cons "fs-rw-allow" (list (string-append "fs:rw:" (path-join ROOT "allow"))))
+36
(cons "fs-both" (list (string-append "fs:ro:" ROOT)
+37
(string-append "fs:rw:" (path-join ROOT "allow"))))
+38
(cons "fs-nested" (list (string-append "fs:rw:" ROOT)
+39
(string-append "fs:ro:" (path-join ROOT "allow"))))
+40
(cons "comma-dir" (list (string-append "fs:rw:" (path-join ROOT "a,b"))))
+41
(cons "bad-relative" (list "fs:rw:relative/path"))
+42
(cons "bad-missing" (list (string-append "fs:rw:" (path-join ROOT "no-such-dir"))))
+43
(cons "bad-mode" (list "fs:xx:/tmp"))
+44
(cons "bad-spec" (list "nonsense"))
+45
(cons "bad-kind-net" (list "net:connect:*"))
+46
(cons "bad-kind-eval" (list "eval:on"))))
+47
+48
;; --- the operations checked against each ------------------------
+49
(define (ops)
+50
(list
+51
(list 'pty #t)
+52
(list 'exec "ls")
+53
(list 'exec "git")
+54
(list 'exec "rm")
+55
(list 'exec "/tmp/evil/ls") ; basename must NOT match
+56
(list 'exec "") ; empty argv[0]
+57
(list 'exec "we,ird") ; a comma: must DENY, never raise
+58
(list 'exec "we:ird") ; a colon: must DENY, never raise
+59
(list 'fs-read ROOT)
+60
(list 'fs-write ROOT)
+61
(list 'fs-read (path-join ROOT "allow"))
+62
(list 'fs-write (path-join ROOT "allow" "x"))
+63
(list 'fs-read (path-join ROOT "allowed" "x")) ; sibling prefix, must NOT leak
+64
(list 'fs-write (path-join ROOT "allowed" "x"))
+65
(list 'fs-read (path-join ROOT "a,b" "f")) ; comma-bearing real dir
+66
(list 'fs-write (path-join ROOT "a,b" "f"))
+67
(list 'fs-read (path-join ROOT "link-out")) ; symlink escaping the root
+68
(list 'fs-write (path-join ROOT "link-out" "x"))
+69
(list 'fs-read "/etc/hosts")
+70
(list 'fs-write "/etc/passwd")
+71
(list 'fs-read (path-join ROOT "nope")) ; nonexistent read
+72
(list 'fs-write (path-join ROOT "nope")) ; nonexistent write (parent exists)
+73
(list 'unknown-op "x")))
+74
+75
(define decisions 0)
+76
+77
(for-each
+78
(lambda (cfg)
+79
(let ((name (car cfg))
+80
(specs (cdr cfg)))
+81
;; grant-add! outcomes are observable behaviour too.
+82
(let ((g (make-grants principal: "diff")))
+83
(for-each
+84
(lambda (s)
+85
(println "ADD|~a|~a" name (outcome (lambda () (grant-add! g s) #t))))
+86
specs)
+87
(for-each
+88
(lambda (o)
+89
(set! decisions (+ decisions 1))
+90
(println "CHK|~a|~a|~s|~a" name (car o) (cadr o)
+91
(outcome (lambda () (grant-check g (car o) (cadr o))))))
+92
(ops))
+93
;; the audit log must keep counting the same way
+94
(println "LOG|~a|~a" name (length (grant-decisions g))))))
+95
configs)
+96
+97
;; The allow-all posture is constructed differently and gets its own pass.
+98
(let ((g (grant-allow-all principal: "diff")))
+99
(for-each
+100
(lambda (o)
+101
(set! decisions (+ decisions 1))
+102
(println "CHK|allow-all|~a|~s|~a" (car o) (cadr o)
+103
(outcome (lambda () (grant-check g (car o) (cadr o))))))
+104
(ops)))
+105
+106
(println "GRANTS-PRED|~a|~a"
+107
(outcome (lambda () (grants? (make-grants))))
+108
(outcome (lambda () (grants? (vector 'nope)))))
+109
(println "PRINCIPAL|~a" (grants-principal (make-grants principal: "who")))
+110
(println "DECISIONS ~a" decisions)