DeepSeek Harness · Sessions & Loops

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.

Course goalBy the end you can explain three things: how DSH lets an Agent run dozens of turns in one session on its own; why permission to change the goal follows message provenance so the model can't extend its own life; and why the right layer against Prompt Injection is mechanism—prompts only persuade.
Interactive demo · Provenance-auth attack/defense bench

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.

Current Turn window
Messages after turn/start; provenance labels stamped by the host
Provenance auth · authority.ts
Checked one by one at tool execution; line numbers match real source
Pick a scenario and hit Play, or scroll here to auto-play Scenario A.
What Goal is

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.

Design idea 1 · Permission follows message provenance

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.

source.kind = 'user' Human message · host-stamped source.kind = 'goal' Auto-continue turn · with round number Everything else Model output · tool result · sub-Agent hasDirectHumanInput Root-agent check + scan Turn window True only if kind:'user' is found authority.ts L70-74 isMatchingGoalRound goalId · revision · round all align True only for the currently authorized round authority.ts L77-83 direct-human full power create · edit · pause · resume complete · blocked also ok goal-round may only close out complete · blocked edit the cap? no way
Teaching diagram:three message-provenance classes map to two permission tiers; line numbers match packages/goal/tool-goal/src/authority.ts.

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.

packages/goal/tool-goal/src/authority.tslines 101–108
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')
}
Source snapshot note:Based on the local deepseek-harness-master repo; verified against 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.

Auth reads the host's stamp—not one word of the model's rhetoric enters the verdict.

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.

Design idea 2 · Credentials bind to version—and expire

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.

Sibling project · ralph took another path

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:

Run a foreground Ralph loop around an immutable goal using a fresh agent each time. Every Round starts a brand-new child that cannot see the parent dialogue or prior child sessions; the shared workspace is long-term memory, and only bounded structured reports pass between Rounds.docs/tool-catalog.zh.md line 1190, excerpt

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.

Side-by-side · Claude Code has a task system—not this layer
Claude Code: thick lifecycle, auth elsewhere

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.

DSH: stop injection in the type system

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.

Classroom Exercise
01

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.

Takeaway:Goal permission follows message provenance—the host's stamp on event metadata, beyond the model's rhetoric. On auto-continue turns the model may only close out, not extend life; editing the cap always needs a human present. The right layer against Prompt Injection is mechanism; prompts only persuade.