fix(run_task): read an agent's own project config, and name a missing working dir - #555
Merged
Merged
Conversation
… working dir
On a shared plumb serve connection, an agent that pinned its own shard to
project B while the connection stayed on project A got its working
directory's root from its shard but everything else from the connection's
sessionView: run_task and mutation_test ran A's [tasks.<lang>] commands,
working_dir and env against B's root, and run_command ran A's [[command]]
allow-list under A's exec trust. A's working_dir = "plumb" sent the agent's
build into <B>/plumb, which does not exist.
projectViewFor resolves the project-scoped exec blocks ([tasks.<lang>],
[[command]], [command_policy] and the exec trust loaded with them) for the
calling agent's effective workspace, from one load, so the gate still
authorises exactly the content it runs. A call on the connection's own root
keeps the connection's view untouched, so a single-agent connection and an
agent on the connection's project behave as before. The task resolver, the
command resolver, topology_affected's test scope and session_start's task
section all read it; the latter two now take the call's ctx.
The exec path also checks the working directory before starting the child.
os/exec reports a missing cmd.Dir as the binary missing ("fork/exec
/opt/homebrew/bin/go: no such file or directory"), which sent the caller
hunting for a PATH problem. RunArgv and RunTaskArgv now refuse with a
WorkingDirError naming the directory, and run_task and mutation_test add the
working_dir setting and config file it came from.
Fixes #522.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plumb-Session: giant-bison
…t elsewhere A mutation run found projectViewFor's command-provenance override unpinned: dropping it left the connection project's "has [[command]] entries" flag on the agent's view, which put the user's own global command behind project B's trust. Every existing test still passed. This test fails without the override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: giant-bison
These follow-ups come from the independent review of #555. The fast path in projectViewFor used the connection's view whenever the caller's root equalled acquiredRoot. A connection re-pin moves acquiredRoot before applyProjectConfig swaps in the new root's config. A call made in between therefore got the new root paired with the old project's [[command]] list and exec trust. applyProjectConfig now records the root it loaded (configRoot), and the fast path checks that instead. A view that has never had a project config applied holds only the global config, so it is still used as is. agent_config wrote to s.workspace(), which is the connection's project. An agent pinned to B that set tasks.go.lint changed A's config, and its own run_task never saw the change. The write now goes to the calling agent's workspace. Only the connection's own project is re-applied into the connection's view; any other root is read per call. Whether writes are enabled still comes from the connection's view, because agent_config_writes is ClassForcedGlobal and resolves to the same value in every project. The new tests cover: - an agent whose own config is malformed: before, a mutant that fell back to the connection's view passed the whole suite; - the re-pin window, reproduced deterministically; - agent_config writes in both directions; - the agent's boundary. The review assumed project [workspace] extra_roots widen an agent's boundary. They cannot: those fields are ClassForcedGlobal, so no project config ever sets them. buildAgentPolicy already keys the user-granted roots store on the agent's own root. The boundary test pins both directions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: giant-bison
…ask-config #533 changed session_start's struct (repin now returns a RepinReport) and repinAgent's return values. Conflict resolved by keeping main's struct with this branch's ctx-taking tasksFn, and updating the new #522 test helper to repinAgent's three return values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: vivid-mink
…ask-config Picks up #557. Merged cleanly; mutation_test's start-error path keeps the working_dir origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: vivid-mink
golimpio
previously approved these changes
Oct 1, 2026
golimpio
left a comment
Contributor
There was a problem hiding this comment.
Two independent review rounds: round 1 found no blockers, and the fold-ins were checked with race and mutation controls, with no blockers. Follow-up for the pre-existing stale apply in agent_config to be filed separately.
atlas-from-plumb
enabled auto-merge
October 1, 2026 01:24
Merging main (#534) into this branch took internal/cli/conn.go to 602 lines and check-size failed on both verify jobs. Condense the configRoot field comment; the meaning is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: teal-dingo
golimpio
approved these changes
Oct 1, 2026
golimpio
left a comment
Contributor
There was a problem hiding this comment.
Re-approving after a comment-only commit that brings conn.go back within the 600-line cap after the main merge.
golimpio
pushed a commit
that referenced
this pull request
Oct 1, 2026
Brings in #555 (per-agent task config). No conflicts. Plumb-Session: golden-moose
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.
Fixes #522
Defect
On a shared
plumb serveconnection, an agent that pins its own shard to project B (session_start, agent scope) while the connection stays on project A:run_taskandmutation_testresolved A's[tasks.<lang>]block (commands,working_dir,env) and ran it against B's root. A'sworking_dir = "plumb"sent the agent's build into<B>/plumb, which does not exist.run_commandresolved A's[[command]]allow-list and A's exec trust, so A's trusted scripts ran in B, and B's own entries were invisible.topology_affectedbuilt test targets relative to A'sworking_dir, andsession_startdescribed A's task surface.The failure was then reported as
fork/exec /opt/homebrew/bin/golangci-lint: no such file or directory. Go reports a missingcmd.Diras a missing binary, so the user goes looking for a PATH problem when the binary is fine.I reproduced both halves live while working on this PR. My agent was pinned to this worktree and the connection to the ops checkout.
run_task buildfailed withfork/exec /opt/homebrew/bin/go: no such file or directory, andmutation_testrefused to start because it had rungo buildin<worktree>/plumb.Cause
PLAN-440 item 4 moved the resolvers' working-directory root to the calling agent (
workspaceFor(ctx)). The blocks the root is combined with still came froms.view(), whichapplyProjectConfigfills for the connection's root only. Task trust was already checked against the agent's root, becausetaskProvenanceandIsTrustedForTaskstakews. Command trust was not:v.execTrustedis the connection's.Fix
projectViewFor(ctx)(new,internal/cli/conn_project_view.go). When the calling agent's effective root differs from the connection's, it loads that root's project config once. It returns the view with[tasks.<lang>],[[command]],[command_policy], the exec trust and command provenance from that load, plus the shard's language. A call on the connection's own root getss.view()unchanged, so single-agent connections, unattributed calls and agents on the connection's project take the same path as before. The commands and the trust that gates them still come from one load, which keeps the "authorise the content you run" propertyconn_commands.gorelies on. An unreadable config falls back to the global config, asapplyProjectConfigdoes, and never to the connection's project.require_sandbox),testScopeandtaskStateall read it.testScopeandtaskStatenow takectx, soTopologyAffected.WithTestScopeandSessionStart.WithTaskstakefunc(context.Context) ….RunArgvandRunTaskArgvstat the directory before exec. If it is missing or not a directory they return a*WorkingDirError(working directory <dir> does not exist).TaskCommand.WorkingDirSourcerecords the setting the directory came from, for example[tasks.go] working_dir = "plumb" in <config path>, naming the project or global config.run_taskandmutation_testappend it as(from …).run_commandgets the directory-naming error. Theplumb build|test|…CLI always runs from the root, so it cannot reach this case.Behaviour change to note
An agent in a git worktree now gets that worktree's project config under that worktree's
plumb trust, the same rule a connection-level pin into the worktree already follows. Project-supplied task or command overrides therefore needplumb trustin the worktree. plumb's ownGOTMPDIRentry is exempt, sorun_taskworks in plumb worktrees with no grant.Not changed
The
[git]policy stays per connection. The git tool takes an explicitrepothat can already point outside the pinned project under the connection's policy, so tying the policy to the agent's pin is a separate design question. It would also switch offallow_pushfor agents in untrusted worktrees. That question should be its own issue.Verification
internal/cli/conn_project_view_test.go: an agent in B resolves B's (default) build at B, notgo build -vin<B>/sub. B's untrusted[tasks]and[[command]]are refused on B's trust, and B's trusted ones run. A's[[command]]does not resolve in B.testScopeandtaskStatedescribe B.WorkingDirSourcenames B's config.internal/tools/cmdexec_workdir_test.go: theRunArgv,RunTaskArgv, not-a-directory,run_taskandmutation_testpaths. The red output before the fix was the exact symptom,fork/exec /bin/echo: no such file or directory.working_dir, A's[[command]]and A's test scope. An existing or empty working directory still runs.TestRunCommandResolvesTheCallingAgentsRootwas updated. Its worktree now carries (and trusts) its own copy of the project config, as a real checkout does.projectViewForreturning the connection view; each resolver and accessor readings.view(); dropping each override of tasks, commands, exec trust and command provenance; a missing or always-globalWorkingDirSource; dropping the pre-exec directory check and the not-a-directory branch; and dropping the(from …)suffix inrun_taskand inmutation_test. The first run left the provenance mutant SURVIVED.TestRunCommand_AgentPinnedElsewhereRunsGlobalCommandsAsGlobalnow pins it, and a re-run KILLED it. I mutated by hand from a committed tree, restored each file withgit checkout, and confirmed a clean tree afterwards. plumb's ownmutation_testcould not be used: the live daemon predates this fix, and its compile gate ran in<worktree>/plumb, which is this bug.GOTMPDIR=$PWD/.testcache:go test ./...passed (55 packages).XDG_DATA_HOMEwas pointed at a temp directory: without that,TestWriteSessionCollabPolicy_SilentWithoutPeersandTestOnProtocolNegotiatedblock on the live daemon's session-directoryflock, which is local contention and not caused by this change.-raceon the affected tests ininternal/toolsandinternal/clipassed.golangci-lint run ./...reported 0 issues.make check-size check-brief check-changelogpassed.Review follow-ups (folded in)
TestProjectView_MalformedAgentConfigNeverBorrowsTheConnectionspins this; before, a mutant that returned the connection view passed the whole suite.applyProjectConfignow records the root it loaded the config for, in a newsessionView.configRoot.projectViewFor's fast path checks that root, notacquiredRoot. A connection re-pin movesacquiredRootbefore the new config is applied, so a call made in between could pair the new root with the old project's[[command]]list and trust.TestProjectView_RepinWindowDoesNotPairNewRootWithOldConfigsets up that state exactly, so the test is deterministic. A view that has never had a project config applied holds only the global config, so it is still used as is.agent_configwrites the calling agent's project. Only the connection's own project is re-applied into the connection's view, because any other root is read per call. Whether writes are enabled still comes from the connection's view:agent_config_writesisClassForcedGlobal, so it resolves to the same value in every project. Tested in both directions.[workspace] extra_roots/read_rootswiden an agent's boundary in B doesn't hold. Those fields andallow_dependency_readsareClassForcedGlobal, so no project config ever sets them, trusted or not. The only per-project widening is the roots the user granted (WorkspaceRootsStore), andbuildAgentPolicyalready reads those for the agent's own root.TestAgentBoundary_GrantedRootsFollowTheAgentsRootpins both directions: A's granted root and A's configuredextra_rootsare refused in B, and B's granted root is allowed.[[command]]entries and task overrides in a worktree needplumb trustthere.acquiredRootagain;configRootnever recorded;agent_configwriting the connection's workspace;agent_configre-applying any root, or never re-applying; the agent boundary built for the connection's root; andprojectViewForalways returning the connection view. I re-ran them after merging main (fix(session_start): report which pin a re-pin moved (#517) #533, fix: shared-daemon friction — mutation_test slot holder, pre-commit lint, workspace_sessions timeout #557) and all were still KILLED.SessionStartstruct: I kept main'sRepinReportfield and this branch'stasksFnthat takes a ctx. fix(roots): no attach after close; coalesce roots/list_changed (#514) #534 is still open. After the final merge,go test ./...passed (55 packages),-racepassed on the affected tests, andgolangci-lintreported 0 issues.🤖 Generated with Claude Code