docs/adr/0216-projects-own-agent-roles-and-tool-policy.md

ADR 0216: Projects own agent roles and tool policy

Status: accepted for engineering (2026-10-03; VUH-1535 root source review). Fleet tool gating is superseded by ADR 0217 (2026-10-04); the membership/tool-cutover text below preserves the historical decision. Project roles, caps, hiring and tracker binding remain current. Deterministic foundation evidence covers schema, migration and compatibility; live project hiring and grant enforcement remain the subsequent issue boundaries below.

Supersedes the identity-role storage decision in ADR 0208. Preserves the repository tracker contract in ADR 0191. Implements the foundation of VUH-1535 within Projects; per-agent tool enforcement and fleet-grant retirement belong to VUH-1558.

Decision

Owner settings contain projects, approved workspaces, role definitions, character role associations, hiring rules, worker limits and tool-policy references. A repo may describe its tracker in .clankie/tracking.json; it cannot grant authority to itself. A project's tracker reference names one of its approved workspaces and that fixed relative path. It does not copy tracker credentials or turn tracker labels into authorization. Label-to-role mappings live in owner settings.

Each project can override the existing host fleet.size and fleet.models defaults. A role can select a harness, model, effort, concurrency cap and naming rule. Roles retain the existing built-ins and validated custom names; comparison is case-insensitive while custom display spelling is preserved. Worker caps and role concurrency caps are independent limits, with zero meaning no new hires. Unset values inherit host policy. An omitted or empty roles list inherits the six built-in roles (planner, designer, builder, tester, reviewer, researcher) without adding project launch overrides. A nonempty roles list defines the project's available roles. Hire admission, native assignment membership, persona associations and tracker role mappings use the same effective policy. VUH-1536 enforces configured role harness, model and effort at the native launch. VUH-1596 extends roles with native-subagent model/effort, delegation, account and placement. Explicit per-hire owner choices now win over role preferences, then fleet.hire defaults. Friendly model names resolve against the model registry before launch. Native-first admission journals a stable project/deliverable key and refuses another pane while that hire is live, starting or uncertain; subagent instructions use the same native first brief. The hiring conversation's verified native assignment or canonical effective workspace pins its project; otherwise the canonical destination workspace selects it. An optional requested project must match that context. Overlapping approvals and cross-project source/destination choices refuse rather than choose a cap bucket. Remote paths cannot be proven by the local filesystem and require a verified source project; unproven native process identity grants no project tools.

The controller journals cap allocations before asynchronous startup, atomically across admission calls. Starting and uncertain hires count alongside live hires; settling a turn or marking a persona done never releases capacity. Close the pane to release its slot: a successful complete native inventory can confirm that allocated pane is gone. An exited harness in an open pane keeps its allocation until the pane closes. Missing or failed inventory retains capacity. Retry uses the original launch choice and receipt even when current settings change; it cannot allocate a replacement. A settings/project change is checked again immediately before native effects. An exact live session can be reused at capacity, but cannot change its recorded role settings or start a replacement if it disappears during resumption.

This journal is controller-owned, separate from semantic role settings. A project assignment requires the actual native session plus a freshly observed harness process PID/start time, shell PID/start time and Herdr socket/session binding. Missing or stale proof refuses membership without workspace fallback. The same journal serves cap accounting and the host assignment lookup; it never creates or copies grants.

A persona remains identity: name, appearance, avatar revision and stable binding. A project role association says what that character does in that project. The same character may have different roles in different projects. These associations are not evidence of a live hire, workspace occupancy or tool eligibility.

Membership and authority

The host first resolves the current agent occupant from its authenticated pane and existing link. A durable explicit project/role assignment recorded by the host when hiring that exact occupant wins over workspace inference. A stale, missing-project or invalid-role assignment refuses membership; it never falls back to a more permissive workspace match. A persona ID, model-selected project, imported settings object, session, or machine alone cannot establish membership.

For agents not started with a host assignment, the host obtains the actual current pane working directory and canonicalizes it on that machine. It compares that evidence with owner-approved canonical roots on that same machine. Descendants match on path-segment boundaries. Linked Git worktrees need their own approval; sharing a repository or Git common directory confers nothing. All matching projects are considered: two matches, even nested ones, are ambiguous and deny. There is no longest-prefix or first-project preference. Unknown/relative/uncanonical paths deny. Windows aliases and case uncertainty require fresh host canonicalization; our conservative selector never guesses a case-insensitive alias. The pure selector is policy calculation, not a capability or proof that its input came from a pane.

Existing session-wide fleet grants are not copied into project grants or workspace approvals. VUH-1558 must explicitly retire/reissue them through the owner, retaining verified account, server, tool/argument bounds, revocation and exact pane/link checks. Removing a grant, changing workspace or replacing the occupant must invalidate eligibility within its required refresh bound and before new effects. A project association never promotes a social conversation to machine authority. No bridge or grant execution changes are claimed by this foundation.

Lossless rollout

Legacy persona roles migrate to the reserved default project with no workspace approvals and no grants. The migration preserves every persona's validated role, including custom spelling, and leaves identity, avatar files and occupant bindings intact. An unrelated existing default project, changed legacy source or incompatible migration receipt stops migration with the original identity file retained.

The service serializes migration before persona operations. It writes owner settings through SettingsStore, syncs file and directory, and verifies the saved project section before removing roles from the identity file. A durable source receipt makes restart after settings commit but before identity cleanup idempotent. It does not overwrite a later owner assignment with the old identity role. Settings failure or uncertain verification leaves the source for reconciliation; no completed migration is reported. Corrupt identity or pending state fails closed.

Native hire adoption already has a synchronous final authority boundary. Preserve it. When adopting an observed native identity, write a bounded durable role intent in the owner settings directory, scoped to that identity store, then persist the identity. Flush that intent through serialized SettingsStore before claiming the role change completed. The journal has an exclusive process writer; live/reused PIDs are never stolen, and dead-process recovery rechecks the original lock. A pending role with no persisted identity remains unresolved. Applied operation IDs prevent crash replay from overwriting a subsequent owner edit. The journal is only a semantic association write, never a native-hire receipt or membership proof. A failed flush retains evidence; it must not cause the native hire to be retried. The canonical native result remains spawned with its exact started seat. Optional roleAssignment reports pending with the durable operation ID, or unsaved if recording the intent itself failed. Neither claims a completed role change. Fully persisted success is unchanged; role failure never becomes a generic hire failure.

The existing set_persona_role setter accepts optional projectId; omission updates the default-project association. Both forms require a current native seat and confirmed hire membership in that project, read through the same host FleetProjectMembership producer. Unknown, offline and other-project characters are refused. Settings generation, current authenticated authority and the exact host-bound persona/seat/occupant are checked synchronously before accepting a durable role intent; the journal retains the admitted operation's existing crash recovery. Native hire adoption records its actual selected project in that journal. The membership display reads the current canonical association for its bound persona, so a reassignment or clear does not resurrect the launch role held in the separate native hire ledger. Existing app persona.role remains a temporary host-derived read projection of that explicit default context, not an identity field on disk. New consumers pass a selected project to read the role; absent context does not choose an arbitrary association. A character assigned only in another project therefore has no default wire role. Revision-bearing project settings requests require the current project-section SHA revision; controllers validate the selected project/persona and owner authority under the existing SettingsStore final guard. No caller-supplied path becomes approved merely by schema parsing.

Delivery boundaries

VUH-1539 adds the bounded existing-project editor: a strict revision-bearing POST to /v1/operator/projects/update, CLI/console entry and app Settings. Only name, roles, worker cap and tracker binding can change through that route. Omission preserves a field; null clears optional cap/binding. Full resulting settings validation retains assignments and label-map invariants. The existing operator/Take Control authorization and SettingsStore final source/authority guard remain in force; this is not a cross-process compare-and-swap guarantee.

The work reader exposes project bindings as read-only virtual repos without writing its registry. It rechecks the current project, exact local machine, canonical directory and existing fixed convention around adapter I/O. Remote or unavailable bindings stay explicit. Existing account adapters and registered repos are unchanged. App save refreshes prioritize that project within the existing eight-repo bound, serialize pending reads and fence stale publication. No persistent selected-project layout or label-role-map interpretation is added.

This change provides node-free project contracts, owner settings validation, migration, legacy compatibility and a membership selector with deterministic tests. The new assignment request is a contract, not a newly exposed endpoint. Project Broader app selected-project views, onboarding and project-grant enforcement retain their separate issue boundaries. VUH-1536 adds deterministic per-hire project recording and cap execution; live acceptance remains unrun. Neither live owner settings nor real grants/hires are changed by this engineering verification. Existing fleet grants remain unchanged until VUH-1558 explicitly retires or replaces them; this release does not claim they are project-scoped.

VUH-1558 project-tool cutover

Fleet grants now remain readable/revocable but confer no tools. Reissue is an explicit owner action; no owner grant or settings migration is performed by the implementation. project add provides only the narrow local workspace registration prerequisite and requires broker-consistent owner authentication. Project editing is added by VUH-1539 above; onboarding remains separate.

The local service resolves actual native process and session identity before consulting the host hire ledger or canonical approved workspace. Settings and assignment changes are fenced again after OS reads. Each tool call rechecks the durable grant immediately before dispatch, and the MCP host retains its account and server-configuration checks. A service-owned private Codex server additionally needs its captured PID lifetime and immutably bound native thread plus an actual matching hire; it never gets a workspace fallback. Full details and explicit owner cutover commands are in worker access.

Native Codex/Claude/OpenCode and registered private Codex paths have deterministic coverage. Installed Pi rewrites its command title, so generic Node cannot prove the installed script: trusted Pi launch provenance remains engineering within VUH-1558. Remote process proof remains VUH-1563. Live owner cutover is unrun.

Owner World drops (VUH-1639)

Existing role, conversation and tracker writes accept an optional freeAgent precondition: original persona, native seat/occupant, project and, for help, exact teammate/pane. It never grants authority. The host refreshes native/project proof and requires confirmed idle, no assignment or active goal and observed zero running children again at the existing effect fences. Unreadable, truncated or otherwise incomplete addressed child observations refuse. This landing proves bounded local Claude/Codex reads; unsupported child observers remain refused. Names are tracker data, never native routing. Existing omission keeps explicit owner/API/CLI/TUI behavior unchanged.

The app waits four seconds with Undo before submitting once. Changed or hidden sources cancel before submission. Work assignment and the pickup note remain separate receipts; there is no inverse API, compensation or uncertain replay. The private app records the gesture decision in its ADR 0072. The shared co-w skill adds optional quick-action metadata without changing its CLI or workflow.

Display placement amendment (2026-10-12, VUH-2192)

The bounded fleet-membership display read also accepts an exact native lead-hire record. It retains the original hiring conversation and optional configured project through later adoption or reattachment. If that conversation has no configured project, its exact recorded deliverable may resolve through the verified tracker to one configured project binding. Ambiguous or missing bindings remain unknown. Tracker lookups coalesce for sixty seconds, retain at most 128 records and admit at most two simultaneous background reads.

Publication still rechecks native occupant/process proof, the settings revision, hire provenance and current persona role. This extends display attribution, not native assignments or tool authority. Paths alone never attribute a lead-hired worker. Each read stays limited to eight tuples; clients batch their full roster and preserve freshness independently for each batch.

The shared native observation pool still admits four operations. Remote fleets may hold at most two of those slots across all reads, including child cleanup, so a stalled SSH batch leaves capacity for local renewal. This does not cache proofs or relax publication checks.

Linear project UUID validation uses a direct project read. Collection searches use fifty-project pages because the nested project projection at 250 entries exceeds Linear's query-complexity limit, including a UUID-filtered collection.

Linear binding validation follows the selected verified MCP connection, including the existing remote OAuth connection and the registered API connection. Calling the API adapter directly can reject a connected host that has only the remote linear credential. Validation checks the connection binding before and after the read, rejects local tracker fallback, and compares the returned UUID (uuid on remote MCP, id on the API adapter) with the requested UUID.