This repository was archived by the owner on Aug 10, 2026. It is now read-only.
feat(agent): a #agent/thread carries capability directly — register its wants as name-keyed pending grants - #183
Merged
Merged
Conversation
…ts wants as name-keyed pending grants The threads-only model (#174–#178) retired #agent/definition; a thread IS the agent. But parseThreadSpec parsed a thread's `wants:` as INFORMATIONAL only — never registered as grants — a regression from the def model, whose retired #agent/definition DID register its own wants keyed by the agent name. Restore the parity: on instantiateThread, register the thread's parsed `wants:` as PENDING grants keyed by the AGENT NAME (`metadata.agent` = spec.name) + derive status from the hub, mirroring the def path's resolveStatusWithGrants + reconcile-GC (reconcileGrants(name, wants)). The injection side already unions over spec.name (resolveInjectedGrantsUnion), so an approved thread grant injects at spawn — this is a REGISTRATION-only change. Safety (mirrors the def discipline exactly): - Grants register as PENDING — the operator still approves (approval is the security boundary; not self-escalation). - Register + reconcile only run from a CONFIDENT live set: after a clean parse + setup, and only when the thread EXCLUSIVELY owns the name (the dedup gate returns false otherwise — a deduped/def-owned name never reconciles). A malformed `wants:` / setup failure returns before any grant traffic, so a transient error can never present a stale/empty set that nukes approved grants. - Keyed by the NAME, its own partition — roles (#agent/role, PATH-keyed via reconcileRole) are the separate composable/curated layer, untouched. resolveStatusWithGrants / reconcileLiveKeys / resolveDefStatus generalize from ParsedAgentDef to a GrantBearingSpec ({ name, wants, declaredConnections }) so both a def and a thread flow through one code path. Not covered (follow-up): a proper removed-thread grant GC on note DELETION needs a thread seen-set + confident removed-thread diff (mirroring pruneRemovedDefs); a deleted thread's approved grants persist until then (a cleanliness gap, never an escalation). Documented on reloadThread. Tests: thread wants register keyed by name (status pending → enabled on approval); wants-free thread stays enabled with no register PUT; removing a want prunes via reconcile; malformed wants → skipped, NEVER register/reconcile (the safety case); a def-deduped thread registers nothing; thread name-partition + role path-partition coexist; a thread's approved name-keyed want injects via resolveInjectedGrantsUnion([name]). rc.1 → rc.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HAzWsgiJy4ndsWSCWY5S
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
What
Restore the capability parity the retired
#agent/definitionhad: a#agent/threadnow registers its ownwants:as pending grants keyed by the agent name, so a thread carries capability directly.The threads-only model (#174–#178) made a thread be the agent, but
parseThreadSpecleft a thread'swants:informational only (agent-defs.ts"parsed for parity, not registered"; "threads register no grants — only roles do"). That was a regression: the retired#agent/definitiondid register its own wants keyed by the agent name. Only#agent/role(path-keyed) registered.Where the registration hooks in
AgentDefRegistry.instantiateThread— after a successful parse +setupAndRegister, it now:resolveStatusWithGrants({ name, wants, declaredConnections: [] })— registers each want as a pending grant keyed byparsed.name(=metadata.agent) + derives status (enablediff all approved, elsepending), exactly like the def load path.status/pending/connections(was hardcodedenabled/[]).reconcileLiveKeys(threadAgent)— the agent-grants: grant rows orphaned when an #agent/definition note is deleted #96 grant-GC, reconciling to the thread's currently-declared wants.The injection side was already correct:
resolveInjectedGrantsUnion(grants, [spec.name, ...roleKeys])unions overspec.name, so an approved thread grant injects at spawn. This is a registration-only change.resolveStatusWithGrants/reconcileLiveKeys/resolveDefStatusgeneralize fromParsedAgentDefto a smallGrantBearingSpecso a def and a thread share one path.Safety (mirrors the def discipline exactly)
falsefor a def-owned/other-source name before any grant traffic). A malformedwants:(parseThreadSpecthrowsAgentDefParseError) or a setup failure returns before register/reconcile, so a transient error can never present a stale/empty set that nukes approved grants.reconcileRole) are untouched. Injection dedups thread-own vs role wants withspec.namefirst (existing behavior, verified not re-implemented).Not covered (documented follow-up)
A proper removed-thread grant GC on note deletion needs a thread seen-set + confident removed-thread diff (mirroring
pruneRemovedDefs— the poll is the real deletion path since the vault has nodeletedtrigger). That is a separate, larger change; a deleted thread's approved grants persist until then — a cleanliness gap, never an escalation (grants are always operator-approved). Documented onreloadThread.Tests
New
agent-defs.test.tsblock + onegrants.test.tsinjection test:wants:register keyed by the agent name; statuspending; connections surfaced with hub grant idsenabledonce approvedenabledwith no register PUT (byte-identical to before)wants:→ skipped, never register/reconcile (approved grants not nuked)resolveInjectedGrantsUnion([name])Gates
bun test ./src/— 1450 pass, 0 fail (the daemon gate; theweb/ui/src/browser tests that fail under a loosesrc/glob are pre-existing + DOM-dependent, run viatest:spa)bun run typecheck— cleanbun run lint— exit 0 (2 pre-existingnoExplicitAnywarnings insrc/transports/vault.test.ts, not touched here)Version:
0.2.4-rc.1→0.2.4-rc.2. rc tag pushed on merge (not here).🤖 Generated with Claude Code
https://claude.ai/code/session_01T6HAzWsgiJy4ndsWSCWY5S