Files
stack/docs/plans/reviews/2026-09-06_foundation-inspector-rocko-feasibility-r2.md
T
jason.woltje 8ebddd6f93 feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
2026-09-07 14:06:35 -05:00

31 KiB
Raw Blame History

FI-ROCKO-2 — Foundation synthetic inspector: corrected feasibility note (r2)

Author: rocko (Claude Fable 5.1, Archify lane author seat). Requested by darkwing (FI-ROCKO-2, 2026-09-06), reconciling the FI-ROCKO-1 note against darkwing's coordinator source check. Issue: #53. Status: planning evidence only. This note proposes; it does not approve, implement, or bind. It supersedes the r1 note for every point where the two differ; r1 is preserved unchanged as history.

0. Admission and scope

FI-ROCKO-2 is admitted. No blocker. My held owner goal (Archify C1 rev 3, awaiting Jason's rulings) is not displaced. This file is the only repository write. No implementation, dependency, commit, push, checkout, live test, credential read, fleet investigation, or contact with filbert occurred. Every citation below was re-read from committed bytes with git show during this task; the only unpinned inputs are the untracked charter draft and darkwing's corrections note.

1. Pinned inputs

Input Identity
Technical map docs/plans/2026-09-06_foundation-technical-map.md (the accepted map) and docs/plans/2026-09-06_foundation-map-handoff.md commit 7345f330fc6bfae5aa1d896c78cfb7cbe62efbae
Requirements document docs/plans/2026-09-06_agent-project-workspace-foundation.md (R1R34), candidate docs/plans/foundation-v1-candidate/{README.md, REVIEW.md, RUNTIME.md, records.schema.json, records.fixtures.json, fingerprint-vectors.json, check.py, semantic-model.py}, docs/plans/2026-09-06_foundation-phase2-contract.md, docs/plans/ROADMAP.md commit d4696d09eb1b5dcf1028f30db2cd63735f51cb16
Charter draft 1 docs/plans/2026-09-06_foundation-inspector-charter.md untracked, SHA-256 fdcdf4df8bf30b9fbfff960f9b1d4160ce7e6bbc4b8bb601a93cda13b56c8996 (unchanged since r1)
Corrections docs/plans/reviews/2026-09-06_foundation-inspector-rocko-corrections.md untracked, SHA-256 fe672fd6f0b745b5f64042a094f31463064ca436aeaeba956f9c15228df5f716
r1 note docs/plans/reviews/2026-09-06_foundation-inspector-rocko-feasibility.md untracked, SHA-256 92fa7b3de2591ba24fb184ed8edbabedd30c54498fdaf101649e36aaebabe9b9

Citation form: commit:path:line. cand/ abbreviates docs/plans/foundation-v1-candidate/; map abbreviates 7345f33:docs/plans/2026-09-06_foundation-technical-map.md.

2. Disposition of F1F9

Finding Disposition Where resolved
F1 wrong map path, nonexistent FINDINGS.md, phantom Node pin Accepted in full. Citations corrected; AJV/fast-uri rationale withdrawn; measured host version separated from a proposed test baseline §3, §4
F2 reference shapes (recordRef no digest, basisRef artifactRef, authorizationRef runtimeId, workspace parent from scope, createdAt calendar rule) Accepted in full. Bundle gains declared artifacts and authorizations simulation inputs; resolver rules rewritten against the schema §5, §6
F3 instruction-order digest Accepted. New digest is named separately, preserves all array order, never claims the README 6.1 fingerprint rule; exact vectors supplied §6.1
F4 mandatory role/policy layers, assignment has no restrictions, no launch default Accepted in full. Layer table rebuilt; role ceilings mandatory; execution layer must be explicitly modelled §7
F5 reassignment authority in both scopes; no success path Accepted. Proposal mode yields only refused or unresolved; caller-supplied observations removed §8
F6 bounded operations, no execution.stop, closed output Accepted. Four supported operations; execution.stop withdrawn (catalog has execution.control); output schema enumerates every field §9, §10
F7 required differential schema verification Accepted. Mandatory Python/jsonschema harness; missing dependency fails, never skips §12.1
F8 honest non-effect tests, descriptor-based open Accepted. Inventory diff on separated roots, static import boundary, `O_NOFOLLOW O_NONBLOCKopen thenfstat`
F9 revision handling Accepted. Contiguous full history or nothing per identity; historical vs current-admission resolution classes §6.3

No finding is disputed. Two places where I go slightly beyond the finding are flagged inline (§9 exit class for unsupported operations; §6.1 ASCII limit on mock-content paths).

3. Corrected repository facts (F1)

  • The accepted map is the technical map at 7345f33. Its recommended increment is map:244-283; exit classes map:275-276; "No npm/Turbo change is needed" map:278; the inspector-must-not-bootstrap disposition map:240; the cross-lane retasking scenario map:290-314 with the adversarial acceptance case at map:305-314. The document I cited in r1 under the map's name is the requirements document (R1R34) at d4696d09; R-number citations stay valid against that file, the map citations did not. Corrected throughout.
  • d4696d09:cand/FINDINGS.md does not exist (git cat-file -e fails) and no file in the pinned tree or in docs/plans/*.md mentions ajv or fast-uri. The audit rationale is withdrawn. The dependency-free recommendation now rests only on in-scope facts: package.json declares one dependency (pi 0.84.4), the map forbids an npm change for this boundary (map:278), and the candidate's own checker states it "never installs dependencies" (d4696d09:cand/check.py:4).
  • There is no repository Node pin: no engines field, no .nvmrc, .node-version, or .tool-versions, and no Node image pin in the build scripts. The measured host runtime is Node v26.8.1, Python 3.12.8, jsonschema 4.26.0. These are measurements, not pins.
  • Proposed inspector test baseline (a new charter decision, not an existing pin): the suite records node --version in its header and refuses to run below a stated minimum. The features used (node:test, node:crypto, TextDecoder with fatal: true, fs.constants.O_NOFOLLOW) are all present in Node 22 LTS; I recommend the charter state "Node ≥ 22, measured on v26.8.1" and that no engines field be added by this increment.

4. Language and dependency recommendation (D-A resolved)

Node ESM, zero new runtime dependencies, hand-rolled closed validation derived field-by-field from d4696d09:cand/records.schema.json, verified differentially by a mandatory Python/jsonschema harness (§12.1). The Python-stdlib runtime alternative is closed: the candidate's check.py is a script that executes assertions on import, not a validation API (d4696d09:cand/check.py:66-96 run at module level), so neither language gets a free validator; Node keeps the CLI surface consistent with scripts/.

5. Bundle contract (exact fields, proposed to freeze)

Closed objects throughout; unknown keys refuse; duplicate JSON keys refuse at parse time; identifiers follow $defs/id (^[a-z0-9][a-z0-9._-]{0,63}$).

{
  "bundleVersion": 1,
  "kind": "foundation-inspector-bundle",
  "simulation": true,                          // literal true or refuse
  "records": [ <envelope record>, ... ],       // ≤256, supported kinds only (§10)
  "registries": [ <registryEntry>, ... ],      // ≤256
  "artifacts": [ {runId, artifactId, digest}, ... ],   // ≤256, declared only
  "authorizations": [ <runtimeId>, ... ],      // ≤256, declared only
  "selection": {
    "agentId": <id>, "projectId": <id>, "workspaceId": <id>,
    "assignmentRef": <recordRef> | null,
    "execution": { "kind": "unrestricted-simulation" }
               | { "kind": "restrictions", "restrictions": <restrictions> }
  },
  "operation": { "name": <supported operation>, "target": <target> | null },
  "proposal": null | {
    "kind": "assignment-change",
    "requesterAgentId": <id>,
    "subjectAssignmentRef": <recordRef>,
    "targetTaskRef": <recordRef>,
    "delegationRef": <recordRef> | null,
    "changeDecisionRef": <recordRef> | null,
    "message": <string, ≤4000> | absent        // informational, never authority
  }
}

5.1 Type corrections (F2)

  • <envelope record> is exactly $defs/envelope: authorizationRef is a runtimeId (UUID v4 pattern), not a recordRef; supersedes is null at revision 1 and a recordRef otherwise (schema allOf); createdAt is $defs/time (YYYY-MM-DDTHH:MM:SS.mmmZ) and must be a real calendar instant with the same round-trip rule as check.py:36-44 (2026-02-30 refuses). It still takes no part in calculations.
  • <recordRef> = {kind, id, scope, revision}. No digest. Record identity is exact-match on those four fields; record content integrity is not digest-checked in increment 1 (stated as a limitation in output docs).
  • <registryRef> = {registry, id, revision, digest}; <artifactRef> = {runId, artifactId, digest}. decision.basisRef and evidenceRefs are artifactRefs and are evidence pointers, not authority.
  • Workspace ownership comes from scope: a workspace record has scope.kind = workspace, scope.projectId names the owning project, and id must equal scope.workspaceId; a project's id must equal scope.projectId (d4696d09:cand/README.md:64). Registrations carry project or workspace scope per the schema's conditional.

5.2 Simulation-only external inputs (F2)

artifacts and authorizations are the only way the bundle can satisfy an artifactRef or authorizationRef. They are declared, not verified: the inspector checks presence and exact match only, never content, signature, or liveness, and the output labels them declared-simulation-inputs. A missing declaration refuses missing-state; it is never treated as valid. Nothing in the bundle asserts authentication; the inspector emits "authentication": "not-modelled" in every result.

5.3 Bounds and file handling (F8)

Bound Value
File size ≤ 1 MiB, checked from fstat before reading
records, registries, artifacts, authorizations ≤ 256 each
Any array ≤ 1024 (d4696d09:docs/plans/2026-09-06_foundation-phase2-contract.md:116)
Nesting depth ≤ 32
Strings ≤ 4096 bytes; relative paths per $defs/relativePath and the mosaic-relative-path format (≤4096 UTF-8 bytes, no Cc/Cf/Cs, no backslash, no empty/./.. segment; check.py:47-64)

Open sequence (Linux, descriptor-based): fs.openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); a symlink fails at open with ELOOP and a FIFO cannot block; then fstatSync(fd) must report a regular file and a size within bound; then read exactly size bytes from the descriptor and refuse if more arrive. Any failure is exit 4 io-failure naming only the path as given on argv. This is a design statement; no live test was run.

6. Digest and reference algorithms

6.1 Mock-content digest (F3): inspector-content-digest/1

This is a new, separately named restricted algorithm for content-bearing registry entries only. It is not the README 6.1 launch-fingerprint projection, which preserves instruction order and ID-sorts skills (d4696d09:cand/README.md:268-285, check.py:115); the inspector computes no launch fingerprint in increment 1.

Rule: domain = objects, arrays, ASCII strings, booleans, null, safe integers; anything else refuses unsupported-capability. Object keys sorted by code unit; compact separators; all array order preserved; digest = "sha256:" + hex(SHA-256(UTF-8 bytes)). Structural uniqueness (uniqueItems) and set intersection operate on parsed values and are independent of serialization. Consequence flagged: pathGrant paths inside mock content must be ASCII in increment 1, while record and target paths may be non-ASCII per the candidate regex (which accepts é/x); the charter should state this asymmetry.

Exact vectors (computed with Python stdlib json.dumps(sort_keys=True, separators=(",",":")), the same primitives as check.py:116):

Vector Content Canonical Digest
V1 {"restrictions":{"operations":["work.read","file.read"],"readPaths":[{"root":"workspace","path":null}],"writePaths":[],"network":"none","endpointRefs":[]}} {"restrictions":{"endpointRefs":[],"network":"none","operations":["work.read","file.read"],"readPaths":[{"path":null,"root":"workspace"}],"writePaths":[]}} sha256:0bc44e14fd8354a8a85be879306a54881da7a0cb4d3dcf1f88bc4bc08a40dc9c
V2 V1 with operations reversed …"operations":["file.read","work.read"]… sha256:63817bffc57803ca6ac5df971aa0a160d4be73646394b3bc536578bd66c82660 (differs from V1: order is significant)
V3 V1 plus file.change and writePaths:[{"root":"workspace","path":"docs"}] {"restrictions":{"endpointRefs":[],"network":"none","operations":["work.read","file.read","file.change"],"readPaths":[{"path":null,"root":"workspace"}],"writePaths":[{"path":"docs","root":"workspace"}]}} sha256:d538c865808cfe7665956615fb48f69819baa0833626d1ef6e1cb3614acbc7da
V4 V1 with a float 1.0 anywhere refuse unsupported-capability
V5 V1 with a non-ASCII string refuse unsupported-capability

Registry entry shapes: declared {registry, id, revision, digest} for harness, settings, context-content, endpoint; content-bearing {registry, id, revision, digest, content:{restrictions}} for scope-role, agent-policy, project-policy, where digest must equal the V-rule digest of content or the bundle refuses invalid-request. The candidate defines no registry content shapes; {restrictions} is a simulation-only stand-in (D-E).

6.2 Reference resolution

Index on (kind, id, scope); duplicate (kind, id, scope, revision) refuses invalid-request. Every recordRef resolves by exact four-field match; every registryRef by exact four-field match including digest; every artifactRef and authorizationRef by exact match against the declared lists (§5.2). Absent → missing-state; kind mismatch against the schema's const constraints (for example policyRef not agent-policy) → invalid-request.

Graph rules (all refuse without repair, naming the rule):

  • Project id = scope.projectId; workspace id = scope.workspaceId; the workspace's scope.projectId must resolve to an active project.
  • A workspace registration's projectRegistrationRef must resolve to an active project registration in the workspace's owning project for the same agentId (README:65).
  • A workspace mission's parentMissionRef must be a project mission in that project; project missions have no parent (README:66-68).
  • A task's missionRef resolves in the selected project; dependencies are tasks; cycle checks run over exactly three edge sets, independently: task.dependencies, mission.parentMissionRef, supersedes. Excluded from cycle checks: intentRef, subjectRefs, projectRegistrationRef, delegationRef, all registryRefs and artifactRefs.
  • Cross-project execution selection refuses (README:71).

6.3 Revision history (F9, D-J resolved)

Per identity (kind, id, scope) the bundle carries either exactly one record at revision 1 or a complete contiguous chain 1..n where each revision r > 1 has supersedes = {kind, id, scope, revision: r-1}. A chain with a missing predecessor, a skipped number, or two records superseding the same predecessor refuses invalid-request; the inspector never invents history. Head = highest revision.

Two resolution classes:

  • Current-admission references must name the head: selection.*, assignment.taskRef, assignment.intentRef, task.intentRef, task.missionRef, task.dependencies, mission.parentMissionRef, registration.projectRegistrationRef, registration.delegationRef, and every proposal ref. Naming a non-head revision refuses stale-revision.
  • Historical references may name any revision present in the bundle: supersedes and decision.subjectRefs. Absent → missing-state.

This satisfies charter A3's stale-reference case with a real fixture (task intent pointing at decision rev 1 while rev 2 exists) without treating all history as forbidden.

7. Permission algorithm (F4, D-D, D-F resolved)

Supported operations for permission preview: work.read, file.read, file.change (§10). Taskless set = {work.read, file.read} exactly (d4696d09:cand/semantic-model.py:24); file.change requires selection.assignmentRef.

# Layer Source Mandatory?
L1 agent ceiling agent-definition.policyRefagent-policy content yes; agent-definition.status must be enabled
L2 project registration active registration in projectScope(projectId) for agentId: scopeRoleRefscope-role content is the ceiling; restrictions (nullable) narrows it further yes (README:99-101)
L3 workspace registration active registration in workspaceScope(projectId, workspaceId) for agentId, projectRegistrationRef = L2's exact record; scopeRoleRef ceiling; restrictions nullable narrowing yes
L4 project policy project.policyRefproject-policy content yes
L5 workspace policy workspace.policyRefproject-policy content (schema const) yes; no fallback to L3's role
L6 assignment binding one assignment = selection.assignmentRef; status: selected, agentId = selected agent, taskRef head in the selected workspace scope, intentRef → approved decision (head) whose subjectRefs include the task required for non-taskless operations; contributes checks only, it has no restrictions field
L7 task task.restrictions (nullable = no narrowing); independently task.status ∈ {ready, active} and task.intentRef → approved head decision status/intent checks mandatory whenever L6 applies
L8 mission chain each mission from task.missionRef up the parent chain: restrictions (nullable = no narrowing); status: active mandatory as L7
L9 execution selection.execution: restrictions narrows; unrestricted-simulation is an explicit no-narrowing marker yes; absent or null refuses missing-state (README:132, semantic-model.py:21)

Intersection: operations as set intersection; explicit empty array = none (README:101); restrictions: null on L2/L3/L7/L8 = no additional narrowing (README:100) but never satisfies a mandatory ceiling or status/intent check. Path grants: path: null = whole root (README:102); component-prefix matching on parsed segments (semantic-model.py:30-34); the intersection of two grant lists keeps, for each pair where one grant is a segment-prefix of the other, the narrower. Network: none dominates; approved-endpoints intersect by exact endpoint registryRef. No union across assignments (contract:172); exactly one assignment consulted (README:134).

Refusal precedence: parse/shape/bounds (exit 2) → graph incoherence (exit 2) → unsupported operation or kind (exit 2) → mandatory layer or declaration missing (exit 3, missing-state) → registration/role absent or revoked (exit 3, not-authorized) → status/intent not current (exit 3, not-authorized, rule named) → operation or target outside the intersection (exit 3, not-authorized). First failure ends evaluation.

8. Proposal mode: assignment-change negative check (F5, D-G, D-I resolved)

Increment 1 implements no success path. The evaluation order is fixed and every branch ends in refused or unresolved; allowed is unreachable by construction, and a unit test asserts that.

  1. Original scope authority: the requester holds an active L2/L3 registration in the subject assignment's workspace whose role ceiling ∩ restrictions include assignment.change. Else not-authorized / requester-lacks-original-scope-authority.
  2. Target scope authority: the same in the target task's project and workspace. Else not-authorized / requester-lacks-target-scope-authority.
  3. Delegation: delegationRef → head decision, decisionKind: delegation, outcome: approved, delegatedOperations ∋ assignment.change, subjectRefs include the requester's agent-definition and the subject assignment or its task (recipient and subject bounds). Else not-authorized / delegation-not-applicable. A message with no delegation refuses with rule message-is-not-authority (map:310).
  4. Recorded change: changeDecisionRef → head decision, decisionKind: assignment-change, outcome: approved, subjectRefs include the subject assignment and the target task; its basisRef must be a declared artifact but is not itself authorization. Else not-authorized / change-not-recorded.
  5. Current intent: the subject assignment's intentRef and the target task's intentRef resolve to approved head decisions. Else stale-revision or not-authorized as applicable.
  6. If 15 pass: result unresolved, reason unknown-effects, rule runtime-reconciliation-required. Stopping and reconciliation are runtime facts (map:311-314); the bundle has no field to assert them and the r1 runtimeObservations field is withdrawn.

In every branch the output echoes selection.assignmentRef unchanged as selectedAssignmentRef and the bundle bytes are untouched. Requester model is agent-only in increment 1 (D-G); delegation chains are not modelled (D-I).

9. Output and exit classes (F6)

Closed result schema; every emitted field is listed here and nothing else is emitted:

{
  "disclaimer": "SYNTHETIC PREVIEW — NO LIVE EFFECTS",
  "preview": "preview: no live registrations or permission grants",   // REVIEW.md:126
  "bundleVersion": 1,
  "authentication": "not-modelled",
  "declaredSimulationInputs": { "artifacts": <count>, "authorizations": <count> },
  "result": "allowed" | "refused" | "unresolved" | "invalid",
  "reason": <reason code>,
  "rule": <rule name> | null,
  "selection": { "agentId", "projectId", "workspaceId", "assignmentRef" },
  "operation": { "name", "target" },
  "consulted": [ <recordRef | registryRef>, ... ],       // identities only, sorted
  "proposal": null | { "result", "reason", "rule", "selectedAssignmentRef" }
}

Text output is a pure rendering of this object; both begin with the disclaimer, on every exit class. No payload bodies, no parser input bytes (byte offset and error class only), no timestamps, no random ids, keys sorted.

Reason codes: the RUNTIME closed set (d4696d09:cand/RUNTIME.md:97) plus inspector-only unsupported-kind and io-failure, documented as not runtime API codes. Exit classes (map:275-276, charter): 0 allowed; 2 malformed, graph-incoherent, unsupported kind or operation; 3 refused or unresolved; 4 I/O failure. Flag: I place unsupported operations in exit 2 with reason unsupported-capability because they are not simulated permission refusals; if darkwing prefers exit 3 for them, nothing else in this note changes (D-C).

10. Supported kinds and operations (F6)

Record kinds: agent-definition, project, workspace, registration, mission, task, assignment, decision. session, context-source, adapter-capability present in records refuse unsupported-kind.

Operations (from the 29-name catalog, d4696d09:cand/records.schema.json $defs/operation): permission previews for work.read (target null), file.read and file.change (target {root:"workspace", path}); proposal negative check for assignment.change (§8). The other 25 catalog names are recognized and refuse unsupported-capability; never exit 0. execution.stop does not exist in the catalog (execution.control does); the r1 remark is withdrawn. Names outside the catalog refuse invalid-request.

11. Physical paths (D-B, D-H resolved)

Unchanged from r1: scripts/foundation-inspect.mjs (CLI, future packages/mosaic); scripts/foundation/{strict-json,canonical,resolve}.mjs (pure, future packages/config; import only each other and node:crypto); scripts/foundation/fixtures/; scripts/foundation/*.test.mjs; scripts/test-foundation.sh; plus the new verification harness scripts/foundation/verify-schema.py (§12.1). No packages/ scaffolding, no root file, no .sh shim, nothing imported from scripts/mosaic-config.mjs (map:240). Layout follows the ROADMAP M20 doctrine (d4696d09:docs/plans/ROADMAP.md:142-159).

12. Test plan

12.1 Required differential schema verification (F7)

scripts/foundation/verify-schema.py: Python 3 stdlib plus the already installed jsonschema (measured 4.26.0); no installation. If import jsonschema fails the suite exits non-zero with verification dependency missing; it never skips. The harness re-declares the two format checkers exactly as check.py:36-64 (calendar round-trip date-time, mosaic-relative-path), loads the pinned cand/records.schema.json from the tree, and compares, for every document in a corpus, the jsonschema verdict with the Node validator's verdict (obtained by running node scripts/foundation/validate-record.mjs over the corpus and reading its JSON verdict list). Any disagreement fails.

Corpus: every fixture record; the candidate's own records.fixtures.json cases (schema-valid and invalid) for the eight supported kinds; and a mutation set generated deterministically from each valid record: drop each required field; add one unknown key at each object level; createdAt = 2026-02-30 and 2028-02-29; paths ../x, a//b, a/./b, /abs, backslash, a control character, 4097 bytes, é/x (must be accepted by both); revision 1 with non-null supersedes; revision 2 with null supersedes; approved delegation with empty delegatedOperations; non-delegation with non-empty delegatedOperations; network: none with an endpoint; approved-endpoints with none; status: ended with null endReason and vice versa; each typed reference with the wrong kind/registry. The runtime remains dependency-free; only verification needs Python.

12.2 Non-effect and privacy observations (F8)

  • Roots are separated: observed roots $SANDBOX/home, $SANDBOX/cwd, $SANDBOX/fixtures, $SANDBOX/data-root; harness writes go only to $SANDBOX/harness. The CLI runs with env -i PATH=… HOME=$SANDBOX/home NO_COLOR=1, cwd $SANDBOX/cwd, MOSAIC_CONFIG unset.
  • Inventory before and after every invocation over each observed root: find -printf '%p %y %s %m %U %i\n' | sort plus SHA-256 of every regular file; compared exactly. A canary config path under home/.config/mosaic-dev is asserted absent after a missing-file run (no bootstrap, A8).
  • Static boundary: a unit test reads the three pure modules and asserts their import specifiers are exactly the allowed set and that process, fs, and Date do not appear; a second asserts the CLI imports no mosaic-config.
  • Canaries (CANARY-DO-NOT-PRINT-n in every unrelated payload and in the second workspace's records) are grepped from all captured output. Stated limit: canaries and inventories demonstrate this tool's bounded behaviour on these inputs; they do not prove absence of all reads or OS-level containment (charter test-plan paragraph).
  • Determinism: each fixture runs twice, byte-identical; --json and text agree on result/reason/rule; golden files compared exactly.

12.3 Units (node --test)

Canonicalizer V1V5; strict JSON (duplicate key, depth, size, fatal UTF-8); each graph rule in §6.26.3; each layer in §7 including "null restrictions do not satisfy a mandatory ceiling", "empty array is none", "no union across assignments", "missing execution layer refuses"; §8 steps 16 and the allowed-is-unreachable assertion.

13. Fixtures and the first demonstration

demo.bundle.json: project P1 with workspaces W1 and W2; project P2; agent A enabled, registered in P1 and W1 only, with scope-role V1 (reader) at W1; one active mission and one ready task in W1 with approved plan-approval intents; one selected assignment for A; agent B registered in P2 with a delegation decision that does not cover assignment.change; all revisions 1; a second variant carries decision rev 1→2 for the stale case.

Fixture Expected
demo-read-w1 0 allowed file.read docs/notes.md
demo-change-w1-refused 3 not-authorized, role ceiling V1 lacks file.change
demo-read-w2 3 not-authorized, workspace-registration-missing, no W2 payload text
demo-cross-project 3 not-authorized, cross-project selection
demo-reassign-message-only 3 not-authorized, message-is-not-authority, selection unchanged
demo-reassign-fully-recorded 3 unresolved, unknown-effects, runtime-reconciliation-required
neg-stale-intent 3 stale-revision (intent names decision rev 1, rev 2 present)
neg-missing-predecessor, neg-fork, neg-duplicate-identity, neg-cycle-{task,mission,supersedes}, neg-wrong-owner 2 invalid-request, rule named
neg-no-execution-layer, neg-missing-role-content, neg-undeclared-artifact 3 missing-state
neg-empty-ops, neg-two-assignments 3 not-authorized
neg-unsupported-op-execution.control, neg-unsupported-kind-session 2 unsupported-capability / unsupported-kind
neg-unknown-key, neg-dup-json-key, neg-bad-utf8.bin, neg-bad-path, neg-bad-calendar 2 invalid-request
symlink, FIFO, missing file 4 io-failure, no bootstrap

14. Resolved decisions D-A..D-J

Id Resolution
D-A Node ESM, zero runtime deps; Python/jsonschema for mandatory verification only (§4, §12.1)
D-B scripts/foundation/ with M20 owner headers; no packages/ (§11)
D-C Graph incoherence and unsupported kind/operation → exit 2; permission refusal/unresolved → 3. Only open sub-choice: exit class for unsupported-capability operations (§9 flag)
D-D Taskless = {work.read, file.read}; work.propose is not implemented at all in increment 1 (§10)
D-E Registry content = {restrictions} simulation stand-in, digest per §6.1
D-F selection.execution is mandatory and explicit; no null default (§7 L9)
D-G Requester is an agent with registrations in both scopes (§8)
D-H scripts/foundation-inspect.mjs, no .sh shim
D-I Delegation: recipient and subject bounds checked; no chains (§8 step 3)
D-J Revision-1-only or complete contiguous chain; historical vs current-admission classes (§6.3)

Remaining design choice for darkwing, stated precisely: whether unsupported catalog operations exit 2 or 3 (§9). Everything else in this note is a concrete recommendation with no open alternative.

15. Feasibility verdict

Still feasible as a bounded, dependency-free increment. The corrections increase scope in three places: mandatory role ceilings and execution layer (§7), the full-history resolver with two reference classes (§6.3), and the required differential verification harness (§12.1). None requires an npm change, a package move, live state, or runtime coupling. The largest new risk is the differential harness itself drifting from the candidate checker's format semantics; re-declaring the two format checkers verbatim from check.py:36-64 and asserting the same four sentinel cases (check.py:66-69) is the mitigation.

This note is evidence for darkwing's charter. It is not approval, and it authorizes no implementation.