Skip to content

Audit log: agent provenance — ventisrv content store + veltro trajectory emitters (INFR-355) - #508

Open
pdfinn wants to merge 6 commits into
masterfrom
audit/agent-provenance
Open

Audit log: agent provenance — ventisrv content store + veltro trajectory emitters (INFR-355)#508
pdfinn wants to merge 6 commits into
masterfrom
audit/agent-provenance

Conversation

@pdfinn

@pdfinn pdfinn commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes INFR-355 (Audit log: AI-agent provenance extension — vac content-store layer).

What this is

The v2 "agent provenance" extension the audit log was designed for (docs/compliance/audit-log-design.md §8): record what an agent was prompted with and what it did — system prompt, task, granted caps, every tool call/result, spawns, completion — in the tamper-evident /mnt/audit chain, with bulky payloads stored by reference in a write-once content store.

The pieces

  • ventisrv(8) salvaged (appl/cmd/ventisrv.b, public domain, Mechiel Lukkien's GSoC-2007 ventivac work). The tree has shipped the entire vac client stack since the fork (vacput/vacget/vacfs, module/{vac,venti}.m) and man/4/vacfs already referenced ventisrv(8) — this adds the server. Compiles unmodified against the in-tree venti.m. Append-only data file + regenerable index; localhost.
  • auditprov(2) (module/auditprov.m + appl/lib/auditprov.b): stores a payload as a vac hash tree and seals content=<score> sha256=<hex> size=<n> onto the record via audit(2). The venti score is the locator only — venti addressing is SHA-1, which is collision-broken, so the chain-sealed SHA-256 is what pins the payload (a chosen-prefix collision buys nothing). Store-unreachable degrades to content=unstored — the event still seals. dialraw/attachfd split the dial from the handshake so a connection crosses pctl(NEWNS) into a restricted child. Session RPC is semaphore-serialized (venti Session.rpc is a bare write-then-read; veltro runs tools concurrently).
  • auditget(1): auditor-side fetch by score, -s verifies against the chain-sealed SHA-256.
  • Veltro emitters: parent (veltro.b) seals agentstart/nscaps/sysprompt/task/plan, per-step prompt/llm/toolcall/toolres (full tool output, pre-truncation), agentdone on every exit path; fail-closed under Audit->ONFILE (the secstored authok posture). spawn auto-grants the append-only /mnt/audit/log into every child's caps (auditcontrolpath already keeps root/chain/ctl ungrantable), seals spawn+subcaps per child, and arms the child via a new SubAgent.setprov() (mirrors setmcp). The child (subagent.b) completes the venti handshake inside the restricted namespace and seals substart/subtask/subllm/subtool/subtoolres/subdone. nsconstruct.b's previously-uncalled emitauditlog hook is now driven by restrictns and seals an nsrestrict record carrying the manifest's SHA-256.
  • Boot: when auditing is enabled, the profile also starts the provenance ventisrv on a persistent store under /usr/inferno/audit/venti.
  • Confidentiality model (decided): plaintext blocks by placement — the store lives under the audit dir's perms, read capability is possession of the score, and subjects never see the chain that maps scores to context. Honest residual: venti dedup is a presence oracle for a subject already holding a candidate payload. Encryption-at-rest stays a deployment choice.

Evidence

  • tests/auditprov_test.b — live integration (real ventisrv + real auditfs in-emu): SHA-256 vector, plain and payload-bearing records, content=unstored degradation, dedup, multi-block + empty payloads, fetch-back by record score. 8/8.
  • All veltro/spawn suites pass: veltro_test 16, veltro_security_test 33, veltro_tools_test 12, spawn_test 12, spawn_helpers_test 33, spawn_schedule_test 12.
  • Live end-to-end smoke (headless emu + llmsrv over local Ollama gpt-oss:20b): a real agent run seals the full trajectory — see the chain excerpt in the PR discussion.
  • Compliance docs updated: SP800-92-audit-log.md (AU-12 row + residual table), SP800-53-controls.md, nist-control-mappings.md, roadmap Tier-1 item 2, audit-log-design.md §8/§10. New man pages: ventisrv(8), auditprov(2), auditget(1).

Also fixes a fresh-clone paper cut surfaced by the worktree smoke: n/ was never tracked, so mount {mntgen} /n failed silently on fresh checkouts (n/.gitkeep, matching the mnt/ convention).

Refs: INFR-355

🤖 Generated with Claude Code

pdfinn and others added 5 commits August 20, 2026 02:21
The tree has shipped the full vac client stack (vacput/vacget/vacfs,
module/{vac,venti}.m, appl/lib/{vac,venti}.b) since the fork, and
man/4/vacfs referenced ventisrv(8) — but the server itself was never
in-tree. Salvage it from Mechiel Lukkien's ventisrv (GSoC 2007
ventivac project, public domain): a venti server with an in-memory
index over an append-only data file + regenerable index file.

Compiles unmodified against the in-tree venti.m (only Score/Scoresize/
Vmsg are used). Verified in emu: ventisrv -q config check, and a full
vacput round-trip against a live server (data file carries correct
block headers; index syncs).

This is the content-store substrate for the audit log's agent-
provenance extension (audit-log-design.md §8): bulky prompt/completion
payloads become write-once content-addressed blocks, referenced from
chain records by score.

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nance records

The agent-provenance layer from audit-log-design.md §8, composed from
what the tree already had: payloads go to ventisrv as vac hash trees
(Datatype blocks under a packed entry sealed as a Dirtype block); the
audit record carries 'content=<score> sha256=<hex> size=<n>'. The
venti score is the locator — mechanism only; the SHA-256 is the
integrity pin, sealed into auditfs's SHA-256 chain, so venti's SHA-1
addressing cannot be used to equivocate a payload (chosen-prefix
collisions buy nothing against a pinned record).

module/auditprov.m + appl/lib/auditprov.b — thin lib in the audit(2)
style: attach() dials $auditventi (default tcp!127.0.0.1!17034);
dialraw()/attachfd() split the dial from the handshake so a parent can
hand the connection fd to a namespace-restricted child (the fd
survives pctl(NEWNS), the child handshakes). put()/get() round-trip
payloads; log() seals a record, degrading to content=unstored (rc -2)
when the store is unreachable — the event still seals, the trail shows
the gap. Session RPC is semaphore-serialized (venti Session.rpc is a
bare write-then-read and veltro runs tool calls concurrently).

appl/cmd/auditget.b — auditor-side fetch: score in, payload out,
-s <sha256> pins it to what the chain sealed.

tests/auditprov_test.b — live integration: real ventisrv + real
auditfs (pipe/spawn/mount, no sh namespace split), 8 tests: sha256
vector, plain + payload records, unstored degradation, dedup,
multi-block and empty payloads, fetch-back by record score. All pass.

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire the agent-provenance emitters (audit-log-design.md §8, INFR-355)
through the whole agent stack:

veltro.b (parent): agentstart/nscaps/sysprompt/task/plan at session
setup, per-step prompt/llm/toolcall/toolres in the loop (tool results
captured pre-truncation, full payloads), agentdone on every exit path.
Fail-closed under Audit->ONFILE: an install that requires auditing
refuses to run an agent whose actions cannot be sealed (the secstored
authok posture). The content-store session is dialed before the
parent's own restrictns, so auditing adds no network grant.

tools/spawn.b: when the install audits, every child's caps auto-grant
the append-only /mnt/audit/log (auditcontrolpath already keeps root/
chain/ctl ungrantable — placement does the rest); the parent seals
spawn (task payload) + subcaps (serialized grant) per child, dials a
store connection, and arms the child via the new SubAgent setprov()
(mirrors setmcp: after restrictns, before runloop).

subagent.b: completes the venti handshake inside the restricted
namespace on the inherited fd, then seals substart (system prompt,
tool list) / subtask / per-step subllm / subtool / subtoolres /
subdone — the same funnel as the existing trajectory log, now
tamper-evident.

nsconstruct.b: restrictns now drives the previously-uncalled
emitauditlog hook with the granted caps summary, placed just before
step 10 hides /tmp/.veltro-ns; emitauditlog builds the manifest,
writes it to AUDIT_DIR as before, and seals an nsrestrict record
carrying its SHA-256 so the manifest cannot be quietly edited.

lib/sh/profile: when auditing is enabled, boot also starts the
provenance ventisrv on a persistent store under /usr/inferno/audit/
venti (localhost, auditprov default address; touch never truncates).

All veltro/spawn test suites pass (16+33+12+12+33+12) plus the live
auditprov integration test (8).

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
audit-log-design.md §8 designed-in -> BUILT, recording the two design
decisions the ticket left open: (1) the SHA-256 pin — venti scores are
SHA-1 (collision-broken), so the score is locator only and the
chain-sealed sha256= field pins the payload; (2) the confidentiality
model — plaintext blocks by namespace placement (possession-of-score
is the read capability, subjects never see the chain), with the dedup
presence-oracle noted as the honest residual and encryption-at-rest
left to deployment. §10 lists the shipped v2 pieces.

SP800-92 AU-12 row + residual-gap table, SP800-53 AU-12, and the NIST
control mappings updated: veltro provenance wired; CDS emitters follow
the CDS guard itself. Roadmap Tier-1 item 2 updated likewise.

New man pages: auditprov(2), auditget(1).

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mnt/ ships .gitkeep placeholders but n/ was never tracked, so a fresh
checkout has no /n and the profile's 'mount -ac {mntgen} /n' fails
(silently — stderr is discarded). Surfaced by the INFR-355 worktree
smoke, which is exactly a fresh checkout.

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pdfinn

pdfinn commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Live end-to-end evidence (headless emu, llmsrv over local Ollama gpt-oss:20b, real ventisrv + auditfs). The chain after one agent run — every payload by reference, scores truncated for width:

1 1787165769 auditfs start 4897ebba21475537c0328f41dc7441a1357fd0ce28332ff34b7714d8915572a7 host=Athenubis-3-.local replay=ok
2 1787165772 veltro nsrestrict d13b1fd1d8aa72665921010ea36f9873cd032b6d844366b03dd1b3fab06c7808 id=45 sha256=715de8ff69f6db8f9b1c987cf1d051d234193bc31bcd069d92e02b33
3 1787165774 veltro agentstart 47dd4ebdd699cd1bd67410b794faa100fb49ff675cff902cb25d882b9e57683c agent=86c31bdf31c60bc88a381aa31bbf7f56 session=9997108108-116104101-1
4 1787165774 veltro nscaps 7ce4b26ef71a9a476d2f58f24df133d82b98da428e2ad134936cd44a2bee36a2 agent=86c31bdf31c60bc88a381aa31bbf7f56 content=08004a2e20c261c4158296567d
5 1787165775 veltro sysprompt f75873ae854726ada870881ba62e0f0e724fc555a51b7b78a7ecf8785c24b381 agent=86c31bdf31c60bc88a381aa31bbf7f56 content=c3f791127bf91a0932cd25c
6 1787165775 veltro task 133bcd6d82598aecad4d4f85ce48d0d53bcaebc86b6fe4463abdfe31876713cf agent=86c31bdf31c60bc88a381aa31bbf7f56 content=5aa5715f3bbc98d5a951653b530d
7 1787165793 veltro plan 303986979ff8b585a5129928629327ee9044ae6326a08e64b6e720542013e439 agent=86c31bdf31c60bc88a381aa31bbf7f56 content=f1bf9f864054910e0b2786b897ab
8 1787165793 veltro prompt d811d4f19c0a70c7a00276c14cc641f658d3ea12a11628e14e13265726ea738d agent=86c31bdf31c60bc88a381aa31bbf7f56 step=0 content=9b7b5aa255ead814a38
9 1787165796 veltro llm 225fae689500eebad08ab5208addedbadbd4145cdc492a619c72e0c00d0ffc2a agent=86c31bdf31c60bc88a381aa31bbf7f56 step=0 content=c9937ca18a6b3ee78dd8da
10 1787165796 veltro toolcall 951779c1157888f33597e324851d0cf4aced517349c22b689bcc611ef2df17eb agent=86c31bdf31c60bc88a381aa31bbf7f56 step=1 tool=list content=eb6fcf
11 1787165796 veltro toolres 3c8eb72b8203c42909781759b1abb4d4aa174b4d31104241e00ded924b676c61 agent=86c31bdf31c60bc88a381aa31bbf7f56 step=1 tool=list content=1b435f6
12 1787165797 veltro llm c3d3e3030fb93c033c5f1e198f091224218be5e873cff01d5310cbdb10fdec99 agent=86c31bdf31c60bc88a381aa31bbf7f56 step=1 content=f2723bf8b44131233fc73
13 1787165797 veltro agentdone bbd5de25964b25f6e0156cb99a4a6beec7becf83c39a9617893f31d1245a856b agent=86c31bdf31c60bc88a381aa31bbf7f56 steps=1 stop=end_turn content=

Offline verification over that chain: auditverify reports ok: 13 records. The veltro task payload fetched back by its record score with auditget -s <sha256> <score> returns the exact task text.

…ventisrv

The 64-bit port left all three casel consumers walking the historical
32-bit table shape. limbo's binary emitter (dis.c Tcasel) lays the
table out as: count in a 2*IBY2WD slot, then per case [lo][hi][pc+pad]
with IBY2LG bounds and the pc padded to 2*IBY2WD, then the default pc.
With IBY2WD == IBY2LG == 8 that is a 4-WORD stride with the pc at
l[2]; the code still assumed the 32-bit shape — 6 four-byte words per
case, pc at l[4]:

- xec.c OP(casel): read garbage bounds/destinations — the interpreter
  looped forever or jumped wild on the first `case big` executed.
- comp-arm64.c / comp-amd64.c comcasel: patched the wrong slots at
  compile time, corrupting module data and indexing patch[] with
  garbage — an immediate SEGV in comp when loading any module
  containing `case big` under -c1.

Nothing in the entire dis tree used `case big` until INFR-355 salvaged
ventisrv(8), whose Dhdrmagic/Fhdrmagic dispatch is a case on big — so
the instruction had been broken since the 64-bit migration with no
symptom. It presented as ventisrv wedging (interp) or crashing emu
(JIT) the moment it restarted on a populated store.

Fix all three to the stride-4 layout. tests/casel_test.b keeps the
instruction exercised (single labels, boundary probes, a 10k-iteration
dispatch loop); 3/3 pass under both -c0 and -c1, and ventisrv now
restarts cleanly on an existing data/index pair.

The amd64 comcasel had the identical bug — worth noting for the
x86-64 emu-corruption flake investigation (GoDis de-flake): any
module with a casel would have corrupted its own module data at JIT
load on those hosts too.

Refs: INFR-355

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pdfinn

pdfinn commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Bonus find while validating store persistence: restarting ventisrv on a populated index deterministically wedged the interpreter and crashed the JIT. Root cause was not ventisrv — it was the Dis casel instruction (case big), broken since the 64-bit migration in all three consumers (xec.c OP(casel), comp-arm64.c/comp-amd64.c comcasel): they still walked the 32-bit six-halfword table shape while the 64-bit limbo emits a 4-WORD stride (dis.c Tcasel: count in a 2*IBY2WD slot, [lo][hi][pc+pad] per case). Nothing in the entire dis tree used case big until ventisrv arrived, so the instruction was silently broken for the life of the port. Fixed in f43e0f74 with tests/casel_test.b (3/3 under both -c0 and -c1).

With the fix in, the full persistence loop verifies end-to-end on a freshly built emu: ventisrv restarts on the existing data/index pair (index replay), and auditget -s <sha256> <score> returns the exact veltro task payload the live agent run had sealed — byte-for-byte, pinned by the chain-sealed SHA-256.

Note for the x86-64 emu-corruption investigation: comp-amd64.c had the identical bug, and its comcasel corrupts module data at JIT load — worth keeping in mind for the GoDis flake follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant