@junghanacs/entwurf 0.12.9 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +135 -248
- package/BASELINE.md +46 -7
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +4 -2
- package/DELIVERY.md +5 -4
- package/README.md +46 -18
- package/VERIFY.md +10 -6
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +10 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
- package/mcp/entwurf-bridge/src/index.ts +190 -82
- package/package.json +7 -3
- package/pi/settings.reference.json +1 -1
- package/pi-extensions/acp-provider.ts +20 -10
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/augment.ts +60 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
- package/pi-extensions/lib/acp/backend.ts +5 -1
- package/pi-extensions/lib/acp/config.ts +19 -5
- package/pi-extensions/lib/acp/engraving.ts +3 -1
- package/pi-extensions/lib/acp/event-mapper.ts +10 -3
- package/pi-extensions/lib/acp/models.ts +69 -7
- package/pi-extensions/lib/acp/overlay.ts +234 -3
- package/pi-extensions/lib/acp/tool-surface.ts +12 -4
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
- package/pi-extensions/lib/meta-session.ts +17 -20
- package/run.sh +200 -62
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-cortex.ts +668 -0
- package/scripts/check-acp-provider-surface.ts +50 -6
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +64 -1
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +7 -7
- package/scripts/check-entwurf-facts.ts +12 -12
- package/scripts/check-entwurf-resume-args.ts +22 -60
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +5 -5
- package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-gate-qualification.ts +787 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-probe-cli-shim.ts +879 -0
- package/scripts/check-probe-ordering.ts +2450 -0
- package/scripts/check-shell-quote.ts +5 -4
- package/scripts/fixtures/probe-cli-shim +20 -0
- package/scripts/fixtures/probe-mcp-server.ts +168 -12
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/lib/probe-acp-turn.ts +207 -0
- package/scripts/lib/probe-cli-shim.ts +464 -0
- package/scripts/lib/probe-cli-target.ts +165 -0
- package/scripts/lib/probe-event-log.ts +383 -0
- package/scripts/lib/probe-verdict.ts +1213 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/acp-cortex.json +196 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/probe-ordering.json +1032 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/smoke-acp-cortex-live.ts +392 -0
- package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -266
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check-agy-permission-matrix — the CONTRACT SPACE of the agy permission engine,
|
|
3
|
+
enumerated as a literal table instead of an appended case list.
|
|
4
|
+
|
|
5
|
+
Why this exists (2026-07-27, the qualification design): every permission defect this
|
|
6
|
+
cut closed — the shallow install parser, uninstall's validate-after-mutate, the
|
|
7
|
+
path-only state doctor, within-list broad-first — was a cell in a matrix nobody had
|
|
8
|
+
enumerated. smoke-agy-install-state grew by appending the cell that had just failed;
|
|
9
|
+
this gate states the axes up front and exhausts the meaningful product, so an empty
|
|
10
|
+
cell is a visible decision, never silence.
|
|
11
|
+
|
|
12
|
+
Axes (operation-scoped, per reduction rules R1-R4 below):
|
|
13
|
+
parser-state none | v1 | v2(ours/theirs/mixed) | malformed{unknown-version,
|
|
14
|
+
empty-provenance, not-json, shape}
|
|
15
|
+
operation permission-install | permission-uninstall | permission-doctor |
|
|
16
|
+
permission-state-doctor
|
|
17
|
+
settings present | absent | malformed (+ wrong-typed containers)
|
|
18
|
+
ownership install: provenance capture {none, all, mixed pre-existed}
|
|
19
|
+
uninstall: removal split {ours, theirs, mixed} + container ownership
|
|
20
|
+
doctor: (composed in the shell layer — see R6)
|
|
21
|
+
precedence doctor only: {allow, ask, deny} x {exact, broad} x {same-list,
|
|
22
|
+
cross-list}
|
|
23
|
+
|
|
24
|
+
Reduction rules — an EXCLUDED cell is a stated decision, printed at the end:
|
|
25
|
+
R1 Both malformed representatives (unknown-version, empty-provenance) ride EVERY
|
|
26
|
+
mutating operation separately — the shallow-parser defect was exactly a second
|
|
27
|
+
parser diverging on them. Expectations agree (REFUSE + byte-identical); cells
|
|
28
|
+
stay separate.
|
|
29
|
+
R2 Under settings in {absent, malformed}, ownership x precedence is undefined and
|
|
30
|
+
NOT expanded; those cells instead assert validate-before-mutate (a refusal
|
|
31
|
+
leaves settings AND state byte-identical).
|
|
32
|
+
R3 Ownership is enumerated per operation meaning, never as a blind product.
|
|
33
|
+
R4 Precedence applies to permission-doctor only — install/uninstall never read it.
|
|
34
|
+
R5 EXCLUDED: symlink targets (settings/state/config) — lifecycle + filesystem
|
|
35
|
+
refusal is owned by smoke-agy-install-state.
|
|
36
|
+
R6 EXCLUDED: shell verdict composition (DRIFT/NOTE/ORPHANED rendering, doctor exit
|
|
37
|
+
codes, agy-bridge.sh wiring) — owned by smoke-agy-install-state.
|
|
38
|
+
R7 EXCLUDED: the MCP-config install lane (mcpServers key) — same owner.
|
|
39
|
+
|
|
40
|
+
Oracle independence: every expectation below is a hand-written literal. Nothing is
|
|
41
|
+
read back from the SUT (`permission-rules` output is deliberately NOT consulted).
|
|
42
|
+
|
|
43
|
+
Offline, deterministic, stdlib only. All writes stay inside one mkdtemp sandbox.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import hashlib
|
|
47
|
+
import json
|
|
48
|
+
import os
|
|
49
|
+
import re
|
|
50
|
+
import shutil
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
import tempfile
|
|
54
|
+
|
|
55
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
56
|
+
CONFIG = os.path.join(REPO, "scripts", "agy-bridge-config.py")
|
|
57
|
+
|
|
58
|
+
# The three rules, LITERALLY. Copied by hand on purpose: if ALLOW_RULES drifts, this
|
|
59
|
+
# gate must go red, not follow it.
|
|
60
|
+
R_V2 = "mcp(entwurf-bridge/entwurf_v2)"
|
|
61
|
+
R_PEERS = "mcp(entwurf-bridge/entwurf_peers)"
|
|
62
|
+
R_SELF = "mcp(entwurf-bridge/entwurf_self)"
|
|
63
|
+
OURS = [R_V2, R_PEERS, R_SELF]
|
|
64
|
+
BROAD_SERVER = "mcp(entwurf-bridge)"
|
|
65
|
+
BROAD_STAR = "mcp(*)"
|
|
66
|
+
OPERATOR = "command(*)"
|
|
67
|
+
|
|
68
|
+
passed = 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ok(label):
|
|
72
|
+
global passed
|
|
73
|
+
sys.stdout.write(" ok %s\n" % label)
|
|
74
|
+
passed += 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def die(label, detail):
|
|
78
|
+
sys.stderr.write("FAIL: %s\n %s\n" % (label, detail))
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sha(path):
|
|
83
|
+
if not os.path.exists(path):
|
|
84
|
+
return "ABSENT"
|
|
85
|
+
with open(path, "rb") as fh:
|
|
86
|
+
return hashlib.sha256(fh.read()).hexdigest()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── fixture builders (parser-state axis) ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def state_v1(settings_path, existed=False):
|
|
93
|
+
return {
|
|
94
|
+
"schemaVersion": 1,
|
|
95
|
+
"managedSettingsPath": settings_path,
|
|
96
|
+
"rule": R_V2,
|
|
97
|
+
"detectMode": "adopt-regular-file",
|
|
98
|
+
"settingsExistedBefore": True,
|
|
99
|
+
"permissionsExistedBefore": True,
|
|
100
|
+
"allowExistedBefore": True,
|
|
101
|
+
"ruleExistedBefore": existed,
|
|
102
|
+
"installedAt": "2026-07-26T00:00:18Z",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def state_v2(settings_path, existed_map, allow_existed=True, perms_existed=True, settings_existed=True, detect="adopt-regular-file"):
|
|
107
|
+
return {
|
|
108
|
+
"schemaVersion": 2,
|
|
109
|
+
"managedSettingsPath": settings_path,
|
|
110
|
+
"rules": OURS,
|
|
111
|
+
"detectMode": detect,
|
|
112
|
+
"settingsExistedBefore": settings_existed,
|
|
113
|
+
"permissionsExistedBefore": perms_existed,
|
|
114
|
+
"allowExistedBefore": allow_existed,
|
|
115
|
+
"rulesExistedBefore": existed_map,
|
|
116
|
+
"installedAt": "2026-07-27T00:00:00Z",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
ALL_OURS = {R_V2: False, R_PEERS: False, R_SELF: False}
|
|
121
|
+
ALL_THEIRS = {R_V2: True, R_PEERS: True, R_SELF: True}
|
|
122
|
+
MIXED = {R_V2: True, R_PEERS: False, R_SELF: False}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_cell(cell):
|
|
126
|
+
sandbox = tempfile.mkdtemp(prefix="agy-perm-matrix-")
|
|
127
|
+
try:
|
|
128
|
+
settings = os.path.join(sandbox, "settings.json")
|
|
129
|
+
state = os.path.join(sandbox, "permission-state.json")
|
|
130
|
+
|
|
131
|
+
st_fixture = cell.get("state")
|
|
132
|
+
if callable(st_fixture):
|
|
133
|
+
st_fixture = st_fixture(settings)
|
|
134
|
+
if st_fixture is not None:
|
|
135
|
+
with open(state, "w") as fh:
|
|
136
|
+
fh.write(st_fixture if isinstance(st_fixture, str) else json.dumps(st_fixture, indent=2))
|
|
137
|
+
|
|
138
|
+
se_fixture = cell.get("settings")
|
|
139
|
+
if se_fixture is not None:
|
|
140
|
+
with open(settings, "w") as fh:
|
|
141
|
+
fh.write(se_fixture if isinstance(se_fixture, str) else json.dumps(se_fixture, indent=2))
|
|
142
|
+
|
|
143
|
+
pre_settings, pre_state = sha(settings), sha(state)
|
|
144
|
+
|
|
145
|
+
op = cell["op"]
|
|
146
|
+
if op == "permission-install":
|
|
147
|
+
argv = [sys.executable, CONFIG, op, settings, state]
|
|
148
|
+
elif op == "permission-uninstall":
|
|
149
|
+
argv = [sys.executable, CONFIG, op, state]
|
|
150
|
+
elif op == "permission-doctor":
|
|
151
|
+
argv = [sys.executable, CONFIG, op, settings]
|
|
152
|
+
elif op == "permission-state-doctor":
|
|
153
|
+
argv = [sys.executable, CONFIG, op, state]
|
|
154
|
+
else:
|
|
155
|
+
die(cell["id"], "unknown op %s" % op)
|
|
156
|
+
proc = subprocess.run(argv, capture_output=True, text=True, timeout=30)
|
|
157
|
+
|
|
158
|
+
exp = cell["expect"]
|
|
159
|
+
# P0-4: a nonzero cell without a hand-written stderr expectation is a hole in
|
|
160
|
+
# the oracle — the refusal REASON is part of the contract, not just the rc.
|
|
161
|
+
if exp["rc"] != 0 and "stderr_token" not in exp:
|
|
162
|
+
die("table integrity", "%s is a nonzero cell without a stderr_token" % cell["id"])
|
|
163
|
+
if proc.returncode != exp["rc"]:
|
|
164
|
+
die(cell["id"], "rc=%d want %d\nstdout=%s\nstderr=%s" % (proc.returncode, exp["rc"], proc.stdout, proc.stderr))
|
|
165
|
+
if "stdout" in exp and proc.stdout.strip() != exp["stdout"]:
|
|
166
|
+
die(cell["id"], "stdout=%r want %r" % (proc.stdout.strip(), exp["stdout"]))
|
|
167
|
+
if "stdout_prefix" in exp and not proc.stdout.strip().startswith(exp["stdout_prefix"]):
|
|
168
|
+
die(cell["id"], "stdout=%r want prefix %r" % (proc.stdout.strip(), exp["stdout_prefix"]))
|
|
169
|
+
if "stderr_token" in exp and exp["stderr_token"] not in proc.stderr:
|
|
170
|
+
die(cell["id"], "stderr=%r missing hand-written token %r" % (proc.stderr, exp["stderr_token"]))
|
|
171
|
+
|
|
172
|
+
if exp.get("byte_identical"):
|
|
173
|
+
if sha(settings) != pre_settings:
|
|
174
|
+
die(cell["id"], "REFUSAL MUTATED settings (validate-before-mutate broken)")
|
|
175
|
+
if sha(state) != pre_state:
|
|
176
|
+
die(cell["id"], "REFUSAL MUTATED the state record (validate-before-mutate broken)")
|
|
177
|
+
|
|
178
|
+
if "settings_allow_after" in exp:
|
|
179
|
+
want = exp["settings_allow_after"]
|
|
180
|
+
if want is None:
|
|
181
|
+
if os.path.exists(settings):
|
|
182
|
+
die(cell["id"], "settings file should be removed, still present")
|
|
183
|
+
else:
|
|
184
|
+
data = json.load(open(settings))
|
|
185
|
+
got = (data.get("permissions") or {}).get("allow")
|
|
186
|
+
if got != want:
|
|
187
|
+
die(cell["id"], "allow=%r want %r" % (got, want))
|
|
188
|
+
if "settings_content" in exp:
|
|
189
|
+
data = json.load(open(settings))
|
|
190
|
+
if data != exp["settings_content"]:
|
|
191
|
+
die(cell["id"], "settings=%r want %r" % (data, exp["settings_content"]))
|
|
192
|
+
|
|
193
|
+
if "state_exact" in exp:
|
|
194
|
+
# P0-4: successful state writes are pinned by EXACT keyset + every stable
|
|
195
|
+
# field + installedAt format — a subset comparison would let an unexpected
|
|
196
|
+
# key or field drift ride green.
|
|
197
|
+
data = json.load(open(state))
|
|
198
|
+
want = dict(exp["state_exact"])
|
|
199
|
+
if want.get("managedSettingsPath") == "__SETTINGS__":
|
|
200
|
+
want["managedSettingsPath"] = settings
|
|
201
|
+
if set(data.keys()) != set(want.keys()) | {"installedAt"}:
|
|
202
|
+
die(cell["id"], "state keyset %r != %r + installedAt" % (sorted(data), sorted(want)))
|
|
203
|
+
for key, val in want.items():
|
|
204
|
+
if data[key] != val:
|
|
205
|
+
die(cell["id"], "state[%s]=%r want %r" % (key, data[key], val))
|
|
206
|
+
if not re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", data["installedAt"]):
|
|
207
|
+
die(cell["id"], "installedAt %r is not the recorded UTC format" % data["installedAt"])
|
|
208
|
+
if "state_after" in exp:
|
|
209
|
+
want = exp["state_after"]
|
|
210
|
+
if want == "removed":
|
|
211
|
+
if os.path.exists(state):
|
|
212
|
+
die(cell["id"], "state record should be removed, still present")
|
|
213
|
+
elif want == "survives":
|
|
214
|
+
if not os.path.exists(state):
|
|
215
|
+
die(cell["id"], "state record should SURVIVE the refusal, was deleted")
|
|
216
|
+
else:
|
|
217
|
+
data = json.load(open(state))
|
|
218
|
+
for key, val in want.items():
|
|
219
|
+
if data.get(key) != val:
|
|
220
|
+
die(cell["id"], "state[%s]=%r want %r" % (key, data.get(key), val))
|
|
221
|
+
ok(cell["id"])
|
|
222
|
+
finally:
|
|
223
|
+
shutil.rmtree(sandbox, ignore_errors=True)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── the table ───────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
CELLS = [
|
|
229
|
+
# ═ permission-install: settings axis x parser-state axis x provenance capture ═
|
|
230
|
+
{
|
|
231
|
+
"id": "I01 install: settings absent + no prior state -> created-new, 3 rules, provenance all-ours",
|
|
232
|
+
"op": "permission-install", "state": None, "settings": None,
|
|
233
|
+
"expect": {"rc": 0, "stdout_prefix": "added " + R_V2,
|
|
234
|
+
"settings_allow_after": OURS,
|
|
235
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
236
|
+
"rules": OURS, "detectMode": "created-new",
|
|
237
|
+
"settingsExistedBefore": False, "permissionsExistedBefore": False,
|
|
238
|
+
"allowExistedBefore": False, "rulesExistedBefore": ALL_OURS}},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"id": "I02 install: empty settings object -> containers created, provenance all-ours",
|
|
242
|
+
"op": "permission-install", "state": None, "settings": {},
|
|
243
|
+
"expect": {"rc": 0, "settings_allow_after": OURS,
|
|
244
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
245
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
246
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": False,
|
|
247
|
+
"allowExistedBefore": False, "rulesExistedBefore": ALL_OURS}},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"id": "I03 install: all three pre-existed -> already-present, provenance all-theirs",
|
|
251
|
+
"op": "permission-install", "state": None,
|
|
252
|
+
"settings": {"permissions": {"allow": [R_V2, R_PEERS, R_SELF]}},
|
|
253
|
+
"expect": {"rc": 0, "stdout": "already-present %s %s %s" % (R_V2, R_PEERS, R_SELF),
|
|
254
|
+
"settings_allow_after": OURS,
|
|
255
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
256
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
257
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": True,
|
|
258
|
+
"allowExistedBefore": True, "rulesExistedBefore": ALL_THEIRS}},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": "I04 install: mixed pre-existence -> per-rule provenance, appended after operator rules",
|
|
262
|
+
"op": "permission-install", "state": None,
|
|
263
|
+
"settings": {"permissions": {"allow": [R_V2, OPERATOR]}},
|
|
264
|
+
"expect": {"rc": 0,
|
|
265
|
+
"settings_allow_after": [R_V2, OPERATOR, R_PEERS, R_SELF],
|
|
266
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
267
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
268
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": True,
|
|
269
|
+
"allowExistedBefore": True, "rulesExistedBefore": MIXED}},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": "I05 install: idempotent re-run over our own write does NOT re-attribute (provenance carried)",
|
|
273
|
+
"op": "permission-install",
|
|
274
|
+
"state": lambda s: state_v2(s, ALL_OURS),
|
|
275
|
+
"settings": {"permissions": {"allow": [R_V2, R_PEERS, R_SELF]}},
|
|
276
|
+
"expect": {"rc": 0, "settings_allow_after": OURS,
|
|
277
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
278
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
279
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": True,
|
|
280
|
+
"allowExistedBefore": True, "rulesExistedBefore": ALL_OURS}},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"id": "I06 install: v1 prior migrates in place (v1 rule stays ours, new rules read from disk)",
|
|
284
|
+
"op": "permission-install",
|
|
285
|
+
"state": lambda s: state_v1(s, existed=False),
|
|
286
|
+
"settings": {"permissions": {"allow": [R_V2, OPERATOR]}},
|
|
287
|
+
"expect": {"rc": 0,
|
|
288
|
+
"settings_allow_after": [R_V2, OPERATOR, R_PEERS, R_SELF],
|
|
289
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
290
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
291
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": True,
|
|
292
|
+
"allowExistedBefore": True,
|
|
293
|
+
"rulesExistedBefore": {R_V2: False, R_PEERS: False, R_SELF: False}}},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"id": "I07 install: re-targeted prior (different managedSettingsPath) is a fresh install for this path",
|
|
297
|
+
"op": "permission-install",
|
|
298
|
+
"state": lambda s: state_v2("/somewhere/else/settings.json", ALL_THEIRS),
|
|
299
|
+
"settings": {"permissions": {"allow": []}},
|
|
300
|
+
"expect": {"rc": 0, "settings_allow_after": OURS,
|
|
301
|
+
"state_exact": {"schemaVersion": 2, "managedSettingsPath": "__SETTINGS__",
|
|
302
|
+
"rules": OURS, "detectMode": "adopt-regular-file",
|
|
303
|
+
"settingsExistedBefore": True, "permissionsExistedBefore": True,
|
|
304
|
+
"allowExistedBefore": True, "rulesExistedBefore": ALL_OURS}},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"id": "I08 install: R1a unknown-version prior -> REFUSE rc4, settings AND state byte-identical",
|
|
308
|
+
"op": "permission-install",
|
|
309
|
+
"state": lambda s: {"schemaVersion": 99, "managedSettingsPath": s},
|
|
310
|
+
"settings": {"permissions": {"allow": [OPERATOR]}},
|
|
311
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
312
|
+
"stderr_token": "prior permission install-state is unusable — unknown schemaVersion 99"},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "I09 install: R1b empty-provenance prior -> REFUSE rc4, settings AND state byte-identical",
|
|
316
|
+
"op": "permission-install",
|
|
317
|
+
"state": lambda s: {"schemaVersion": 2, "managedSettingsPath": s, "rules": OURS, "rulesExistedBefore": {}},
|
|
318
|
+
"settings": {"permissions": {"allow": [OPERATOR]}},
|
|
319
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
320
|
+
"stderr_token": "no boolean provenance for"},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"id": "I10 install: not-json prior state -> REFUSE rc4, nothing written",
|
|
324
|
+
"op": "permission-install", "state": "not-json{{{",
|
|
325
|
+
"settings": {"permissions": {"allow": [OPERATOR]}},
|
|
326
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
327
|
+
"stderr_token": "is not valid JSON"},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"id": "I11 install: malformed settings JSON -> REFUSE rc4 before any write (R2)",
|
|
331
|
+
"op": "permission-install", "state": None, "settings": "this is not json{{{",
|
|
332
|
+
"expect": {"rc": 4, "byte_identical": True, "stderr_token": "is not valid JSON"},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"id": "I12 install: wrong-typed permissions container -> REFUSE rc4, byte-identical",
|
|
336
|
+
"op": "permission-install", "state": None, "settings": {"permissions": []},
|
|
337
|
+
"expect": {"rc": 4, "byte_identical": True, "stderr_token": "permissions must be a JSON object"},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"id": "I13 install: wrong-typed allow container -> REFUSE rc4, byte-identical",
|
|
341
|
+
"op": "permission-install", "state": None, "settings": {"permissions": {"allow": "everything"}},
|
|
342
|
+
"expect": {"rc": 4, "byte_identical": True, "stderr_token": "permissions.allow must be a JSON array"},
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
# ═ permission-uninstall: parser-state x settings x removal split x containers ═
|
|
346
|
+
{
|
|
347
|
+
"id": "U01 uninstall: no state -> rc2, nothing to undo",
|
|
348
|
+
"op": "permission-uninstall", "state": None, "settings": {"permissions": {"allow": OURS}},
|
|
349
|
+
"expect": {"rc": 2, "byte_identical": True, "stderr_token": "no permission install-state at"},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"id": "U02 uninstall: not-json state -> rc4 refusal",
|
|
353
|
+
"op": "permission-uninstall", "state": "not-json{{{", "settings": {"permissions": {"allow": OURS}},
|
|
354
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
355
|
+
"stderr_token": "is not valid JSON"},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"id": "U03 uninstall: state without managedSettingsPath -> rc4 refusal, state survives",
|
|
359
|
+
"op": "permission-uninstall", "state": {"schemaVersion": 2, "rules": OURS, "rulesExistedBefore": ALL_OURS},
|
|
360
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
361
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
362
|
+
"stderr_token": "has no managedSettingsPath"},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "U04 uninstall: all-ours -> removes exactly ours, operator rule survives, state removed",
|
|
366
|
+
"op": "permission-uninstall",
|
|
367
|
+
"state": lambda s: state_v2(s, ALL_OURS),
|
|
368
|
+
"settings": {"permissions": {"allow": [R_V2, OPERATOR, R_PEERS, R_SELF]}},
|
|
369
|
+
"expect": {"rc": 0, "stdout_prefix": "uninstalled " + R_V2,
|
|
370
|
+
"settings_allow_after": [OPERATOR], "state_after": "removed"},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"id": "U05 uninstall: all-theirs -> removes nothing, says so, state removed",
|
|
374
|
+
"op": "permission-uninstall",
|
|
375
|
+
"state": lambda s: state_v2(s, ALL_THEIRS),
|
|
376
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
377
|
+
"expect": {"rc": 0, "stdout_prefix": "uninstalled (nothing",
|
|
378
|
+
"settings_allow_after": OURS, "state_after": "removed"},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "U06 uninstall: mixed split is exact — theirs stays, ours goes",
|
|
382
|
+
"op": "permission-uninstall",
|
|
383
|
+
"state": lambda s: state_v2(s, MIXED),
|
|
384
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
385
|
+
"expect": {"rc": 0, "settings_allow_after": [R_V2], "state_after": "removed"},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"id": "U07 uninstall: a v1 state uninstalls on its own shape (removes the one rule it granted)",
|
|
389
|
+
"op": "permission-uninstall",
|
|
390
|
+
"state": lambda s: state_v1(s, existed=False),
|
|
391
|
+
"settings": {"permissions": {"allow": [R_V2, OPERATOR]}},
|
|
392
|
+
"expect": {"rc": 0, "settings_allow_after": [OPERATOR], "state_after": "removed"},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"id": "U08 uninstall: R1a unknown-version -> REFUSE rc4, state SURVIVES, settings untouched",
|
|
396
|
+
"op": "permission-uninstall",
|
|
397
|
+
"state": lambda s: {"schemaVersion": 99, "managedSettingsPath": s},
|
|
398
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
399
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
400
|
+
"stderr_token": "unknown schemaVersion 99"},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"id": "U09 uninstall: R1b empty-provenance -> REFUSE rc4, no blind revoke, state SURVIVES",
|
|
404
|
+
"op": "permission-uninstall",
|
|
405
|
+
"state": lambda s: {"schemaVersion": 2, "managedSettingsPath": s, "rules": OURS, "rulesExistedBefore": {}},
|
|
406
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
407
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
408
|
+
"stderr_token": "no boolean provenance for"},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"id": "U10 uninstall: R2 malformed prior + settings ABSENT -> refusal BEFORE state deletion",
|
|
412
|
+
"op": "permission-uninstall",
|
|
413
|
+
"state": lambda s: {"schemaVersion": 2, "managedSettingsPath": s, "rules": OURS, "rulesExistedBefore": {}},
|
|
414
|
+
"settings": None,
|
|
415
|
+
"expect": {"rc": 4, "state_after": "survives",
|
|
416
|
+
"stderr_token": "no boolean provenance for"},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"id": "U11 uninstall: valid state + settings absent -> rc0, state removed (nothing to edit)",
|
|
420
|
+
"op": "permission-uninstall",
|
|
421
|
+
"state": lambda s: state_v2(s, ALL_OURS),
|
|
422
|
+
"settings": None,
|
|
423
|
+
"expect": {"rc": 0, "state_after": "removed"},
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"id": "U12 uninstall: valid state + malformed settings -> rc4, state SURVIVES for the retry",
|
|
427
|
+
"op": "permission-uninstall",
|
|
428
|
+
"state": lambda s: state_v2(s, ALL_OURS),
|
|
429
|
+
"settings": "not json at all",
|
|
430
|
+
"expect": {"rc": 4, "byte_identical": True, "state_after": "survives",
|
|
431
|
+
"stderr_token": "is not valid JSON"},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"id": "U13 uninstall: containers WE created are dropped when emptied (allow+permissions gone, file kept)",
|
|
435
|
+
"op": "permission-uninstall",
|
|
436
|
+
"state": lambda s: state_v2(s, ALL_OURS, allow_existed=False, perms_existed=False, settings_existed=True),
|
|
437
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
438
|
+
"expect": {"rc": 0, "settings_content": {}, "state_after": "removed"},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"id": "U14 uninstall: an operator-owned allow container is KEPT even when emptied",
|
|
442
|
+
"op": "permission-uninstall",
|
|
443
|
+
"state": lambda s: state_v2(s, ALL_OURS, allow_existed=True, perms_existed=True),
|
|
444
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
445
|
+
"expect": {"rc": 0, "settings_allow_after": [], "state_after": "removed"},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"id": "U15 uninstall: created-new settings holding only our structure is removed entirely",
|
|
449
|
+
"op": "permission-uninstall",
|
|
450
|
+
"state": lambda s: state_v2(s, ALL_OURS, allow_existed=False, perms_existed=False,
|
|
451
|
+
settings_existed=False, detect="created-new"),
|
|
452
|
+
"settings": {"permissions": {"allow": OURS}},
|
|
453
|
+
"expect": {"rc": 0, "settings_allow_after": None, "state_after": "removed"},
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
# ═ permission-doctor: presence x precedence (R4) — token line oracle ═
|
|
457
|
+
{
|
|
458
|
+
"id": "D01 doctor: settings absent -> absent",
|
|
459
|
+
"op": "permission-doctor", "state": None, "settings": None,
|
|
460
|
+
"expect": {"rc": 0, "stdout": "absent"},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"id": "D02 doctor: malformed settings -> invalid-json",
|
|
464
|
+
"op": "permission-doctor", "state": None, "settings": "nope{{{",
|
|
465
|
+
"expect": {"rc": 0, "stdout": "invalid-json"},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"id": "D03 doctor: empty object -> not-configured",
|
|
469
|
+
"op": "permission-doctor", "state": None, "settings": {},
|
|
470
|
+
"expect": {"rc": 0, "stdout": "not-configured"},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"id": "D04 doctor: wrong-typed permissions container reads as none -> not-configured",
|
|
474
|
+
"op": "permission-doctor", "state": None, "settings": {"permissions": []},
|
|
475
|
+
"expect": {"rc": 0, "stdout": "not-configured"},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"id": "D05 doctor: all three granted -> configured",
|
|
479
|
+
"op": "permission-doctor", "state": None, "settings": {"permissions": {"allow": OURS}},
|
|
480
|
+
"expect": {"rc": 0, "stdout": "configured"},
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"id": "D06 doctor: empty ask/deny lists do not shadow -> configured",
|
|
484
|
+
"op": "permission-doctor", "state": None,
|
|
485
|
+
"settings": {"permissions": {"allow": OURS, "ask": [], "deny": []}},
|
|
486
|
+
"expect": {"rc": 0, "stdout": "configured"},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"id": "D07 doctor: one missing -> partially-configured names exactly the gap",
|
|
490
|
+
"op": "permission-doctor", "state": None,
|
|
491
|
+
"settings": {"permissions": {"allow": [R_V2, R_PEERS]}},
|
|
492
|
+
"expect": {"rc": 0, "stdout": "partially-configured " + R_SELF},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"id": "D08 doctor: two missing -> partially-configured names both, in rule-set order",
|
|
496
|
+
"op": "permission-doctor", "state": None,
|
|
497
|
+
"settings": {"permissions": {"allow": [R_PEERS]}},
|
|
498
|
+
"expect": {"rc": 0, "stdout": "partially-configured %s %s" % (R_V2, R_SELF)},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"id": "D09 doctor: nothing of ours, no broad -> not-configured",
|
|
502
|
+
"op": "permission-doctor", "state": None,
|
|
503
|
+
"settings": {"permissions": {"allow": [OPERATOR]}},
|
|
504
|
+
"expect": {"rc": 0, "stdout": "not-configured"},
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"id": "D10 doctor: operator's mcp(*) covers a bare host -> covered-by-allow, named",
|
|
508
|
+
"op": "permission-doctor", "state": None,
|
|
509
|
+
"settings": {"permissions": {"allow": [BROAD_STAR]}},
|
|
510
|
+
"expect": {"rc": 0, "stdout": "covered-by-allow " + BROAD_STAR},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"id": "D11 doctor: server-wide rule covers a partial host -> covered-by-allow, named",
|
|
514
|
+
"op": "permission-doctor", "state": None,
|
|
515
|
+
"settings": {"permissions": {"allow": [R_V2, BROAD_SERVER]}},
|
|
516
|
+
"expect": {"rc": 0, "stdout": "covered-by-allow " + BROAD_SERVER},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"id": "D12 doctor: exact deny shadows ONE tool -> shadowed-by-deny exact, that rule named",
|
|
520
|
+
"op": "permission-doctor", "state": None,
|
|
521
|
+
"settings": {"permissions": {"allow": OURS, "deny": [R_SELF]}},
|
|
522
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-deny exact " + R_SELF},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"id": "D13 doctor: exact ask shadows ONE tool -> shadowed-by-ask exact",
|
|
526
|
+
"op": "permission-doctor", "state": None,
|
|
527
|
+
"settings": {"permissions": {"allow": OURS, "ask": [R_SELF]}},
|
|
528
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-ask exact " + R_SELF},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"id": "D14 doctor: broad mcp(*) in deny -> shadowed-by-deny broad",
|
|
532
|
+
"op": "permission-doctor", "state": None,
|
|
533
|
+
"settings": {"permissions": {"allow": OURS, "deny": [BROAD_STAR]}},
|
|
534
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-deny broad " + BROAD_STAR},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"id": "D15 doctor: broad server rule in ask -> shadowed-by-ask broad",
|
|
538
|
+
"op": "permission-doctor", "state": None,
|
|
539
|
+
"settings": {"permissions": {"allow": OURS, "ask": [BROAD_SERVER]}},
|
|
540
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-ask broad " + BROAD_SERVER},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"id": "D16 doctor: CROSS-LIST — deny-exact + ask-broad reports the BROAD ask (the shipped defect) [QK:AGY-CROSS-LIST-BROAD-FIRST]",
|
|
544
|
+
"op": "permission-doctor", "state": None,
|
|
545
|
+
"settings": {"permissions": {"allow": OURS, "deny": [R_SELF], "ask": [BROAD_SERVER]}},
|
|
546
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-ask broad " + BROAD_SERVER},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"id": "D17 doctor: CROSS-LIST mirror — deny-broad + ask-exact reports the BROAD deny",
|
|
550
|
+
"op": "permission-doctor", "state": None,
|
|
551
|
+
"settings": {"permissions": {"allow": OURS, "deny": [BROAD_STAR], "ask": [R_SELF]}},
|
|
552
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-deny broad " + BROAD_STAR},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"id": "D18 doctor: a broad rule in deny shadows even its own twin in allow (Deny > Allow)",
|
|
556
|
+
"op": "permission-doctor", "state": None,
|
|
557
|
+
"settings": {"permissions": {"allow": [BROAD_STAR], "deny": [BROAD_STAR]}},
|
|
558
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-deny broad " + BROAD_STAR},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"id": "D19 doctor: two broad rules in one deny list -> first in FILE order is named",
|
|
562
|
+
"op": "permission-doctor", "state": None,
|
|
563
|
+
"settings": {"permissions": {"allow": OURS, "deny": [BROAD_STAR, BROAD_SERVER]}},
|
|
564
|
+
"expect": {"rc": 0, "stdout": "shadowed-by-deny broad " + BROAD_STAR},
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
# ═ permission-state-doctor: the parser-state axis, read-only (hard rule 13) ═
|
|
568
|
+
{
|
|
569
|
+
"id": "S01 state-doctor: absent -> absent",
|
|
570
|
+
"op": "permission-state-doctor", "state": None, "settings": None,
|
|
571
|
+
"expect": {"rc": 0, "stdout": "absent"},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"id": "S02 state-doctor: not-json -> corrupt, named",
|
|
575
|
+
"op": "permission-state-doctor", "state": "junk{{{", "settings": None,
|
|
576
|
+
"expect": {"rc": 0, "stdout": "corrupt not valid JSON"},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"id": "S03 state-doctor: valid v1 -> ok 1",
|
|
580
|
+
"op": "permission-state-doctor", "state": lambda s: state_v1(s), "settings": None,
|
|
581
|
+
"expect": {"rc": 0, "stdout": "ok 1"},
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"id": "S04 state-doctor: valid v2 -> ok 3",
|
|
585
|
+
"op": "permission-state-doctor", "state": lambda s: state_v2(s, ALL_OURS), "settings": None,
|
|
586
|
+
"expect": {"rc": 0, "stdout": "ok 3"},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
"id": "S05 state-doctor: R1a unknown version -> corrupt, refuses to guess",
|
|
590
|
+
"op": "permission-state-doctor",
|
|
591
|
+
"state": {"schemaVersion": 99, "managedSettingsPath": "/x"}, "settings": None,
|
|
592
|
+
"expect": {"rc": 0, "stdout_prefix": "corrupt unknown schemaVersion 99"},
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"id": "S06 state-doctor: R1b empty provenance map -> corrupt (no boolean provenance)",
|
|
596
|
+
"op": "permission-state-doctor",
|
|
597
|
+
"state": {"schemaVersion": 2, "managedSettingsPath": "/x", "rules": OURS, "rulesExistedBefore": {}},
|
|
598
|
+
"settings": None,
|
|
599
|
+
"expect": {"rc": 0, "stdout_prefix": "corrupt no boolean provenance for"},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"id": "S07 state-doctor: v2 with a non-list rules field -> corrupt",
|
|
603
|
+
"op": "permission-state-doctor",
|
|
604
|
+
"state": {"schemaVersion": 2, "managedSettingsPath": "/x", "rules": "all", "rulesExistedBefore": {}},
|
|
605
|
+
"settings": None,
|
|
606
|
+
"expect": {"rc": 0, "stdout_prefix": "corrupt `rules` must be a non-empty list"},
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"id": "S08 state-doctor: v1 missing its boolean -> corrupt",
|
|
610
|
+
"op": "permission-state-doctor",
|
|
611
|
+
"state": {"schemaVersion": 1, "managedSettingsPath": "/x", "rule": R_V2},
|
|
612
|
+
"settings": None,
|
|
613
|
+
"expect": {"rc": 0, "stdout_prefix": "corrupt schemaVersion 1 needs"},
|
|
614
|
+
},
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
EXCLUSIONS = [
|
|
618
|
+
("R2", "ownership x precedence under settings in {absent, malformed} — undefined; replaced by byte-identical refusal cells"),
|
|
619
|
+
("R4", "precedence on install/uninstall — the engine never reads it there"),
|
|
620
|
+
("R5", "symlink settings/state/config refusals — owned by smoke-agy-install-state"),
|
|
621
|
+
("R6", "shell doctor verdict composition (DRIFT/NOTE/ORPHANED, exit codes) — owned by smoke-agy-install-state"),
|
|
622
|
+
("R7", "the MCP-config install lane (mcpServers) — owned by smoke-agy-install-state"),
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
EXPECTED_CELLS = 55
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def main():
|
|
629
|
+
if not os.path.exists(CONFIG):
|
|
630
|
+
die("preflight", "missing SUT %s" % CONFIG)
|
|
631
|
+
before = subprocess.run(["git", "-C", REPO, "status", "--porcelain"], capture_output=True, text=True).stdout
|
|
632
|
+
if len(CELLS) != EXPECTED_CELLS:
|
|
633
|
+
die("table integrity", "CELLS holds %d cells, the stated contract is %d — extend the TABLE "
|
|
634
|
+
"and this count together, never silently" % (len(CELLS), EXPECTED_CELLS))
|
|
635
|
+
ids = [c["id"].split()[0] for c in CELLS]
|
|
636
|
+
if len(set(ids)) != len(ids):
|
|
637
|
+
die("table integrity", "duplicate cell ids")
|
|
638
|
+
# P0-4: the total alone can hide a swapped operation mix — assert the per-operation
|
|
639
|
+
# counts AND the exact ID sequence, so a dropped/duplicated cell names itself.
|
|
640
|
+
per_op = {"I": 13, "U": 15, "D": 19, "S": 8}
|
|
641
|
+
want_ids = ["%s%02d" % (prefix, n) for prefix in ("I", "U", "D", "S") for n in range(1, per_op[prefix] + 1)]
|
|
642
|
+
if ids != want_ids:
|
|
643
|
+
die("table integrity", "cell ID sequence drifted: %r" % [a for a, b in zip(ids, want_ids) if a != b])
|
|
644
|
+
for cell in CELLS:
|
|
645
|
+
run_cell(cell)
|
|
646
|
+
after = subprocess.run(["git", "-C", REPO, "status", "--porcelain"], capture_output=True, text=True).stdout
|
|
647
|
+
if before != after:
|
|
648
|
+
die("checkout purity", "the matrix run changed the working tree")
|
|
649
|
+
sys.stdout.write("\ncheck-agy-permission-matrix: %d cells executed (contract table, not an append log)\n" % passed)
|
|
650
|
+
for rule, why in EXCLUSIONS:
|
|
651
|
+
sys.stdout.write(" excluded by %s: %s\n" % (rule, why))
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
if __name__ == "__main__":
|
|
655
|
+
main()
|