Conversation
afogel
force-pushed
the
slice/v6
branch
2 times, most recently
from
August 10, 2026 06:17
068cd3b to
c9821d8
Compare
afogel
force-pushed
the
slice/v6
branch
2 times, most recently
from
August 10, 2026 06:28
75f1019 to
e849829
Compare
afogel
force-pushed
the
slice/v6
branch
2 times, most recently
from
August 11, 2026 03:45
cab302a to
2456279
Compare
afogel
force-pushed
the
slice/v6
branch
2 times, most recently
from
August 11, 2026 11:07
4f766b0 to
bdea8ef
Compare
afogel
force-pushed
the
slice/v6
branch
2 times, most recently
from
August 13, 2026 18:09
7e5b7d5 to
fd49db5
Compare
V6 has no code yet, so the affordance tables are still the only place these two functions are named -- and both names were set to mislead the implementer who writes them first. N22 `appendSessionEntry()` -> `appendContextEntry()`. Its store is SessionContext (S3), so any reader of that store is `loadSessionContext`, and the pair `loadSessionContext` / `appendSessionEntry` names one store with two different nouns. N25 `persistResultLabels()` -> `persistIfcLabels()`. This Guardian has had a real result gate since V4 (`post_tool_call`), so "result labels" now reads as that gate's labels rather than as AGT's IFC tags, which is what they are. S5 and the demo sentence stop making ACS `Provenance` the label store. `provenance.json` defines `provenance_id` / `origin` / `source_id` / `derived_from` and is already a field on every tool argument and result output item in the v0.1.0 hook schemas; the labels ride it as a named `IfcLabels` field, so the docs say field rather than store. N23's row and node pick up the names its two assemblers actually ship under -- `assemblePreToolCallSnapshot` / `assemblePostToolCallSnapshot`, per packages/guardian/src/assemble-snapshot.ts, whose header cites these tables as the authority for its own naming.
…ists `@acs/host-adapter` already exports SessionConfig, SessionConfigStore, ResolvedSessionConfig, SessionConfigNotStoredError, SessionConfigStoreFailedError and SessionFailureKind, every one of them about the handshake. V6 adds a session-shaped noun that is about something else, and with no V6 file written yet that name is still free to be chosen deliberately rather than by whoever types it first. Five commitments, each a sentence an implementer can be held to rather than a bullet of vocabulary: SessionContext stays distinct from SessionConfig and stays Guardian-side (R6.2, A3); the IFC label store is `IfcLabels` / `source_labels` and ACS `Provenance` is not it; N25 is `persistIfcLabels` twinned with `supplySourceLabels`; N22 is `appendContextEntry` twinned with `loadSessionContext`; and session state is injected into both shipped assemblers rather than collapsing N23 back into one function that asks which intervention point it is on. Nothing here asserts what an unwritten function will do -- each claim about a shipped object names the file it is shipped in. The demo sentence picks up the same field-not-store correction as the master doc.
Renaming N23's Detail C row left two live-voice sites still naming the retired assemblers, both of which now contradict that row. `acs-reference-impl-slices.md:173` (§V4 body prose) described the system as it is and said `N23 assembleSnapshot` / `assembleResultSnapshot`; it now says `assemblePreToolCallSnapshot` / `assemblePostToolCallSnapshot`. `slices/v4/README.md:88-89` had the same defect in the same voice, in its list of what V4 delivered, and moves the same way. The sibling structure those sentences describe is unchanged and still true -- only the names were stale. **Why this commit reaches into a V4 file from slice/v6.** It puts one V4 README hunk in V6's PR diff, which is not where a reviewer expects it. The trade is deliberate: the alternative is a master doc whose Detail C row and whose V4 README disagree about what two shipped functions are called, and a reader trusting the live-voice prose would cite a name that exists nowhere in the tree. A contradiction in the docs costs more than a slightly wider diff. Two sites keep the old names because they are dated records rather than live description, and both now say so rather than leaving a reader to infer it: §V1's affordance row (a new note dates it and points at Detail C for current names) and §V4's correction paragraph (which quotes the sentence it corrects, so changing the names would break the quotation). Also: V6 commitment 5 moves from "is injected" to "must be injected", so it reads as the obligation it is rather than as a behavioural claim about code that does not exist yet.
Fix round 1 parked six of these hits behind "needs frozen-path access". That was wrong. `scripts/verify-zero-diff.sh` freezes `policy/lib/`, not `policy/`, and `packages/guardian/src/`, not `packages/guardian/test/` -- and the script's own comment says why the manifest is deliberately outside the fence: it is the deployment's tool registry, not its policy. Only `packages/guardian/src/handshake.ts:54-55` was ever out of reach. policy/manifest.yaml was the sharpest instance and is now fixed: it pointed a reader at packages/guardian/src/assemble-snapshot.ts by a name absent from that file, which is the exact failure the sweep exists to prevent. Two guardian test comments move for the same reason. `assembleResultSnapshot` was never a function. `git log --all -S"export function assembleResultSnapshot"` returns nothing, and V4's own 32dec42 shipped that assembler as `assemblePostToolCallSnapshot` directly -- the old name lived only in prose. So there is no shipped history for it to be a record of, which is why §V4's unquoted clauses were renamed rather than dated. `assembleSnapshot` is different: it really did ship, from ccebb52 until ed274c7 renamed it, so the one quotation that carries it stays. §V4's correction paragraph now leads with the naming note instead of closing with it, so a reader meets the disclaimer before the stale name rather than after -- the previous placement left the whole paragraph reading as current fact until its last sentence.
The reviewed default used origin: "acs.guardian", which is not one of the seven values spec/acs/specification/v0.1.0/provenance.json defines for Provenance.origin. Add ProvenanceOrigin as a named union of those seven values and type origin with it, so a value outside the enum is a compile error here rather than a schema failure downstream. The Guardian's synthesized session provenance now reads origin: "system" with source_id: "acs.guardian" -- the spec's field for "identifier within the origin" is where that name belongs. Also cover putProvenance, which was on the brief's produced interface but untested: assert it replaces the provenance record, ifc_labels included, and that the replacement reads back through loadSessionContext. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…side copy
Task 2 fix round 1: the propagated-labels citation named policy/lib/ifc.rego,
the upstream module AGT_ifc.rego's own header disclaims ("AGT users MUST
import data.agt.ifc rather than the upstream package"). Cite
policy/lib/agt_ifc.rego instead -- the module policy/lib/agt_default.rego
actually imports -- and say why it is the one that runs. Also add the
write-side defensive-copy test that was missing next to the read-side one.
Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…hat the chain knows Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…arries no labels Turns on AGT's own IFC gate (policy/lib/data.json's agt.defaults.config.ifc. sink_clearance: "confidential") and seeds emptySessionContext's provenance at the lattice floor (["public"]), because policy/lib/agt_ifc.rego's flow_allowed_with_lattice requires count(labels) > 0 -- a zero-label flow is denied outright, not waved through, so an unseeded session could do nothing at all. Six pre-existing fixtures (test/redaction.test.ts, packages/agt-bridge/test/ bridge.test.ts, packages/guardian/test/assemble-snapshot.test.ts) build snapshots by hand, bypassing the session store the seed lives in. Each now carries the ["public"] label the Guardian would have supplied, so it represents what this deployment actually produces. A new precedence test in test/ifc-round-trip.test.ts pins that a labelled snapshot still reaches the pattern gate -- IFC deny outranks every other gate in agt_default.rego's own severity ranking, so this is what proves the six edits are fixture repairs and not silenced tests. Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…n, and strip round citations from shipped comments
Important: "denies the -fr spelling too" (packages/agt-bridge/test/
bridge.test.ts) was the only coverage of data.json's second destructive-
command regex, and it had no label -- so it was passing for IFC's reason
rather than the pattern's, and deleting that regex would have left the
whole suite green. Gave it the same public label and reason assertion as
its sibling; proved it by temporarily removing the regex from data.json
(cp backup, restored byte-identical after) and watching the test go red.
Also: restored the "surfaces the policy config to Rego" test's ability to
tell the patterns config surfaced from the ifc config surfacing (label
plus a reason assertion); corrected the publicLabel comment in
bridge.test.ts, which claimed a selection rule ("checks more than
.decision") its own code contradicted; and stripped "(Task 4, fix round
N)" citations from shipped comments across six files -- an SDD workspace
reference that will be unresolvable once this slice lands, with the
behavioural half of each comment kept intact.
Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…ved sessions, and stop main.ts from splitting rendered text to find a row Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…r-misses a reader would hit Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
Seven statements in shipped text said things the code beside them does not
do. None of them changed behaviour and none is fixed by changing behaviour;
each is replaced by what was measured.
- The `bridge` option and the two `recordingBridge` comments said the stock
IFC gate was off and that no real verdict carried `result_labels`. It is
on (`policy/lib/data.json` sets `config.ifc.sink_clearance`) and they do
(`test/redaction.test.ts`). The real reason the stub exists is that AGT
propagates the labels it is handed and originates none, so a real bridge
could only ever show `public -> public`, and it returns a verdict rather
than the snapshot it was given.
- The session-context log fail-open test claimed a hand-verified ENOTDIR.
Measured: EEXIST -- `mkdirSync(dirname(path), {recursive: true})` is asked
to create a directory where a plain file already sits. ENOTDIR is the
errno for a path beneath such a file, which this is not.
- `handshake.ts` named `assembleSnapshot` / `assembleResultSnapshot` as the
dispatch targets. Neither exists; the branches call
`assemblePreToolCallSnapshot` / `assemblePostToolCallSnapshot`.
- Three files said correlating a call with its result "is V6's session
chain". The chain records both steps in one session's order, each entry
carrying the arriving message's own `request_id`, and holds no reference
from a result back to its call. `request_id_ref` is declared and read
nowhere. Correlation is unimplemented.
Also, without changing what any of it does: name the hash test for what it
measures, say what the chain check is not (it compares links and never
recomputes `hashEntry`, so a self-consistent rewrite, a trailing truncation
and a deleted first entry all read as unbroken -- each measured), correct an
import alias that named the wrong package, and record that the session store
is never evicted and its projection never rotated.
V6's plan is now tracked and linked from the slice README and from the
slices doc, as V1-V5's are.
Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
…thing
Each finding is a defect rather than a trade-off, so each is fixed rather
than recorded as a divergence.
`AgtSessionState` is gone. It was a one-field bag (`{ sourceLabels }`)
announcing session state and holding labels, bound to a parameter named
`session` in `ifcMember`, both assemblers and `server.ts`'s assembler type --
on the same step where V3 already binds `GovernStepInput.session` to
`ResolvedSessionConfig`, and under a name commitment 1 reserves for neither
object it governs. Both assemblers now take `sourceLabels: IfcLabels`, which
also drops a fourth spelling of the same tags. No parameter in the Guardian
is named `session`. The test fixture is `EMPTY_SOURCE_LABELS`, not
`NO_SESSION_STATE`: an empty label list is a value these assemblers pass
through, not the unseeded session the `["public"]` seed exists to prevent.
`IfcLabels`' doc called itself a store, which this slice's own README already
denied.
`Provenance` is `SessionProvenance`. Two provenance records live here and
never meet: the wire's `{value, provenance}` member, typed `unknown` and
stripped at C5, and this one, synthesized per session and seeded. One word
for both invited a reader of provenance.json to think they were the same.
N25 no longer asks. `persistIfcLabels` kept `undefined` vs `[]` -- a fact
about a verdict, read next to the verdict -- but loaded the session, spread
its provenance record and wrote the whole thing back through `putProvenance`,
which could replace `origin` and `source_id` while meaning to set labels.
The store is told instead: `replaceIfcLabels` and `sourceLabels`, both naming
the field, with the defensive copies moved to the owner of the array.
`supplySourceLabels` returns `IfcLabels`, so the naive mutation no longer
compiles; its test casts to keep measuring the runtime copy underneath.
S3's name is S3's alone. `SessionContext` carried entries, intent and
provenance, so `loadSessionContext` returned something wider than its name.
`SessionContext` is now the chain; `SessionState` is the aggregate the store
holds; `loadSessionContext` returns the former and `store.load` the latter.
`renderSessionChainRow` is split. It decided the chain break AND recorded the
hash, under a module header claiming every function pure -- so rendering was
a write, and rendering one entry twice reported a break the log did not
contain. `checkSessionChainLink` records and decides; the renderer is pure.
Both cases are now tests.
U22 prints `session_id=`, not `session=`. S14's row prints `audit_session=`,
qualified because that value is the host's raw id; both stream past one eye
when `main.ts` tails both logs. The Inspector barrel exports the third tailer
and the chain functions beside the other two tailers.
Detail C drew U22 as a third child of N50 (`tailEnvelopeLog`), which it never
calls -- it reads a different file through `tailSessionContextLog`. Table
first, then the mermaid: N50's row loses U22, S3's gains it, and the edge is
`S3 -.-> U22` with no minted N-number, because minting one is a shaping call.
Filed in §V6 as the ask, alongside the same gap for `supplySourceLabels` and
`loadSessionContext`.
Re-captured rather than hand-edited: both U22 blocks in the runbook, from the
same tracked log through the same commands. The label-carriage capture was
re-run too and is unchanged apart from its generated session id.
795 pass / 1 skip / 0 fail, typecheck clean, verify:pin clean.
Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
Comments in this slice carried review archaeology ("PR #15 review", "used to
be", a retired putProvenance/persistResultLabels narrated instead of just its
current shape), internal shaping identifiers (S3, S4, S5, N21-N25, R2.1,
R5.1, U22, C5) that resolve only against documents not in front of them, and
slice-number scheduling references (V1, V4, V6) standing in for what those
slices actually did. Test and describe/it names carried the same
identifiers and were renamed to say in words what they named.
Every comment here now stands on its own: what the code does, and the
reasoning behind it that the code cannot show itself. Where a constraint
came from an identifier, the constraint is stated instead of named.
Claude-Session: https://claude.ai/code/session_019qZbQWyJHrYG7UodpyKjYr
setIntent has no sender outside tests -- the ACS wire's intent object is unread -- so every Guardian stand-in was made to answer a message production never sends. It moves to SessionIntentStore, which the memory store still implements, so S4 is off the live interface rather than deleted. load() went with it. The module header claimed S5 was reachable only through replaceIfcLabels and sourceLabels while load handed back the whole SessionState: the wide write was gone and the wide read was not. Every read is now one record wide -- context, intent, provenance, sourceLabels -- and the aggregate is the memory store's own business. provenance() exists so that replaceIfcLabels leaving provenance_id, origin and source_id as session birth wrote them stays a measurement rather than an inspection.
policy/manifest.yaml carried the last dense pocket of archaeology: requirement numbers, task numbers, slice names and a PR-review citation, across the two intervention points and all three tool registrations. Each is replaced by the fact it stood for -- the two-file agreement each policy_target has to keep, and why an unregistered tool name fails AGT closed before any rule runs. One count went with it rather than being restated. The post_tool_call comment claimed moving that leaf fails a specific number of other tests "(measured)"; the number was not backed by a measurement anyone could reproduce, so the sentence now says what those other tests can and cannot tell without counting them. The session store's own comments lose their store numbers, and the remaining fixture's two null rows get the same self-explanatory note as the real table.
`append` rebuilt the whole history on every step -- O(n) per step and O(n^2) per session -- and `sessions` had no cap, no TTL and no eviction, so a long-lived Guardian retained every session and every entry for the life of the process. That is a bypass, not an untidiness: a slower Guardian misses the host's negotiated `timeout_config.default_ms`, a missing decision is a delivery failure, and the default `proceed` posture resolves a delivery failure by running the tool ungoverned. `append` now pushes onto the array the session already owns, so a step costs the same at the forty-thousandth entry as at the first. Measured on this tree, per-append cost at 40000 entries against 2000: pushing is 1.07-1.19x, rebuilding was 8.7-9.0x. The bound is on sessions, not on entries. Truncating a session's oldest entries would leave a chain whose first surviving entry links to something absent, and `checkSessionChainLink` documents that it never marks a first entry -- so a front-truncated chain reads exactly like a complete one. Evicting whole sessions cannot produce that: a retained chain is entire and an evicted one is absent. The eviction is reported twice over, by `onEvict` and by the resumed session restarting at genesis, which the Inspector renders as a chain break rather than as a fresh session. `SessionState` now holds the chain as the array it is appended to, and `context` copies on the way out for the same reason `sourceLabels` already did, plus one more: the stored array grows.
README.md was byte-identical to slice/v5's. V6 gave both the Guardian and the Inspector a session-context log and re-transcribed neither banner, so six claims here were false: a three-line Guardian banner carrying `(S6)` / `(D8)` labels the process does not print, a paragraph asserting as an executed capture that it "prints all three lines", a two-line Inspector banner where the process prints a three-path tailing line and a posture badge, "three kinds" of `.acs/` artifact tailed "two" at a time where there are four and three, and both env tables missing `ACS_SESSION_CONTEXT_LOG` and the Inspector's `--session-context-log`. All six are corrected from measured output: both processes were run and their stdout pasted, per the convention that a fenced block in a doc is a capture rather than a composition. The root cause was that no test in this repo read any doc, so all six stayed invisible behind a green suite. `test/readme-captures.test.ts` parses the fenced captures out of README.md and asserts them against the real stdout of both processes, so a banner line that is added, removed, reworded or relabelled fails `bun test` instead of surviving a slice. The port is normalised and nothing else is -- the quickstart tells the developer to leave a Guardian on 8787, so binding it would flake on exactly the machine that followed the instructions.
V1 added ACS_GUARDIAN_HOST and left the env table untouched, which is the same omission this slice just spent two commits repairing for the session-context log: a variable the process reads and the README does not name. Recorded with the reason for the default rather than only the default, since loopback is a security posture and an operator reaching for 0.0.0.0 should read what it costs before setting it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7
Stacked on
slice/v5. Review the diff against that base, notmain. Slice 6 of 8.What this slice is for
The Guardian remembers the session: each step is chained, and labels AGT emits on one step come back as input on the next.
Without it, AGT’s own spec says the engine stores nothing — and nobody here is doing that job either. IFC / session claims are empty, and V7 has nothing honest to measure there.
V5 proved two hosts share one runtime. This slice is the Guardian doing the host job AGT explicitly delegates. It is not a signed audit chain, and it is not the handshake cache from V3 (
SessionConfigvsSessionContextare different objects).What a reviewer is looking at
On every arriving step — before a snapshot is assembled, so a later deny is still recorded — the Guardian appends a SHA-256-linked entry (
method,request_id,tool_name; never a decision). Labels from AGT’sresult_labelspersist on a session provenance record and return asinput.ifc.source_labels.The store is in-memory. Restarting the Guardian drops it. An optional JSONL projection is what the Inspector tails. Responses do not carry ACS
chain_hash. The Inspector checks thatprev_hashmatches the previous line for that session; it does not recompute the digest, so this is not tamper-evidence.Neither host changes. The demo talks to the Guardian over HTTP. The labels never appear on the ACS wire.
What ships
SessionContextin the GuardianpersistIfcLabels/supplySourceLabels)config.ifc.sink_clearance: "confidential"indata.json(no authored Rego)Intentexists as an immutable store and is not wired from the ACSintentfield yet.Captured walkthrough:
docs/demos/v6-runbook.md.