Files
stack/docs/plans/reviews/2026-09-06_foundation-inspector-rocko-feasibility.md
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

467 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FI-ROCKO-1 — Foundation synthetic inspector: measured feasibility note
Author: rocko (Claude Fable 5.1, Archify lane author seat). Requested by: darkwing
(FI-ROCKO-1, 2026-09-06). Issue: #53. Status: planning evidence only. This note
proposes; it does not approve, implement, or bind. Every choice below is a
recommendation for darkwing to accept, alter, or reject in the frozen charter.
## 0. Admission
**FI-ROCKO-1 is admitted. No blocker.**
- Compatibility: my current owner goal (Archify C1 rev 3, HELD for Jason's
T2/T3 rulings) is a waiting state with no lane action due; this note displaces
nothing. I take no supervisory authority over darkwing, filbert, or Jason's
queue; Jason may veto or discard this note at any time.
- Authority basis observed, not assumed: `docs/plans/CURRENT.md` (working tree)
records Jason's acceptance of the map and "continue with filbert/rocko"; the
charter draft restates it. I did not verify this with Jason directly.
- Scope honoured: this file is the only repository write. No commit, push,
checkout, reset, live/runtime test, credential read, or `~/.mosaic`
investigation was performed. Dewey's `extensions/**`, `.pi/**`, and scripts
were not touched. Filbert's verdict files were not opened for editing.
## 1. Pinned inputs
| Input | Identity | How read |
|---|---|---|
| Accepted map `docs/plans/2026-09-06_agent-project-workspace-foundation.md` | commit `7345f330fc6bfae5aa1d896c78cfb7cbe62efbae` | `git show 7345f33:…` |
| Source/plan baseline (candidate schema, semantic-model.py, check.py, README, REVIEW, RUNTIME, phase-2 contract, ROADMAP, foundation plan) | commit `d4696d09eb1b5dcf1028f30db2cd63735f51cb16` | `git show d4696d09:…` |
| Charter draft 1 `docs/plans/2026-09-06_foundation-inspector-charter.md` | untracked; SHA-256 `fdcdf4df8bf30b9fbfff960f9b1d4160ce7e6bbc4b8bb601a93cda13b56c8996` at write time | working tree |
| Filbert map verdict (FM-FILBERT-1) | SHA-256 `6b08c6fac0718d3db527cf9ffbfab49407e7b289d09782f5d1d0e26493eaabb3` (as cited in the charter) | read only |
The charter is a moving draft: an earlier read in this session hashed to
`9c065ef1…`. Recommendations below target the `fdcdf4df…` text. If the frozen
charter differs materially, this note must be re-checked, not assumed valid.
Citations use `commit:path:line`. `cand/` abbreviates
`docs/plans/foundation-v1-candidate/`.
## 2. Repository facts that constrain the design
- Host toolchain: Node v26.8.1 with `node:test` available; Python 3.12.8 with
`jsonschema` 4.26.0 importable. `package.json` declares one dependency
(pi 0.84.4); no `node_modules` is present in the tree. `RELEASE` = 0.0.12.
- `scripts/` is flat; no `packages/` tree exists yet. ROADMAP places the
package layout under M20 (`d4696d09:docs/plans/ROADMAP.md:114`) with the
layout doctrine and phase-1 sequencing at `:142-145` and the target tree at
`:152-159` (`packages/config` at `:159`).
- The map's recommended increment forbids npm or Turbo changes
(`7345f33:docs/plans/2026-09-06_agent-project-workspace-foundation.md:278`)
and proposes exit classes 0/2/3/4 (`:275`).
- Existing validation idiom is hand-rolled and closed: `scripts/mosaic-task.mjs`
(`rejectUnknownKeys`, `fail(2, …)`). `scripts/mosaic-config.mjs` reads
`$MOSAIC_CONFIG` (`:42-43`) and refuses symlinks (`:54`); importing it runs
config resolution, which the charter forbids for the inspector.
- Existing suite style: `scripts/test-config.sh` uses a `mktemp` sandbox,
`check`/`expect_exit` helpers, and `NO_COLOR`.
- `cand/FINDINGS.md` records an `ajv`/`fast-uri` audit concern; the candidate's
own checker uses Python `jsonschema` (`cand/check.py`).
- The candidate's fingerprint rule is restricted-domain canonicalization, not
full RFC 8785 (`d4696d09:cand/README.md:265-288`, ASCII-only vectors
`:281-282`; `check.py` sorts keys, uses compact separators, ASCII-only,
sorts skills by id, digest = `"sha256:" + hex(SHA-256(UTF-8))`).
## 3. Language and dependency pin (recommendation)
**Recommend: Node ESM (`.mjs`), zero new dependencies, hand-rolled closed
validation.** Pin: the repository's existing Node engine (v26.8.1 on host); no
`package.json` change; no `ajv`, no `jsonschema` at runtime.
Why:
- Matches the map (`:278`, no npm/Turbo change) and the repo's validation idiom.
- Avoids the `ajv`/`fast-uri` audit issue and avoids adding a runtime dependency
for a preview tool that must be reproducible offline.
- Node gives `TextDecoder('utf-8', { fatal: true })` for malformed-UTF-8
refusal, `node:crypto` for SHA-256, and `node:test` for units, all built in.
- A hand-rolled closed validator is smaller than the candidate schema but must
be **derived from** `cand/records.schema.json` field-by-field. To keep it
honest, the test suite should run the Python candidate checker over every
fixture bundle's records as a cross-check when `python3 -c "import jsonschema"`
succeeds, and skip with a visible notice otherwise (see §9).
Alternative (viable, not preferred): Python 3.12 stdlib only, reusing
`check.py`'s canonicalizer verbatim. Rejected because the CLI surface of the
stack is Node/bash and because the charter says not to promote the author-only
Python model into the inspector. This is unresolved decision D-A.
Explicitly not recommended: JSON Schema at runtime via `ajv` (audit issue,
dependency) or via Python `jsonschema` in a Node CLI (two runtimes).
## 4. Physical source paths (recommendation, M20-consistent)
No `packages/` scaffolding in this increment. Files land in `scripts/`, which
is the existing flat layout, with a comment header naming the logical owner so
M20 phase 1 can move them without rewriting.
| Path | Logical owner (M20 target) | Contents |
|---|---|---|
| `scripts/foundation-inspect.mjs` | `packages/mosaic` (CLI presentation) | argv parsing, file read, output rendering, exit classes |
| `scripts/foundation/strict-json.mjs` | `packages/config` | strict JSON reader: fatal UTF-8, duplicate-key refusal, depth/size limits, byte-offset-only errors |
| `scripts/foundation/canonical.mjs` | `packages/config` | restricted-domain canonicalizer + `sha256:` digest, out-of-domain refusal |
| `scripts/foundation/resolve.mjs` | `packages/config` | bundle validation, reference resolution, graph checks, permission intersection, proposal evaluation. Pure: no I/O, no env, no clock |
| `scripts/foundation/fixtures/*.json` | test data | demo bundle plus negative fixtures |
| `scripts/test-foundation.sh` | suite | sandboxed CLI/privacy/non-effect suite, invokes `node --test scripts/foundation/*.test.mjs` |
| `scripts/foundation/*.test.mjs` | unit tests | `node:test` units for the three pure modules |
Rules:
- `resolve.mjs`, `canonical.mjs`, `strict-json.mjs` import only each other and
`node:crypto`. Never `mosaic-config.mjs`, never `process.env`, never `Date`.
- `foundation-inspect.mjs` is the only file that touches `process.argv`,
`fs`, `stdout`, `stderr`, and `process.exit`. It reads `process.env` only to
honour `NO_COLOR`; it never reads `MOSAIC_CONFIG`, `HOME`, or the data root.
- Root stays bootstrap-only (AGENTS.md invariant 1): nothing is added to root.
- The command name `mosaic foundation inspect <bundle.json>` from REVIEW
(`d4696d09:cand/REVIEW.md:123`) is the eventual `packages/mosaic` surface.
In this increment the wrapper is `scripts/foundation-inspect.mjs`; a
`foundation-inspect.sh` shim is unnecessary. Naming is decision D-H.
## 5. Bundle contract (exact fields, proposed to freeze)
One file, one closed object. Every object is closed: unknown keys refuse.
Duplicate keys refuse at parse time. All strings must be ASCII printable when
they participate in digests (§6); identifiers are `^[a-z0-9][a-z0-9-]{0,63}$`.
```
{
"bundleVersion": 1,
"kind": "foundation-inspector-bundle",
"simulation": true, // must be literally true; anything else refuses
"records": [ <record>, ... ], // candidate envelope records, ≤256
"registries": [ <registryEntry>, ... ], // ≤256
"selection": {
"agentId": "<id>",
"projectId": "<id>",
"workspaceId": "<id>",
"assignmentRef": <recordRef> | null,
"launchRestrictions": <restrictions> | null // simulated execution layer, D-F
},
"operation": { "name": "<one of the 29 operation names>", "target": <target> | null },
"proposal": null | {
"kind": "assignment-change",
"requesterAgentId": "<id>",
"subjectAssignmentRef": <recordRef>,
"targetTaskRef": <recordRef>,
"delegationRef": <recordRef> | null, // decision, decisionKind delegation
"changeDecisionRef": <recordRef> | null, // decision, decisionKind assignment-change
"message": "<string>" | absent, // informational only; never authority
"runtimeObservations": {
"originalExecutionStopped": true|false|null,
"effectsReconciled": true|false|null
}
}
}
```
`<record>` is exactly the candidate envelope (`d4696d09:cand/records.schema.json`):
`{schemaVersion:1, kind, id, scope, revision, createdAt, createdBy, supersedes,
authorizationRef, payload}`, closed. `createdAt` is carried and validated as a
string but never used in calculations (charter: no timestamps in results).
`<recordRef>` = `{kind, id, scope, revision}`; `<registryRef>` =
`{registry, id, revision, digest}`; `<restrictions>` = `{operations[],
readPaths[], writePaths[], network, endpointRefs[]}` with `pathGrant`
`{root:"workspace", path|null}`; all as defined in the candidate schema.
`<target>` for file operations: `{root:"workspace", path:"<relative>"}`. For
record operations: a `<recordRef>`. For operations that take no target: `null`.
An operation whose target shape does not match its name refuses `invalid-request`.
`<registryEntry>` has two shapes:
- Declared: `{registry, id, revision, digest}` for harness, settings,
context-content, endpoint. Existence and exact-match only.
- Content-bearing: `{registry, id, revision, digest, content}` for
`scope-role`, `agent-policy`, `project-policy`. `content` is
`{restrictions: <restrictions>}`; `digest` must equal the canonical digest of
`content` (§6) or the bundle refuses `invalid-request`. These are the
"separately identified mock policy inputs" the charter requires. The candidate
does not define registry content shapes; this is decision D-E.
Bounds (refuse `invalid-request` before any calculation):
| Bound | Value |
|---|---|
| File size | ≤ 1 MiB (aligns with RUNTIME frame limit `d4696d09:cand/RUNTIME.md:41` and contract `…phase2-contract.md:116`) |
| `records` | ≤ 256 |
| `registries` | ≤ 256 |
| Any array | ≤ 1024 items (contract `:116`) |
| Nesting depth | ≤ 32 |
| Any string | ≤ 4096 bytes; path strings ≤ 4096 bytes |
| Input file | regular file only; symlink, directory, FIFO, device refuse `invalid-request` (matches `mosaic-config.mjs:54` doctrine) |
| Path argument | exactly one positional bundle path; relative paths resolved against cwd but never searched; no defaults |
Explicitly absent by design: no `$ref`, no includes, no URLs, no `env`, no
`credentials`, no `authenticated`/`authorized` booleans anywhere. Presence of
any of those keys is an unknown-key refusal, not a warning.
## 6. Reference and digest algorithm
### 6.1 Canonical digest (restricted domain)
Implement `canonical.mjs` as the candidate's rule, not JCS:
1. Domain: objects, arrays, strings, booleans, null, and integers within
`Number.isSafeInteger`. Floats, `-0`, non-ASCII strings, control characters
other than those JSON must escape, and values outside the domain **refuse**
with `unsupported-capability`. This is stricter than JCS and is the documented
restricted domain (`d4696d09:cand/README.md:265-288`).
2. Object keys sorted by UTF-16 code unit order (same as `sort_keys=True` on
ASCII keys), compact separators, no whitespace.
3. Set-valued arrays are sorted by a stable key before serialization:
`skillRefs` by `id`, `instructionRefs` by `id`, `operations` lexically,
`readPaths`/`writePaths` by `path` with `null` first, `endpointRefs` by `id`.
Sequence-valued arrays (`dependencies`, `subjectRefs`, `evidenceRefs`) keep
order. The list of set-valued fields is enumerated in code and in the frozen
charter; anything not listed is a sequence.
4. Digest = `"sha256:" + lowercase hex SHA-256 of the UTF-8 bytes`.
5. Test vectors: reproduce the README's vectors (`:281-282`) and add one
out-of-domain vector per rejected class.
No claim of RFC 8785 conformance is made in code, docs, or output.
### 6.2 Reference resolution (bounded resolver, `resolve.mjs`)
Index build:
- Key `(kind, id, scope)` → list of revisions. Two records with the same
`(kind, id, scope, revision)` refuse `invalid-request` (duplicate identity, A3).
- `supersedes` chains must be contiguous from revision 1: revision *n* must
supersede exactly `(kind, id, scope, n-1)`; gaps, forks (two records
superseding the same predecessor), or a supersedes pointing outside the
identity refuse `invalid-request`. The bundle may carry only the head
revision if it carries no history at all for that identity (D-J).
- `id` must agree with `scope` per README `:64`; mismatch refuses.
Reference check (every `recordRef` and `registryRef` in every payload):
- Exact match on all fields; `digest` compared byte-for-byte after §6.1
recomputation for content-bearing entries. A ref to a superseded revision
refuses `stale-revision`. A ref to an absent identity refuses `missing-state`.
Kind mismatch (e.g. `policyRef` not pointing at `agent-policy`) refuses
`invalid-request`.
Graph rules (all refuse without repair; the violated rule name is emitted):
- `workspace.payload` must reference exactly one active `project` (README `:65`;
charter "exactly one project owns a workspace").
- A workspace `registration` for agent X must reference, via
`projectRegistrationRef`, an **active** project `registration` for the same
agent X in the owning project (README `:65`). Missing or revoked → refuse.
- `mission.parentMissionRef` must resolve to a mission in the same project;
a task's `missionRef` must resolve to a mission in the selected project.
- `assignment` must bind one task and one agent; `intentRef` must resolve to a
`decision` with `decisionKind ∈ {plan-approval, delegation, assignment-change}`
whose `subjectRefs` include the task. Otherwise `not-authorized`.
- Cycle checks (A3, "document which edges participate"): DFS over exactly three
edge sets, evaluated independently: (i) `task.dependencies`, (ii)
`mission.parentMissionRef`, (iii) `supersedes`. A cycle in any of them
refuses `invalid-request`. Edges deliberately excluded from cycle checks:
`intentRef`, `authorizationRef`, `evidenceRefs`, `subjectRefs`, all
`registryRef`s (they point outside the record graph).
- Cross-project execution selection refuses (README `:71`).
## 7. Permission algorithm
Inputs: the resolved graph, `selection`, `operation`. Output: `allowed` or
`refused` + reason code + violated rule + the identities consulted. Never the
payload bodies.
Taskless set: `{work.read, file.read}` (from `semantic-model.py`). Every other
operation requires `selection.assignmentRef` to be non-null (D-D asks whether
`work.propose` belongs in the taskless set; recommendation: no, keep the
candidate's set).
Layers, in fixed order, each producing a `<restrictions>` view or a refusal:
| # | Layer | Source | Missing → |
|---|---|---|---|
| L1 | agent policy | `agent-definition.policyRef` → registry `agent-policy.content.restrictions` | refuse `missing-state` |
| L2 | project registration | active `registration` for agent in `projectId`, `restrictions` | refuse `not-authorized` (A2) |
| L3 | workspace registration | active `registration` for agent in `workspaceId` whose `projectRegistrationRef` is L2's record | refuse `not-authorized` (A2) |
| L4 | project policy | `project.policyRef` → registry `project-policy` | refuse `missing-state` |
| L5 | workspace policy | `workspace.policyRef` if present in candidate payload; else scope-role of L3 | refuse `missing-state` |
| L6 | assignment | `selection.assignmentRef`; must be `status: selected`, agent = `agentId`, task in `projectId` | non-taskless op → refuse `not-authorized` |
| L7 | task | `task.restrictions` | null = no narrowing |
| L8 | mission chain | each mission up the `parentMissionRef` chain, `restrictions` | null = no narrowing |
| L9 | simulated execution layer | `selection.launchRestrictions` | null = no narrowing; when present, narrows only (D-F) |
Intersection semantics (README `:96-142`, contract `:158-193`):
- `operations`: set intersection. An explicit empty array is **none** (`:101`).
A `null` restrictions object at L7L9 means "no narrowing"; at L1L5 a
`null` refuses (required layer, `:133`).
- `readPaths`/`writePaths`: a grant `{root:"workspace", path:null}` is the whole
root (`:102`). Intersection of two grant lists = the set of pairs where one
is a component-prefix of the other, keeping the narrower. Component-prefix
means path segments, not string prefix (`a/b` covers `a/b/c`, not `a/bc`).
Target paths must be relative, normalized, contain no `..`, no empty
segment, no leading `/`, no `\`, no NUL; otherwise refuse `invalid-request`
before any permission result (A5).
- `network`: `none` ∧ anything = `none`; `approved-endpoints`
`approved-endpoints` = intersection of `endpointRefs` by exact ref.
- Never union across assignments (contract `:172`); exactly one assignment is
consulted (README `:134`). A second `selected` assignment for the same
agent+task in the bundle is not an error, but only the selected one is used.
- Result for the operation: allowed iff `operation.name ∈ operations` and,
for file operations, the target path is covered by the corresponding
`readPaths`/`writePaths` after intersection, and for network operations the
endpoint is covered.
Refusal precedence (deterministic): parse/shape errors (exit 2) → graph
incoherence (exit 2, D-C) → missing required layer (exit 3, `missing-state`)
→ registration absent (exit 3, `not-authorized`) → operation not in
intersection (exit 3, `not-authorized`). The first failing check ends
evaluation; the output names only that rule.
## 8. Proposal mode (cross-lane reassignment, map `:290-314`)
Evaluated only when `proposal !== null`, after the base permission result:
1. `requesterAgentId` must hold an active registration in the target task's
project (authority in target scope) — else refuse `not-authorized`.
2. `delegationRef` must resolve to a `decision` with `decisionKind: delegation`,
outcome approved, `delegatedOperations` including `assignment.change`, and
`subjectRefs` covering the subject assignment or its task — else refuse
`not-authorized`. A `message` with no delegation refuses with the same code
and the violated rule "message-is-not-authority" (A7).
3. `changeDecisionRef` must resolve to a `decision` with
`decisionKind: assignment-change` whose `subjectRefs` include both the
subject assignment and the target task and whose `basisRef` is the
delegation decision — else refuse `not-authorized`.
4. `runtimeObservations`: if either field is `null`, result is **unresolved**
with reason `unknown-effects` (charter: report reconciliation-required
rather than simulate success). If `originalExecutionStopped` is `false`
or `effectsReconciled` is `false`, refuse `unsafe-replacement`.
5. On any outcome, the output's `selection.assignmentRef` is echoed unchanged
and the bundle bytes are never written (A6, A7).
Exit class for unresolved: 3 (it is a refusal to affirm), with
`result: "unresolved"` in JSON and text so it cannot be read as a permission
refusal. Requester model in increment 1 is agent-only (D-G).
## 9. Output and exit classes
- Text (default) and `--json` must be generated from the same result object;
the text renderer is a pure function of the JSON. First line of both, always:
`SYNTHETIC PREVIEW — NO LIVE EFFECTS` (text) / `"disclaimer": "SYNTHETIC
PREVIEW — NO LIVE EFFECTS"` (JSON), including on every refusal and on
exit 2/4. REVIEW's phrasing "preview: no live registrations or permission
grants" (`d4696d09:cand/REVIEW.md:126`) is emitted as the second line.
- JSON result shape (closed): `{disclaimer, bundleVersion, result: allowed |
refused | unresolved, reason: <code>, rule: <rule-name>, selection: {agentId,
projectId, workspaceId, assignmentRef}, operation, consulted: [<recordRef |
registryRef>], proposal: null | {result, reason, rule, selectedAssignmentRef}}`.
No payload bodies, no paths other than the requested target, no input
snippets, no timestamps, no random ids, keys sorted.
- Reason codes: the RUNTIME closed set (`d4696d09:cand/RUNTIME.md:97`:
allowed, invalid-request, not-authorized, stale-revision, missing-state,
already-active, control-conflict, retired, unsupported-capability,
audit-unavailable, unknown-effects, unsafe-replacement, request-id-conflict,
expired) plus inspector-only codes `unsupported-kind`, `unresolved`,
`io-failure`. Inspector-only codes are documented as not being runtime API
codes (charter).
- Exit classes: 0 allowed; 2 malformed input (parse, shape, bounds, graph
incoherence, unsupported kind/operation); 3 simulated refusal or unresolved;
4 I/O failure (missing file, unreadable, not a regular file). Exit 4 output
names the path as given on argv and nothing else; no bootstrap, no config
read, no directory creation (A8).
- Errors from `strict-json.mjs` report byte offset and error class only, never
the surrounding bytes.
## 10. Supported record kinds and operations
Supported kinds in increment 1: `agent-definition`, `project`, `workspace`,
`registration`, `mission`, `task`, `assignment`, `decision`. The remaining
candidate kinds refuse `unsupported-kind` (exit 2) when present in `records`,
rather than being ignored. Operation vocabulary is the candidate's 29-name enum
(`d4696d09:cand/RUNTIME.md:55`); a name outside it refuses `invalid-request`.
Operations whose semantics need runtime state the inspector cannot model
(stop, durability, audit) are accepted as names for permission intersection
only; the output states `"scope": "permission-only"` for them so a `0` is not
read as an executability claim.
## 11. Fixtures and the first demonstration
`scripts/foundation/fixtures/demo.bundle.json` implements the charter's
scenario: project P1 with workspaces W1 and W2; project P2; agent A registered
in P1 and W1 only; one mission, one task in W1, one selected assignment for A;
agent B registered in P2 with a delegation decision that does **not** cover
`assignment.change`. Derived bundles (same records, different selection or
proposal) cover:
| Fixture | Expectation |
|---|---|
| `demo-read-w1` | exit 0, allowed, `file.read` on `docs/notes.md` |
| `demo-read-w2` | exit 3, not-authorized, rule workspace-registration-missing, no W2 payloads in output |
| `demo-cross-project-exec` | exit 3, not-authorized, cross-project selection |
| `demo-reassign-message-only` | exit 3, not-authorized, message-is-not-authority, selection unchanged |
| `demo-reassign-no-observations` | exit 3, unresolved, unknown-effects |
| `neg-duplicate-identity`, `neg-stale-ref`, `neg-missing-parent`, `neg-cycle-task`, `neg-cycle-mission`, `neg-fork-supersedes` | exit 2, named rule |
| `neg-unknown-key`, `neg-dup-json-key`, `neg-bad-utf8.bin`, `neg-bad-path` | exit 2, before any permission text |
| `neg-empty-ops-layer` | exit 3, explicit empty = none |
| `neg-union-two-assignments` | exit 3, second assignment does not widen |
Fixture digests are generated by a checked-in helper
(`node scripts/foundation/canonical.mjs --digest <file>`) and the suite fails
if a stored digest disagrees with a recomputed one, so fixtures cannot drift
silently.
## 12. Test harness (isolated, non-effect, privacy)
`scripts/test-foundation.sh`, same shape as `scripts/test-config.sh`:
- Runs under `env -i PATH=… HOME=<mktemp> NO_COLOR=1 node …`; `MOSAIC_CONFIG`
unset; no `~/.config/mosaic-dev` or data root exists in the sandbox. A
canary `HOME/.config/mosaic-dev/config.json` is **absent** and the test
asserts it stays absent (no bootstrap on missing input, A8).
- Non-effect: before and after every CLI invocation, `find <sandbox> -newer
<stamp>` must be empty and `sha256sum` of each bundle must be unchanged (A6).
Also asserts no `.pi`, `roles`, or `state` directory appears under the sandbox.
- Privacy: fixture payloads contain a canary string
(`CANARY-DO-NOT-PRINT-<n>`) in every unrelated record body and in W2's
payload; every stdout/stderr capture is grepped for it and must be clean
(A2, charter diagnostics rule). Exit-2 parse errors are checked for absence of
any input bytes beyond the byte offset.
- Determinism: each fixture runs twice; outputs must be byte-identical;
`--json` and text must agree on `result`/`reason`/`rule` (A8).
- Golden files: `scripts/foundation/fixtures/expected/<name>.{txt,json}`,
compared exactly.
- Units (`node --test scripts/foundation/*.test.mjs`): canonicalizer vectors
and out-of-domain refusals; strict-json duplicate key, depth, size, fatal
UTF-8; resolver graph rules one per test; permission intersection tables
(empty=none, null=whole root, component-prefix, no union); proposal rules.
- Cross-check (optional, skipped visibly when Python `jsonschema` is absent):
`python3 cand/check.py` over each fixture's `records` to confirm the
hand-rolled validator has not drifted from the candidate schema.
- The suite does not touch Dewey's extension installation, `~/.mosaic`, the
network, or any engine/process launch. It states this in its header. The
existing required suites remain untouched and must stay green.
## 13. Unresolved decisions for the frozen charter
| Id | Decision | Recommendation |
|---|---|---|
| D-A | Language: Node ESM vs Python stdlib | Node ESM, zero deps (§3) |
| D-B | Directory layout before M20 | `scripts/foundation/` with owner headers, no `packages/` (§4) |
| D-C | Exit class for graph incoherence (cycle, stale, duplicate) | 2 (malformed input), not 3, so it cannot be mistaken for a policy refusal |
| D-D | Taskless operation set | `{work.read, file.read}` exactly; `work.propose` stays assigned-only |
| D-E | Registry content shapes for scope-role / agent-policy / project-policy | `{restrictions}` only, declared simulation-only in the charter; candidate schema does not define these |
| D-F | Simulated execution layer | `selection.launchRestrictions`, narrowing-only, default null |
| D-G | Requester model in proposal mode | agent-only (agentId + registration); no human/operator principal in increment 1 |
| D-H | Wrapper naming | `scripts/foundation-inspect.mjs`, no `.sh` shim |
| D-I | Delegation enforcement depth | require `delegatedOperations ∋ assignment.change` and subject coverage; do not model delegation chains |
| D-J | Whether bundles carry revision history | permit either head-only or contiguous full history per identity; forbid partial history |
One further point the charter should settle explicitly: whether a `0` result
for stop/durability/audit-class operations is acceptable at all in increment 1,
or whether those names should refuse `unsupported-capability` outright. My
recommendation is the latter for `execution.stop`-class names, to keep A9's
"understands the limitation" honest.
## 14. Feasibility verdict
Feasible as a bounded increment with no new dependencies, no package
migration, and no runtime coupling. Estimated size: three pure modules of a
few hundred lines each, one CLI wrapper, roughly twenty fixtures, one bash
suite, and unit tests. The main risk is drift between the hand-rolled validator
and the candidate schema; the Python cross-check in §12 and the enumerated
set-valued field list in §6.1 are the mitigations. Nothing here weakens the
accepted semantics; where the candidate is silent (D-E, D-F), the proposal is
narrowing-only and simulation-labelled.
This note is evidence for darkwing's charter. It is not approval, and it
authorizes no implementation.