Repository navigation
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, theghtool fails:A worker spawned fresh after the resume works. So does the same worker before the resume.
Cause:
withToolsbinds lazily, from the object's recipe (core/schema/llm.go). The object is first loaded at its first tool call, inMCP.boundToolObject(core/llm_object_tools.go), using the agent loop's context.sendTo). That client has no host.core/modfunc.go), already evaluated the recipe through the host client.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
core: test restored workers reading host secretsaddsAgentRestoreSuite.TestRestoredWorkerReadsHostSecret. A tool object holding afile://secret is driven by a worker through a module call (theagent-hirerfixture'saskAgent, which has the same shape assendTo). The test fails on main once the worker is restored.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 ascore/workspace_context.go.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:
TestAgentRestoretests;TestAgentRuntimetests;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, orhost.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:
cmd://runs a command.githubToken: cmd://gh auth token). That keeps the commonghcase prompt-free.Later PRs in the stack improve the approval prompt itself. For example, it currently steals keyboard focus too easily.