[deferred] Activate the immutable-id identity model behind a catalog migration gate - #463
Conversation
…gate Decision 0015 splits one overloaded string into two typed namespaces, so selection needs a type rather than a precedence rule. - `AgentSelector` has exactly two forms. An exact-ID selector performs only catalog-global ID lookup and never falls through to address lookup, which is what stops an existing semantic ID from staying alive as a route after a rename. Equal bytes across the two namespaces do not collide. - Ordinary references resolve through R24's fail-closed candidate set instead of a precedence rule, because a dotted semantic address and a host-qualified bus address are the same shape: collecting both readings and requiring exactly one surviving subject makes the reference decidable without guessing which dot is the separator. Absence and ambiguity both refuse, and ambiguity names the surviving subjects. - The address book excludes retired subjects: retirement releases the address and makes the subject non-routable, so it neither resolves nor occupies the namespace. Suspended subjects stay. - `activation` is DELTA-003 step 5's gate. It answers `Activated` only when every live and structurally archived subject carries an explicit ID and no migration transaction is outstanding. A partially migrated catalog has no coherent ID namespace to key ownership, provenance, or task identity on, so the gate is all-or-nothing and legacy behavior stays normative until it opens.
…ble id R26 derives runtime ownership, default task IDs, and `ST_AGENT` from the immutable agent ID rather than the mutable address. Activation is decided once per reconcile pass and threaded as an explicit parameter, never per subject and never through a global. - `ST_AGENT` becomes the agent key: the raw immutable ID once the catalog is migrated, the legacy bus identity while it is not. `ST_SUPERVISOR` stays a verbatim pass-through, because catalog ID migration already rewrote every supervisor reference to the parent's migrated ID — re-resolving it would reintroduce address parsing on an exact selector. - Default task IDs follow R26: `<agent-id>` for the canonical compact agent task, `<agent-id>.<task-name>` for a long-form named task without an explicit id, and an authored id stays authoritative. Because migration freezes each live subject's ID to its former bus identity, every legacy task ID and session socket path is preserved by construction rather than by a compatibility path. - Managed PTYs receive the schema-2 owned snapshot: `agent.actor.id` plus `agent.actor.address` replace `agent.actor.path`, which is removed as an owned tag. `role=agent` and native `displayName` narrow to the canonical compact agent task and are cleared on every other PTY. Unrelated tags are preserved and clearing a value removes only its own tag. - An address change is a metadata projection: it patches tags and enters no launch, teardown, replacement, or flapping accounting. The legacy branch is byte-for-byte today's behavior, schema 1 included.
Ordinary message resolution was a precedence rule over bus identity and bare identity. It becomes the R24 address algorithm over an address book built from the same fenced snapshot, plus an explicit exact-ID path. The catalog-generation and transition fence and its retry are unchanged: that fence is exactly what keeps one lookup on one coherent before-or-after address book, which an atomic address cutover requires. - Version-2 Sent records carry immutable IDs in `from`/`to`, publication-time bus addresses as display-only snapshots, and explicit endpoint kinds; a principal or external endpoint keeps its canonical address with the matching kind. The writer emits version 2 only for a provably migrated catalog and otherwise keeps version 1's exact bytes, which the digest-named pending record requires. - Collision-aware attribution is wired into the version-1 read path from the migration's durable record: an untouched legacy endpoint is its own migrated ID, a reassigned one resolves only for the row's own state owner, and any other colliding endpoint stays unattributed — rendered as a historical address, with reply and automation authority refused rather than addressed to the live replacement. - Rendered frontmatter and the DING sender projection read address-for-humans with the ID as authority, and fall back cosmetically when a subject no longer resolves. - Stream and resync publication selects its recipient by exact ID once activated, so a subject created with a UUIDv7 is reachable; the legacy path and the persisted stream-record bytes are unchanged. The transition branch of the address book stays keyed on the legacy pair: mid-apply the declaration bytes are unreadable, and it locates a retained state directory rather than resolving a route.
…ership The `agent` field of both harness records means the immutable agent ID in version 2 and the bus identity in version 1; the shape is otherwise identical, so the version suffix is the whole read contract. - A producer emits version 2 only once its catalog is migrated. The decision is taken once per driver start from the catalog root each driver already holds — never per record write — and a driver that cannot decide falls back to legacy rather than guessing activated. - Write-side ownership becomes exact equality against the writer's OWN version, in both directions. A v1 writer must not replace a v2 record it cannot read (its `seq` decodes as serde-default zero, which every claim would exceed), and a v2 writer must not coalesce or restamp a v1 record: `agent` means different things under each version, and restating one namespace's bytes under the other's promise is not a merge. Read admission stays strictly wider than both. - Resync watch sets and seat IDs, and task-inventory agent and runtime-id columns, derive from the shared reconcile key and task-ID rules instead of local copies. For a migrated legacy subject every derived value is byte-identical, which the tests assert rather than assume. - Task inventory rows gain a nullable address without weakening completeness: no declared task is dropped, the duplicate-runtime-id refusal still fires, and a missing address never reads as a missing task.
`st2 agent address` is the third sibling of `st2 rename` and `st2 describe` and reuses their whole path: the exclusive authoring lock before discovery, exact selection by immutable agent ID, the caller-supplied `ST_AGENT` self/descendant guardrail over ID-keyed supervisor edges, the Nix-owned and non-KDL refusals, one span-bounded edit, and the source-recheck-then-atomic-rename publication. - The value is validated against R24's grammar, and host-local effective-address uniqueness is proved by validating the complete prospective catalog through the existing `dup-address` rule rather than a second implementation. `--clear` restores the positional identity fallback and is admitted only when that fallback is itself unique. - Direct `id` mutation is refused for address and presentation edits alike: there is no `id` writer anywhere in the tree. - Every agent-selecting command gains the mutually exclusive exact-ID form, and a command defaulting from `ST_AGENT` consumes it as an exact selector rather than re-resolving it as a route. - `unarchive` refuses an unmigrated archive once the live catalog is migrated, and validates ID uniqueness against the prospective live-and-archived set rather than the live catalog alone. No path in this tree synthesizes a new declaration, so UUIDv7 creation has no call site here: every new subject enters as authored KDL bound byte-exactly by a digest, and injecting an id would break that binding. The obligation belongs to the grammar gate and to the authoring generator, and is recorded in DELTA-003.
A catalog is either fully migrated to explicit agent IDs or not migrated at all. A mixture has no coherent ID namespace — `effective_id` answers with a frozen legacy bus identity for one subject and an authored ID for its neighbour — so ownership, provenance, and task identity would be keyed two ways in one pass while the activation gate kept every writer on legacy behavior. `agent-id-missing` is therefore what stops a new declaration from entering a migrated catalog without an `id`, including one published through a digest-bound path whose exact bytes cannot carry an injected one. Only whole-catalog ID migration holds a mixed state, inside its own transaction: it proves an all-legacy plane before its writes and an all-migrated plane after them, and a resume skips the pre-check precisely because what it observes is the half-migrated state this rule refuses.
…TA-003 The target contract is implemented behind one catalog-scoped activation gate, so the VRS stops describing it as unimplemented and starts describing the gate. - Root spec, agent-spec F02, harness-state, and harness-context notes now say what ships: readers accept both record versions, producers emit the new version once their catalog is migrated, and write-side ownership stays exact own-version equality. - DELTA-003 stays open and records progress against its own five rollout steps. Two things keep it open: the canonical Agent Spec grammar still makes `id` optional, and no admitted host is migrated — the model is proved on fixtures and on a scratch copy of a real 722-declaration plane, not on the live fleet, and no downstream generator emits `id`. - The invariant rows DELTA-003 names move with the behavior they pin: runner-owned task identity to the raw ID and schema-2 actor tags, R23's inventory to ID-keyed ownership with a nullable address, archival to frozen archived IDs and safe unarchive, and unbindable session sockets to the new default task-ID shape with legacy socket continuity preserved.
|
@codex review Posted on behalf of @schickling
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Superseded in part, deferred in part. The branch stays. What shipped out of this PR, as #467 stacked on #456:
What is deferred ( Why the version bumps did not ship. The premise at DELTA-003:42-46 — "strict version-1 readers reject the new fields" — is false for every record but the sender ledger. Before the deferred half lands, two defects recorded in the amendment and in the narrowed DELTA-003: Worth keeping from this branch when it returns, beyond the identity model: the activation-gate consolidation (it is currently re-derived in 8 places with four separately-tested fail-closed policies) and the Posted on behalf of @schickling
|
Third and last of the DELTA-003 stack. Stacked on #457, which is stacked on #456. Review those first;
this branch's base is #457's head.
Problem
#456 made an explicit immutable
idand an optional mutableaddressexpressible and taught everyreader the target shapes. #457 assigns them:
st2 catalog migrate-idsfreezes every live andstructurally archived subject's ID in one transaction and rewrites supervisor references to IDs.
Nothing yet uses them.
ST_AGENT, default task IDs, PTY presentation tags, message provenance,harness records, stream and resync ownership, and every ordinary agent reference still key on the
positional
identityplus the current host — the exact overloadingdecision 0015 exists to undo.
This PR is step 5 of DELTA-003's rollout: "activate UUIDv7 creation, mutable-address routing,
raw-ID
ST_AGENT, ID-keyed runtime ownership, message version 2, harness-state and harness-contextversion 2, and PTY schema 2 together."
The gate
DELTA-003 also says, in the same section: "until step 5 completes, existing identity resolution and
every current invariant remain normative implementation behavior." So activation is not a flag —
it is a property of the catalog, computed in
src/identity.rs:It is all-or-nothing rather than per-subject because a mixed catalog has no coherent ID namespace:
effective_idwould answer with a frozen legacy bus identity for one subject and an authored ID forits neighbour, keying ownership and provenance two ways in one pass. The last commit makes that state
inadmissible outright (
agent-id-missing), so the only holder of a mixed catalog is the migrationtransaction itself, inside its own fence.
Every writer here consults activation once per pass or command — from the discovery the caller
already performed, via
activation_from, never rediscovering and never per subject — and threads itas an explicit parameter rather than a global.
Change
Selection (
src/identity.rs) —AgentSelectorhas two forms. An exact-ID selector performsonly catalog-global ID lookup and never falls through to address lookup; that is what stops an
existing semantic ID from staying alive as a route after a rename. Ordinary references run R24's
fail-closed candidate set rather than a precedence rule, because a dotted semantic address and a
host-qualified bus address are the same shape —
dev4.notesis both a legal bare address and a legal<host>.<address>split. Both readings are collected, deduplicated by agent ID, and exactly onemust survive; absence and ambiguity refuse with address-specific diagnostics. A pinned host admits
only its own qualified split, so a reference cannot reach another host by spelling it. The address
book excludes retired subjects: retirement releases the address, so it neither resolves nor occupies
the namespace, while suspension keeps both.
Runtime identity —
ST_AGENTbecomes the agent key. Default task IDs follow R26 (<agent-id>for the canonical compact agent task,
<agent-id>.<task-name>otherwise, authored idsauthoritative). Managed PTYs receive the schema-2 owned snapshot:
agent.actor.idandagent.actor.addressreplaceagent.actor.path, which is removed as an owned tag;role=agentandnative
displayNamenarrow to the canonical agent task and are cleared on every other PTY.ST_SUPERVISORstays a verbatim pass-through — migration already rewrote every supervisor referenceto the parent's migrated ID, so re-resolving it would reintroduce address parsing on an exact
selector. Because migration freezes each live subject's ID to its former bus identity, every legacy
task ID and session socket path is preserved by construction, not by a compatibility path; the
tests assert that equality with literals rather than deriving it through the helper under test.
Messages — ordinary recipient resolution moves to the address algorithm plus an exact-ID path,
keeping the existing catalog-generation and transition fence and its retry: that fence is precisely
what holds one lookup on one coherent before-or-after address book, which an atomic address cutover
requires. Version-2 Sent records carry immutable IDs in
from/to, publication-time bus addressesas display-only snapshots, and explicit endpoint kinds. Collision-aware attribution is wired in from
#457's durable record: an untouched legacy endpoint is its own migrated ID; a reassigned one resolves
only for the row's own state owner; any other colliding endpoint stays unattributed — rendered as
a historical address, with reply and automation authority refused rather than addressed to the live
replacement (
MESSAGE-R04). Stream and resync publication selects its recipient by exact ID onceactivated, so a subject created with a UUIDv7 is reachable at all.
Harness records — both
agentfields mean the immutable ID in version 2 and the bus identity inversion 1. Producers decide once per driver start from the catalog root each driver already holds,
and a driver that cannot decide falls back to legacy rather than guessing activated. Write-side
ownership becomes exact equality against the writer's own version, in both directions: a v1
writer must not replace a v2 record it cannot read (its
seqdecodes as serde-default zero, whichevery claim would exceed), and a v2 writer must not coalesce or restamp a v1 record, because
agentmeans different things under each version and restating one namespace's bytes under the other's
promise is not a merge. Read admission stays strictly wider than both.
Authoring —
st2 agent address <value>|--clearis the third sibling ofst2 renameandst2 describeand reuses their whole path: exclusive lock before discovery, exact selection byimmutable ID, the
ST_AGENTself/descendant guardrail over ID-keyed supervisor edges, Nix-owned andnon-KDL refusals, one span-bounded edit, source recheck then atomic rename. Host-local uniqueness is
proved by validating the complete prospective catalog through the existing
dup-addressrule ratherthan a second implementation, and
--clearis admitted only when the restored identity fallback isitself unique. Direct
idmutation is refused for address and presentation edits alike — there is noidwriter anywhere in the tree. Every agent-selecting command gains the mutually exclusive exact-IDform.
unarchiverefuses an unmigrated archive once the live catalog is migrated and validates IDuniqueness against the prospective live-and-archived set.
Gates and evidence
nix develop -c cargo test --no-fail-fast, and the decisive comparison:comm -13(regressions)comm -23(silently fixed)The two sets are byte-identical, so this PR introduces no failure and hides none. Green counts on
this branch:
st2lib 762 passed, 1 ignored;agent-spec17 + 75 + 6; and every otherintegration suite green except the standing red lane below.
Pre-existing red, each proved on a clean worktree at an ancestor commit and none of it touched here:
tests/agent_publish.rsmain0d22352and stack baseb30bc54tests/catalog_apply.rsmain0d22352tests/catalog_diff.rsmain0d22352— introduced by #399 tightening admission under fixtures authored before ittests/eval_up.rsorigin/mainb30bc54—tests/eval_up.rsstill expects baret.awhile #180 made spec-team task IDs host-qualified in 2026-08tests/eval_run_e2e.rsorigin/mainb30bc54— a strict-validation root-count refusal and a live judge timeoutinvariants,materialize,native_only,nomad_survival,stream_authoring_cli,targeted_reconcile,task_inventory_cli,transport_isolationOne of those deserves a call-out because it touches this PR's own bookkeeping:
invariants::qualified_proof_references_resolvefails on the first unresolvable reference it finds,tests/resource_profile_supervisor_e2e.rs::demand_observation_settlement_matrix_is_atomic_and_preserves_facts,which predates this stack. That means it cannot vouch for the rows this PR moved, so every proof
reference added to
INVARIANTS.mdhere was checked directly against a declaredfn— all 36 resolve.New proof
Roughly 40 new tests. The load-bearing ones:
closed; one address on two hosts needing a host; a pinned host refusing another host's split; one
subject reached twice not being ambiguous with itself; exact-ID selection resolving a subject whose
address bytes belong to a different subject, and refusing an address as an ID.
resync watch-set and seat-id continuity across migration; an in-memory spec team with no ids
gating Legacy and lowering to literally its pre-PR task IDs.
replacement; a healthy task keeping its PID, creation identity, and generation.
identity; the version gate refusing ownership in both directions with everything else equal
(same seat, same session token, same claimed sequence) so the version is the only deciding factor;
a version-1 send still emitting version 1's exact field set and digest.
at the other endpoint, and reply authority refused for the unattributed one.
same address on another host admitted; invalid grammar, Nix-owned, TOML, ambiguous target, and
idmutation all refused; the self/descendant guardrail admitting a descendant and refusing astranger;
--clearrefused when the fallback would collide;unarchiverefusing an unmigratedarchive once the catalog is migrated.
agent-id-missing; both uniform statesadmitted; activation requiring both planes and a completed transaction.
Intentionally left
existing host
identity::activationanswersLegacy, and every writer's legacy branch isbyte-for-byte today's behavior. That is the property that makes this PR safe to land ahead of the
live migration rather than after it.
every new subject enters as operator- or generator-authored canonical KDL bound byte-exactly by a
digest (
st2 agent publish --input-sha256,st2 catalog bootstrap|apply), and injecting anidwould break that binding. The obligation belongs to the grammar gate — the canonical Agent Spec
requiring
id— and to whichever generator authors the declaration. The generator exists and isproved; DELTA-003 records the gap and the reasoning rather than leaving a dead branch here.
idis still optional in the grammar. Making it required is a canonical-Agent-Spec change incompoundingtech/evalsplus its proof corpus, not an st2 decision.change that must precede it.
emitting the model, and the model-free proof corpus passing against an immutable st2 artifact.
This PR records progress against each of the five rollout steps instead of claiming closure.
Stack created with GitHub Stacks CLI • Give Feedback 💬
Posted on behalf of @schickling
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile