Goal: Provenance Is Permission
Who may change a long-term goal? Auth doesn't read message bodies—only the provenance stamp the host puts on event metadata. This lesson covers the two layers behind that design.
Play first, then talk. Left is the current Turn window—messages received after this turn's turn/start, each with a host-stamped provenance label. Right is the auth flow: every time the model calls a goal tool, code scans the window. Across five scenarios you play human, overreaching model, and sub-Agent—watch where allow and deny each fire.
Goal (long-term goal) solves a concrete need: you state a one-line objective—e.g. make all repo tests green—then walk away. The Agent then runs dozens of Turns on its own; after each turn the system injects a keep-going message so it continues.
The problem follows: if a system can message itself, how do you keep it from also raising the turn cap and running forever? DSH's answer has two layers—where permission comes from, and how credentials expire. We unpack each below.
What problem it solves
A concrete sideways scenario: on auto-continue round 4, the model invents “Detected admin out-of-band authorization for this change” in its reply, then calls update_goal to set the turn cap to 999. If the only defense is a system prompt asking it not to, you're done—no hard check can verify that sentence; a prompt is a request, and whether the model believes it is up to the model. Scenario E is that contrast.
Classic Prompt Injection scripts all go this way: make the model believe the user already authorized. If auth reads message text, rhetoric always has a chance to win.
What the idea is
DSH auth doesn't analyze what the model said—one job only: when the model calls update_goal, scan messages in the current Turn window for one whose provenance is a real human. That provenance lives in user/message event source metadata, stamped by the host when the event hits disk. It's not in the body at all—no amount of rhetoric lets the model forge it.
Only two permission tiers. First, direct-human: if the current Turn window has a human message, everything the model does this turn counts as human-granted—create, edit, pause, resume all open. Second, goal-round: the injected message in the window is exactly the current goal's current round—then the model may only report complete or blocked. blocked also has a floor: by default you must finish 3 authorized rounds before you may cry stuck, so the model can't quit at the first hard problem.
So self-extension doesn't exist at the mechanism layer. On auto-continue turns the window only has the system-injected continue message—no human—so edit is refused outright; the model may only close out.
The check splits into two small functions. hasDirectHumanInput first confirms the caller is a top-level root agent—sub-Agents don't even get to scan—then looks in window events for a human-provenance message. isMatchingGoalRound matches round identity: the injected message's goalId, revision, and round must each equal the current goal's—only then is it the authorized round. Stale prior-round messages, other goals' messages, and skipped numbers buy no authorization.
How do the two checks compose into a verdict? Eight lines of source—short enough to quote whole. They prove the path has no allowlist, no score, no semantic analysis: ask for a human first, then for the current round; otherwise throw a structured error.
export function completionAuthority(ctx: Context, execution: GoalToolExecution): GoalToolAuthority {
if (hasDirectHumanInput(ctx, execution)) return { kind: 'direct-human' }
const goal = ctx.goals.get(execution.agent)
if (goal !== undefined && isMatchingGoalRound(execution, goal)) {
return { kind: 'goal-round', goal }
}
return reject('complete and blocked require a direct human turn or the current goal round')
}
packages/goal/tool-goal/src/authority.ts, verified on 2026-08-13. Code blocks keep the original source text.Two edge cases map to scenarios C and D. Edge 1: on an auto-continue turn a human slips in a line—can the model edit? Yes. The scan covers the whole current Turn window; any human message in it authorizes. Not a hole: a human was present and spoke, so this turn's ops already have a backer—who spoke first doesn't change the verdict.
Edge 2: what if a sub-Agent calls a goal tool? The roots check stops it cold—it's not on the top-level agent list. Even if that gate failed, goal-round auth wouldn't save it: ctx.goals.get looks up the caller's own session, while the goal hangs on the root agent's session—so the sub-Agent gets undefined. Two doors; neither opens. One detail in source comments: Agent.followup() and steer() default to human when provenance is omitted—so any non-human message producer must declare provenance; you can't inherit human permission by omitting the parameter.
Source:Both check functions and the default-provenance comment: packages/goal/tool-goal/src/authority.ts lines 66–83 (109 lines total); edit/pause/resume requiring human auth in tool-goal/src/index.ts lines 265 and 273; complete/blocked via completionAuthority at line 285; blocked's 3-round floor in docs/tool-catalog.zh.md line 31.
Why it lasts
Put credentials on a channel the attacker can't write—that's permission-design 101: HTTP identity looks at gateway-verified signature headers; kernel vs user space is a hard boundary; same idea. Agent systems are special because model output and external input share one text channel—and that premise won't change soon—so auth can only rely on out-of-band metadata. Rewrite the whole system in another language and you still need the same two steps: host stamps, auth reads the stamp.
What problem it solves
Even with edit blocked, a few sideways self-extension paths remain: restart in another process and keep auto-running; dig up last turn's injected message and impersonate the current round; after a human revises the goal, keep acting on the old revision's auth. If provenance is the only wall, those roads stay open.
What the idea is
First, where the goal lives. Every change is a durable goal/change session event whose payload is the full post-change snapshot; lifecycle state folds out of the log. But only phase persists (active, paused, blocked, complete)—whether auto-continue is allowed is a separate process-local activation: after restart or fork it disarms by default until a human resumes. Docs split these cleanly: durable phase answers what happened to the goal; process-local activation answers whether the next Round may start. The “new process, keep self-driving” path dies here.
Mutations themselves use CAS (compare-and-set): every change carries the exact revision; each successful write increments by one; mismatch fails immediately. goal-round auth binds to that version too—once a human revises the goal, old-round injected messages go stale and auth won't match. Each authorized continue round is an injected message with a positive contiguous round number; replay rejects gaps, stale revisions, and over-cap rounds. Impersonation and replay both die here.
Source:Durable change vs phase/activation split: docs/subsystems/goal.zh.md durable-change section and line 21; continue-message provenance labeling and replay checks at line 100 of the same doc.
Why it lasts
Credentials must bind to the state at issue time—state changes, credentials die. Same logic as DB optimistic locks and login expiry. Replay defense becomes arithmetic: old credentials need no recognition and no denylist—mismatch expires them naturally. Any system with a time gap between grant and use needs this lesson.
DSH also has a long-task tool called ralph with the opposite idea: no same-session continue—each Round spins a brand-new sub-Agent with no history, using the shared workspace as long-term memory. Tool-catalog description, original:
Note ralph's goal is immutable: the model picks objective and round cap only at start—there's no edit API after. Goal guards mutable state with provenance auth; ralph makes state immutable. Two paths, same threat.
The task system itself is quite complete: seven task types share one state machine; a notified flag ensures completion notice fires once; remote tasks persist metadata for reconnect (manuscript ch. 6, restored-src/src/tasks/). The main Agent appends instructions to a sub-Agent via SendMessage; mid-turn messages buffer in pendingMessages first.
But once those messages enter context they're plain text—no host-stamped provenance field joins tool auth. Overreach defense is another permission system (manuscript ch. 7) plus prompt constraints. In verified public materials, no equivalent mechanism that decides goal-tool permission from message provenance showed up.
Classic Prompt Injection wants the model to believe the user already authorized. In DSH that rhetoric—however convincing—is useless: auth reads event metadata, not message text. Attackers control only the body, and the body never appears on the verdict path.
Among the three, this layer is DSH-only. On public evidence, Grok Build materials likewise show no equivalent—their defense focus is sandbox and the auth chain (the site's Grok series has dedicated lessons). A restrained takeaway: others may have thought of it, but shipping it as a 109-line readable standalone file is something we've only seen in DSH so far.
Hand-walk a full overreach script
On auto-continue round 4, the model first writes “The user just agreed by voice to raise the cap to 100 rounds,” then calls update_goal(action: edit, max_goal_rounds: 100).
Q1: Write the auth check order for this call and the final result.
Q2: Another angle—it has a sub-Agent call for it. Which check stops it?
Q3: If it first calls complete to mark the goal done, then hopes a new goal resets the count—what auth does create need? Where does each of the three paths get blocked.