Visitar URL original
core: restored workers can't read host secrets by vito · Pull Request #15163 · dagger/dagger · GitHub
Skip to content

core: restored workers can't read host secrets - #15163

Draft
vito wants to merge 3 commits into
mainfrom
fix-restored-worker-host-secrets
Draft

vito wants to merge 3 commits into
mainfrom
fix-restored-worker-host-secrets

Conversation

@vito

@vito vito commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Draft / hand-off. This is the bottom of a planned stack on approval prompts. Its current fix is a stopgap that the approval work should replace. The decisions and evidence are in a Context (importContext(from: "b8cf143e4168e4823588e99195c7a4df")).

The bug, on main

After a session resume (dagger agent -r), a restored staff worker can't read host secrets that its tool objects hold. For example, the gh tool fails:

secret env "TOKEN": failed to get session attachable caller for client "…": PermissionDenied desc = SDK client access to host session attachables is denied

A worker spawned fresh after the resume works. So does the same worker before the resume.

Cause:

  1. withTools binds lazily, from the object's recipe (core/schema/llm.go). The object is first loaded at its first tool call, in MCP.boundToolObject (core/llm_object_tools.go), using the agent loop's context.
  2. A worker's loop belongs to the module client of the call that drives it (staff's sendTo). That client has no host.
  3. A fresh worker's load is a cache hit: the chief, or composition (core/modfunc.go), already evaluated the recipe through the host client.
  4. In a restored session nothing has evaluated it yet. Query.secret(uri: "cmd://…") runs again in the module client and records that client as the secret's source (core/schema/secret.go), so every later read is denied (engine/server/session.go).

Commits

  • 7ba8843 core: test restored workers reading host secrets adds AgentRestoreSuite.TestRestoredWorkerReadsHostSecret. A tool object holding a file:// secret is driven by a worker through a module call (the agent-hirer fixture's askAgent, which has the same shape as sendTo). The test fails on main once the worker is restored.
  • f5356b1 core: route restored bindings to the restorer. A restored agent records the client metadata of its restore call. The bindings it was restored with then load under that metadata, which means the CLI, with its host. Only the metadata is swapped, not the client scope, the same pattern as core/workspace_context.go.
  • efcfc7a core: match restored bindings by ID identity. Bindings are now matched by the exact ID objects the restored conversation holds, rather than by digest, which closes a forged-digest route the code reviewer found. The reload path (core/agents_reload.go) gets the same routing.

Tests that pass:

  • the new test;
  • the 10 existing TestAgentRestore tests;
  • the related TestAgentRuntime tests;
  • core unit tests;
  • go vet.

Why this stays a draft

f5356b1/efcfc7a trust every recipe in a restored worker's bindings with the user's host. That includes ones a module built itself, such as secret("cmd://…"), which is host command execution, or host.directory. Before the resume, those calls ran in the module's host-less client. A restored chief already gets host access this way; workers didn't.

Direction agreed with the maintainer: gate host-secret access behind an approval flow modelled on the existing push approval, instead of widening trust silently. Proposed shape, not settled:

  • Where to gate: where a host-less client resolves a host-backed secret URI. It prompts the user, naming the requesting agent/module and showing the URI verbatim, because cmd:// runs a command.
  • Pre-approved: secret URIs the user already declared in their own workspace config (e.g. githubToken: cmd://gh auth token). That keeps the common gh case prompt-free.
  • Remembered: per (URI, requester) for the session.
  • No TTY: deny.
  • The restorer routing in this PR then becomes unnecessary, or goes through the gate itself.

Later PRs in the stack improve the approval prompt itself. For example, it currently steals keyboard focus too easily.

vito added 3 commits November 12, 2026 21:19
A worker bound to a tool object holding a host secret (file://) reads it
before a restore, and after one when the restoring client drives it. Driven
the way staff's sendTo drives it, from a module function's own host-less
client, the restored worker's tool fails: the lazily bound object is rebuilt
from its recipe in that client, re-minting Query.secret(uri:) where no host
can be read (PermissionDenied: SDK client access to host session attachables
is denied).

Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Bound tool objects load lazily from their recipes, in the client of
whichever request started the agent's loop. For a staff worker that is the
module client of the staff call that woke it, which has no host. In the
session that composed the binding the recipe was already evaluated where
its host inputs were read, so the load is a cache hit; after a restore
nothing has evaluated it, so host reads in the recipe such as
Query.secret(uri:) re-execute in the host-less client and mint a secret
nothing can read.

A restored agent now records the client that restored it, and its loop
loads the bindings it was restored with under that client's metadata. The
restorer already loads its own restored agents' bindings with its host, so
this extends that trust to the recipes it restored, and no further: only
client metadata is swapped, not the held ClientScope, as for a Workspace
owner's routing, and bindings gained after the restore load as before.

Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
An ID's digest is whatever its encoding claims, so matching restored
bindings by digest let an ID built elsewhere, e.g. bound through withTools
and reseed by a module holding the agent, claim a restored recipe's routing
over different calls. Match the restored conversation's own *call.ID values
instead; anything else loads in the loop's client as before.

Route the recompose path's lazy binding load too, so reloading a restored
agent's expertise loads its restored state the same way a tool call does.

Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant