# Defense of Scoped Delegation Tokens for Subagent DB Access ## Position Subagents need bounded, auditable write access to milestone/slice/task state. The current hard wall—zero DB access for delegated agents—is an overcorrection that breaks real parallelism workflows without providing meaningful safety guarantees. Scoped delegation tokens are the correct middle ground: they give subagents just enough authority to record their findings without exposing the full SF tool surface or the planning authority that belongs to the parent agent. --- ## 1. Why the Hard Wall Breaks Real Use Cases ### The Verification Evidence Problem Consider the canonical parallel verification pattern: ``` Parent dispatches 3 reviewer subagents in parallel: - Requirements Coverage reviewer → checks requirement_coverage completeness - Cross-Slice Integration reviewer → checks API/interface consistency - Acceptance Criteria reviewer → checks UAT coverage Each subagent runs verification commands and produces a verdict. ``` Today, **those verdicts cannot be written to the DB by the subagents**. The evidence table (`verification_evidence`) is write-protected. The parent must: 1. Wait for all three subagents to complete 2. Receive their output strings 3. Re-parse the output 4. Issue `record_verification_evidence` calls itself This destroys the parallelism benefit. The subagents ran concurrently but the parent is now a synchronous bottleneck that has to re-verify what the subagent already verified. More critically: if the parent's context window evicts the subagent output before it can be recorded, the verification evidence is **permanently lost**—not because the subagent failed, but because the recording channel was blocked. ### The Blocker Discovery Problem A scout subagent dispatched to explore a codebase dependency risk discovers that a slice is blocked by an upstream schema migration that hasn't happened yet. The parent needs this information to set `slice.status = "blocked"` and record the blocker in `slices` or `tasks.blocker_discovered`. With the hard wall, the scout returns a string: `"Slice S02 blocked: depends on users_v2 migration which doesn't exist yet"`. The parent must then: 1. Parse this string (natural language, unreliable) 2. Issue its own DB update The information existed in the subagent's context at the moment of discovery. The subagent has the correct identity (`milestone_id`, `slice_id`) in its prompt. Forcing the parent to re-interpose creates a brittle translation layer and makes the subagent's finding second-hand rather than authoritative. ### The Async/Hanging Problem With the hard wall, the parent must remain alive for the entire duration of any subagent dispatch to receive and record findings. If the parent process is interrupted (user cancels, context overflow, crash), subagent findings in flight are lost. The subagent did the work; the recording failed because of process lifecycle, not because of any safety check. Scoped tokens survive subagent process lifetime: the subagent writes to the DB directly, and the parent's only job is to synthesize the final outcome. A parent crash after subagent completion doesn't lose the subagent's recorded evidence. --- ## 2. How Scoped Tokens Work Concretely ### Token Anatomy A scoped delegation token is not a raw SQL connection or an admin API key. It is a **bounded operation grant** with four components: ``` Token { scope: { milestone_id?: string, // null = all accessible milestones slice_id?: string, // null = all slices in milestone task_id?: string, // null = all tasks in slice }, operations: [ "record_verification_evidence", // append-only evidence table "update_task_status", // set task status to: completed | blocked "append_milestone_evidence", // append-only audit trail "append_slice_evidence", // append-only audit trail ], parent_fingerprint: string, // HMAC of parent envelope for audit expires_at: ISO8601, // token TTL = subagent expected lifetime } ``` ### Issuance Tokens are issued by the parent at dispatch time, embedded in the subagent's environment (via the existing `SF_PARENT_*` inheritance mechanism): ``` SF_DELEGATION_TOKEN="scope=milestone:S01/slice:S02/task:*;ops=record_verification_evidence,update_task_status,append_slice_evidence;fingerprint=abc123;exp=2026-05-08T12:30:00Z" ``` The token is **not** a secret. It is a structured grant that any process can inspect. Enforcement is done server-side in `sf-db.js`: every write operation validates the token's scope and operations list before executing. ### Operation Validation (in sf-db.js) ```javascript // Before any write in a subagent context: function validateDelegationToken(token, operation, scope) { if (!token) return { ok: false, reason: "No delegation token" }; if (new Date(token.expires_at) < Date.now()) return { ok: false, reason: "Token expired" }; if (!token.operations.includes(operation)) return { ok: false, reason: `Operation ${operation} not in token grant` }; // Scope check: token.scope.milestone_id must match the target milestone return { ok: true }; } ``` ### Attack Surface vs. Current Model | Surface | Current (Hard Wall) | Scoped Tokens | |---|---|---| | Subagent can write to milestones table | ❌ No | ❌ No | | Subagent can write to slices table | ❌ No | ❌ No | | Subagent can write to tasks table | ❌ No | Only via explicit `update_task_status` grant | | Subagent can write verification_evidence | ❌ No | ✅ Via `record_verification_evidence` | | Subagent can read full DB | ❌ No | ❌ No (read path unchanged) | | Subagent can access SF tools (bash, edit, etc.) | ✅ Via inheritance envelope | ✅ Via inheritance envelope | | Subagent can bypass permission profile | ❌ Blocked by inheritance | ❌ Blocked by inheritance | | Subagent can use blocked providers | ❌ Blocked by inheritance | ❌ Blocked by inheritance | The attack surface is **strictly smaller than the parent's surface**. The subagent cannot do anything the parent couldn't do—but it can record its own findings directly. Compare to a system where subagents get full tool access: scoped tokens limit what can be written to the DB even if the subagent is compromised. ### What Happens Without a Token If a subagent process is somehow tricked into making a DB write without a valid token (e.g., a bug in the dispatch layer, or a man-in-the-middle on the IPC channel), the write is rejected. The rejection is logged with the parent fingerprint for audit. This is a better failure mode than the current hard wall, which fails **silently and completely**—the evidence is lost with no record that it was even attempted. --- ## 3. Response to "Subagents Shouldn't Mutate Project State" This objection conflates two distinct concepts: **authority** and **causality**. ### Authority Is Not the Issue No one is proposing that a subagent should be able to reprioritize milestones, change slice goals, or override task verification contracts. Scoped tokens do not grant planning authority. The parent retains full control over: - Milestone sequencing and status transitions - Slice goal changes or deletion - Task dependencies, blockers, and escalation decisions A subagent recording `verification_evidence` is not making a planning decision. It is recording a **factual observation**: "I ran command X on task T, it exited with code Y, my verdict is Z." The parent then synthesizes these observations into planning decisions. The subagent cannot set a milestone to complete—that requires the parent's judgment. ### Causality Is the Real Issue When a researcher subagent discovers a blocker, the discovery happened **inside the subagent's context**. Forcing the parent to re-discover or re-interpret the finding introduces a lossy translation step: 1. Subagent discovers: "Slice S02 blocked by missing `users_v2` table" 2. Parent receives: natural language string 3. Parent interprets: "this means I should set slice S02 status to blocked" 4. Parent writes: `UPDATE slices SET status = 'blocked' WHERE id = 'S02'` Step 3 is where information is lost or distorted. The subagent had precise context (the exact SQL error, the migration file that should exist, the timeline). The parent has a string summary. Scoped tokens let step 4 happen directly from step 1, preserving precision. ### The "Pure Worker" Model Is Internally Inconsistent The opposing view says subagents should return output to the parent for the parent to record. But consider: - A `record_verification_evidence` call **is** the subagent returning its output—just to the DB instead of to a string in the parent context. - The "pure worker" model works for compute tasks (subagent computes, parent uses result). It breaks for **observational tasks** (subagent observes, finding is the observation itself, not a computation on it). - Verification evidence is observational. The subagent's verdict **is** the record. Having the parent transcribe it is redundant and lossy. ### Safety of the Mutation Is What Matters, Not Its Existence The relevant question is not "can subagents mutate state" but "can they mutate state unsafely?" Scoped tokens answer the safety question affirmatively: - **Bounded scope**: the token constrains writes to specific (milestone, slice, task) tuples - **Operation whitelist**: only append-only and specific status transitions are allowed - **Append-only by default**: evidence tables are append-only (no UPDATE or DELETE) - **Parent fingerprint audit**: every write is tagged with who dispatched it and under what constraints - **TTL expiry**: tokens auto-expire so a stray subagent process can't write indefinitely --- ## 4. Minimum Surface Area The minimum viable surface for scoped tokens covers three operational patterns that currently force synchronous parent re-interposition: ### A. `record_verification_evidence` (Append-only) **Schema**: `verification_evidence(task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at)` **Grant**: `scope.milestone_id = "M01", scope.slice_id = "S01", scope.task_id = "T01", operations = ["record_verification_evidence"]` **Why minimum**: This is the highest-value, lowest-risk operation. It is strictly append-only (no UPDATE/DELETE). It records what the subagent already did. The parent cannot re-run the verification without re-executing the subagent's work. Evidence is the primary artifact of verification-phase subagents. **Constraints enforced**: - Only `INSERT` (no UPDATE or DELETE on evidence rows) - Exit code and verdict are bounded enums - Bounded by task identity in the grant ### B. `update_task_status` (Status-only, narrow) **Schema**: `tasks` table, `status` column only **Allowed transitions**: - `pending` → `completed` (subagent finished work) - `pending` → `blocked` (subagent discovered a blocker; sets `blocker_discovered = 1`) **Not allowed**: No direct transition to `pending`, no status flips on other tasks, no mutation of `title`, `goal`, `verification_type`, or any other column. **Why minimum**: A worker subagent that completes a task should be able to mark it complete. A scout subagent that finds a blocker should be able to record it. These are the two status changes that subagents legitimately produce. All other status transitions (re-opening, escalating, deferring) remain parent-only. **Constraints enforced**: - Only the `status` column writable (and `blocker_discovered` as a companion flag) - Only the specific task in the grant scope - No mutation of other columns ### C. `append_slice_evidence` / `append_milestone_evidence` (Append-only audit trail) **Schema**: `slice_evidence(milestone_id, slice_id, evidence_type, content, recorded_at)`, `milestone_evidence(milestone_id, evidence_type, content, recorded_at)` **Grant**: `scope.milestone_id = "M01", operations = ["append_slice_evidence", "append_milestone_evidence"]` **Why minimum**: These are the existing evidence tables (Tier 1.3 spec). They are append-only audit trails. A subagent that discovers relevant architectural context, a blocking assumption, or an unexpected constraint should be able to record it as an evidence entry without requiring the parent to parse and re-record it. **Constraints enforced**: - Only `INSERT` (no UPDATE or DELETE) - `evidence_type` is a bounded enum (`finding`, `blocker`, `note`, `decision_record`) - Content size bounded by DB column limits --- ## Summary | Property | Hard Wall | Scoped Tokens | |---|---|---| | Parallel verification evidence recording | ❌ Lost or re-verified | ✅ Direct append | | Scout blocker discovery | ❌ String parsing required | ✅ Direct status + evidence | | Parent crash resilience | ❌ Evidence lost in flight | ✅ Subagent writes survive | | Planning authority | Parent retains all | Parent retains all | | DB write exposure | Zero | Bounded to 3 operations | | Audit trail | Incomplete | Fingerprinted per token | | Migration path from current model | N/A | Additive, no existing code broken | The scoped token model is not a security risk amplification—it is a security risk **reclassification**. The current hard wall does not prevent evidence loss; it only makes it silent. Scoped tokens make evidence recording possible while keeping planning authority with the parent and exposing only bounded, auditable, append-mostly operations to subagents. The correct objection is not "subagents shouldn't mutate state" but "subagents shouldn't have unbounded mutation authority." Scoped tokens are the mechanism that draws the bound.