v0.24.0 — the attach stops travelling decrypted in stream state
Catalog credentials no longer reach the access log
VgiCallState carried plaintext_attach — the unwrapped attach (uuid(16) || catalog_bytes) — so each turn could shard storage on its UUID without re-opening the auth-scoped seal.
Two problems. It was redundant: the sealed envelope is already in the same object at init_call.bind_call.attach_opaque_data, so the call state held both the ciphertext and its plaintext. And serialized state does not stay sealed at rest — the access log records the decrypted state at DEBUG as a deliberate audit artifact, and on the init turn that state is call_state || cursor. A decrypted attach therefore base64s catalog credentials into the log, which is exactly what loggable_attach_options exists to prevent elsewhere. It also sat decrypted in the transport's call-state cache.
The comment defending it claimed the seal "can't be reopened on a later, possibly different-auth, produce/finalize turn". That isn't true, and is now a test: continuation turns are ordinary dispatches carrying the caller's identity, so a real multi-turn HTTP stream shows the identity present and the envelope re-opening on every turn. Re-opening also restores a per-turn identity check the cached copy had quietly skipped.
Making it free
Re-opening cost 42 µs/turn — ~13% of a turn, which is why the copy existed. The open is memoized on the worker, keyed by (envelope, caller identity):
| before | after | |
|---|---|---|
| warm unwrap | 35–42 µs | 1.19 µs |
| catalog stream | 42 µs/turn | 1.54 µs/turn |
| non-catalog stream | 0.13 µs | 0.14 µs (no envelope) |
The identity half of that key is load-bearing. An envelope's AAD is the only thing binding it to a principal, and a cache hit skips the AEAD — so keyed on the envelope alone (hashed or not), a stolen envelope becomes a bearer token for anyone. Keyed on both, a mismatched caller misses and takes the open, which fails as it should. Only successes are cached, so a rejection never becomes a later hit. Tests cover cross-principal and anonymous callers against a warm cache, non-poisoning, and the eviction bound.
The cache holds plaintext in memory, which is unavoidable — the worker cannot resolve a storage shard without it — and widens nothing that opening the envelope already did. The property protected is that it never reaches serialized state or logs.
Fixes
MetaWorkercould not open an attach. Re-opening on the rehydrate path calls the object the transport supplies as the implementation, and under a MetaWorker that is not aWorker— it proxies rather than subclasses. Every HTTP continuation of a catalog-backed stream failed. It now routes the way_resolve_functiondoes, to the sub-worker owning the catalog._partition_fields_cachedconflated schemas. It was keyed onpa.Schema, whose__eq__/__hash__ignore field metadata — the exact thing the function reads. An annotated schema and an unannotated one of the same names and types shared an entry, so a function with no partition annotations inherited a neighbour's fields and thepartition_valuescontract check passed where it had to raise. Now keyed through a wrapper that keeps the cheap metadata-blind hash and discriminates viaequals(check_metadata=True).
Both were caught by the integration suite while 2,132 unit tests passed.
Also
- Requires
vgi-rpc >= 0.33.0. - The haybarn engine extra moves to
1.5.5rc1, matching the build the vgi extension is compiled against — in-process filter evaluation and the extension now agree.
Integration green on every transport: subprocess 10,334 assertions, HTTP 10,020, and all four CI lanes (stdio, http, shm, launch).