Skip to content

run: support working directory argument and persist it across calls (CMDLINE-12, #166) - #249

Merged
bbondy merged 11 commits into
brave:mainfrom
winshaurya:run-working-directory
Sep 12, 2026
Merged

bbondy merged 11 commits into
brave:mainfrom
winshaurya:run-working-directory

Conversation

@winshaurya

@winshaurya winshaurya commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Hello @bbondy,

This pull request implements CMDLINE-12 and resolves #166.

Summary of Changes

  1. Workspace resolution: Exposed Workspace::resolve so tools can resolve and confine workspace-relative and added directory paths.
  2. run tool schema and execution:
    • Added optional directory parameter to the run tool schema.
    • Promoted and gated directory through the policy layer, checking path confinement and deny rules.
    • Initialized run_directory to the workspace root at the start of each turn.
    • Carried run_directory across calls in Tools, persisting the directory across calls once a plan is endorsed.
    • Updated the pinned fields test in tools.rs.
  3. Spec clause and verification:
    • Added the_working_directory_persists_across_calls in crates/agent/tests/turn.rs.
    • Updated docs/specs/tools/command-line.md so CMDLINE-12 is verified by the new test.

Please let me know if anything else is needed or if any changes would be preferred. Thank you very much.

@winshaurya
winshaurya requested a review from bbondy as a code owner September 12, 2026 00:47
@winshaurya

winshaurya commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

I have pushed an update with a few refinements and expanded test coverage:

  1. Policy handling: Updated the directory parameter to be declassified through display release for plan endorsement, matching how command lines are handled. This ensures commands with a directory argument execute cleanly even in untrusted contexts.
  2. State mutation timing: Deferred updating the working directory state until pre-flight validations and launch succeed, preventing directory desynchronization on failed or invalid runs.
  3. Schema description: Updated the parameter description to note that both workspace-relative paths and added directories are accepted, as specified in CMDLINE-12.
  4. Test portability and coverage: Swapped command calls in tests to use portable binaries across platforms, and added test cases covering added directories, confinement escape rejection, non-existent directory errors, rejection rollbacks, and untrusted context execution.
  5. Formatting: Ensured full compliance with formatting and clippy checks.

Thank you very much for your time and guidance reviewing this.

bbondy
bbondy previously approved these changes Sep 12, 2026
winshaurya and others added 11 commits September 12, 2026 12:29
…e root

A vouched entry is keyed by resolved program path and exact argv, and records no
directory at all. While the working directory was fixed at the workspace root
that key was whole, because there was only one tree a line could run in. Now the
planner chooses it, and both halves of what `a` grants leaked to a tree nobody
was shown: the line ran unasked, and what it printed was labelled `(T,priv)`.
`git log --oneline -50`, vouched for in a person's own repository and reissued
with `directory` pointing at a vendored dependency, would have handed the planner
commit messages an attacker wrote, as trusted content.

The vouched road is open only where the line runs at the root, which is where
every standing answer is spelled, and closed when no root is known for the same
reason `read_proven` closes there. A line outside the root is asked about every
time and its output is quarantined. Widening the key to include a directory would
grant the shortcut back, and is not done: RUN-9 describes an entry as a program
and its arguments, and the session record spells it that way.

Three core tests state the two halves and the unknown-root case, and one turn
test says the same thing end to end. The three tests that already covered the
write and private-input precedence now name the root, since they put their plans
at `/work` and would otherwise be passing for the new reason instead of theirs.

Part of brave#166
…admit

Two ways a proposed directory got through that should not have. It was checked
against the `Read` rules, and a program's relative writes land in the directory it
runs in, so a tree an `Edit` rule protects was not protected by the check that
consulted only the read side: `npm install` in `vendor` writes throughout it
without naming a file. It is an effect, and asks the effect rules.

And a `directory` that was present but not a string was dropped rather than
refused, which ran the line wherever the last call left off, the one place a
planner that bothered to name a directory cannot have meant. `null` still reads
as absent, because that is what filling an optional field in with nothing says.

TOOL-1 names `directory` among the arguments that route, which is the table's
statement of why either check exists.

Part of brave#166
The line alone stopped saying where its output came from as soon as the directory
began to carry across calls: the call that named it can have scrolled off the
transcript or been summarised out of the conversation, and what reaches the person
and the planner is the line. Both now read `cargo test (in vendor/dependency)`
where the tree is not the workspace root, and are unchanged where it is.

Structure either way, and a path this crate resolved itself, so nothing about what
ran decides any of it.

Part of brave#166
It moved before the run, so a line that was refused at the prompt, or that failed
to start, still left the next line pointing at a tree the person had said no to.
The background branch already waited for its launch to succeed; the foreground now
carries it in the same place, once the stages have actually run.

The test that covered this had its own copy of a `Confirmer` inline to answer two
runs differently. That is `AskedAboutRuns::answering_in_turn`, which the shared one
now offers and refuses anything past the end of the queue, so a test that runs a
line more than it meant to fails rather than quietly approving it.

Part of brave#166
The assertion was `contains("refused:") || contains("escapes")`, and the body it
reads carries the whole conversation including the call's own arguments, so the
second half held for the directory the test itself had named however the tool had
answered. Both halves are now required, the wording is the one the workspace error
actually prints, and the directory is named `../elsewhere` so nothing in the
request can satisfy the check on its own.

Part of brave#166
`resolve` was made `pub` to reach it from the tools module, which is in this crate
already, so `pub(crate)` is what it needed. A resolver is the thing that decides
whether a path escapes the workspace, and a caller outside the crate that resolved
a path itself would be deciding that without the policy layer.

Part of brave#166
…g is short of

The clause named one test and five more were written against it. It also has two
things to say now that a directory is a field a caller can set: that a directory
other than the root is asked about every time and its output quarantined, which is
RUN-8's consequence and the thing a reader of this clause alone would get wrong,
and that the carrying lasts a turn rather than a session.

The turn is where the second of those is decided, so it says so where the value is
made, along with what it would take to carry one further: the session record and
`--resume`, the way the vouched list is carried.

Part of brave#166
@bbondy
bbondy merged commit ade9e78 into brave:main Sep 12, 2026
15 checks passed
@winshaurya

Copy link
Copy Markdown
Contributor Author

Thanks a lot for the review and for improving the pr @bbondy I learned a lot from the changes, especially checking edge cases, security, permissions, and what happens when something fails. I’ll make sure to keep these things in mind and apply them in my next prs. appreciate it

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.

command-line: a call cannot name its working directory

3 participants