Skip to main content

CommandBackend

Struct CommandBackend 

Source
pub struct CommandBackend { /* private fields */ }
Expand description

A subprocess backend: prompt on stdin, completion on stdout.

Implementations§

Source§

impl CommandBackend

Source

pub fn new(argv: Vec<String>, timeout: Duration) -> Self

A backend named by its own command line.

The named constructors below are the production path; this is for a backend with nothing better to call itself, which in practice means a test double.

Source

pub fn with_working_dir(self, dir: &Path) -> Self

Run the child in dir.

The repository root, for any backend whose prompt names repo-relative paths — which the default one does. See the field for what goes wrong without it, and why it goes wrong silently.

Source

pub fn with_cancel(self, flag: Arc<AtomicBool>) -> Self

Kill the child as soon as flag is set.

Source

pub fn with_timeout(self, timeout: Duration) -> Self

Source

pub fn claude_cli(fetch: &str) -> Self

The default: headless, text output, and read-only tools (ADR 0022).

ADR 0010 denied tools outright, because the evaluated grouping tool kept exiting 1 on stop_reason: "tool_use". Denying them cured it by sending no tool definitions at all, so the model could not ask. An allowlist is the other cure: it can ask, and the answer is yes.

fetch is the executable the prompt tells the model to run — normally this process. The allowlist is derived from it, so the two cannot disagree about what the model is allowed to invoke.

Nothing here can write. The fetch command reads the document the engine just wrote; the rest read the repository. git log and git show are what reach the reason a change was made, which no prompt can carry.

git diff is advertised; the rest are not. The prompt names the fetch command and git diff, and nothing else.

That is a change of rule, and it is worth saying why. git diff is advertised because it is now the only way to see what a hunk says: the fetch command’s diff query is gone, having duplicated class except for the text. A tool the model must use and is not told about is a tool it will not use.

It costs an invitation to read the whole repository, and the prompt is what pays for that: it says to read what decides a label and then stop.

It no longer costs a route around the generated content this stage folds away, though it did when it was written. generated is part of the shape-class key now (ADR 0004), so no class the model is given contains a generated file and there is nothing folded left for it to ask git diff about by accident. The prompt still says not to go looking.

Read, Grep, Glob, git log and git show stay unadvertised for the original reason: a model that needs the code around a hunk can go and read it, but it is not sent looking. If you add a tool here, do not add a line about it to the prompt.

The allowlist is this function’s business, not the user’s, and there is no config that replaces it. [grouping].agent picks between agents by name; it used to take a free argv, which handed a stranger’s process the prompt and none of the allowlist, fetch command or read path the prompt is written for.

fetch is where a binary lives, so it is the one part of this argv that says nothing about what the model will do. The cache identity stands a placeholder in its place: change the allowlist and every cached grouping is rightly invalidated, move the binary and none of them are.

--permission-mode default is what makes the allowlist mean anything, and it was missing for two releases (ADR 0033). --allowed-tools ADDS permissions; it does not cap them. A user whose own settings set defaultMode to auto, acceptEdits or bypassPermissions was handing this call an agent that could write, commit and push, and nothing anywhere said so. default means ask, and a headless call has nobody to ask, so the answer is no.

It was found by dfr agents --probe, on the first run, against the agent that had shipped as the only option. That is the whole argument for the probe existing.

Source

pub fn codex_cli() -> Self

Headless codex exec, read-only by OS sandbox (ADR 0033).

Codex has no tool allowlist and needs none: --sandbox read-only is enforced by the kernel — Seatbelt on macOS, bubblewrap on Linux — so the model may run any command it likes and the writes are refused beneath it. That is a different boundary from Claude Code’s and an equally real one, which is why fetch does not appear in this argv at all. The prompt still names the fetch command; nothing has to permit it.

-c approval_policy="never" is the headless half. Without it a command the sandbox refuses escalates to a human who is not there, and the call sits until the deadline kills it. With it the refusal returns to the model as a tool failure, which is what we want it to see.

It is a config override rather than the --ask-for-approval flag the docs name, because that flag does not exist on codex exec — it is on the interactive top-level command only, and codex exec rejects it outright. Checked against 0.154.0, where passing it is error: unexpected argument, which is a failure to spawn rather than a bad grouping.

codex exec already defaults to never asking, so this says out loud what is currently true anyway. That is the point: a boundary resting on another program’s default is one release away from being no boundary, and --ignore-user-config means nothing on disk can move it back.

--color never keeps stdout clean. The response parser takes the text between the first { and the last }, and an escape sequence inside that span is a parse error with a sample nobody can read.

The trailing - makes stdin the whole prompt. Codex will otherwise treat stdin as context for an argv instruction, and there is no argv instruction here.

Never pass --full-auto, --yolo or --dangerously-bypass-approvals-and-sandbox: each removes the boundary.

--ignore-user-config and --ignore-rules are the same lesson Claude Code taught (ADR 0033): the sandbox a flag asks for is not the sandbox that runs if the user’s own config.toml or execpolicy rules say otherwise. An argv that can be widened by a file this crate never reads is not a boundary, it is a request.

Source

pub fn droid_cli() -> Self

Headless droid exec, read-only by default (ADR 0033).

Droid is the one agent whose boundary is what this function does NOT pass. Its documented default is read-only file inspection plus git read operations, with file edits, package installs and git writes blocked, and a blocked action fails rather than asking — so a bare droid exec neither writes nor stalls.

Never pass --auto at any level, and never --skip-permissions-unsafe. Each is the whole boundary, given away.

-o text prints the final message only. - makes stdin the prompt.

Source

pub fn copilot_cli(fetch: &str) -> Self

Headless copilot, read-only by allowlist and an explicit deny (ADR 0033).

The closest of the five to Claude Code: an allowlist derived from fetch, so the prompt can never name a command the model may not run.

There is deliberately no -p. Copilot reads the prompt from stdin, and its own documentation says piped input is ignored when -p is given. Passing both would send an empty prompt and waste a call.

-s suppresses the session decoration around the reply, for the same reason Codex gets --color never. --no-ask-user stops the agent pausing for a human who is not there.

--deny-tool write is belt and braces: write is already absent from the allowlist, and a deny takes precedence over any allow, so the two cannot be talked out of agreeing.

Never pass --allow-all-tools or --allow-all-paths.

Source

pub fn pi_cli() -> Self

Headless pi. Read-only is NOT enforced here (ADR 0033).

Every other constructor in this file hands the model a boundary. This one cannot, and the reason is Pi’s design rather than an oversight in this argv.

Pi ships no sandbox, no per-command allowlist and no approval prompts. Its -t flag toggles whole tools, and bash is one tool: the model needs it to run the fetch command and git diff, and the same tool lets it write a file, commit or push. Nothing but the prompt asks it not to.

Dropping bash would restore the boundary and take the change with it. The model would be back to grouping from class ids alone, which is the truncated payload ADR 0022 was written to end — a worse grouping, every time, in exchange for a risk the prompt never asks anyone to take.

So the author chose this knowingly, and the duty that comes with it is disclosure: Agent::read_only answers NotEnforced for Pi, and every place that offers the name says so.

The rest of the argv is hermetic sealing, and it is not decoration. -nc drops AGENTS.md and CLAUDE.md, -na drops the repository’s own .pi/ config, and --no-extensions --no-skills drop the user’s. Each is a file outside the cache key that could otherwise change a grouping, which is the hole ADR 0022 names and cannot close. --no-session stops Pi writing a session file for a call nobody resumes.

Source

pub fn program(&self) -> &str

The program this backend spawns, for a caller checking PATH.

dfr agents says whether each agent is installed, and the answer has to come from the argv that will actually run rather than from a second list of executable names that could disagree with it.

Source

pub fn command(&self) -> &str

The whole command line, for a caller showing what will run.

Not LlmBackend::name, which is a product name, and not LlmBackend::identity, which stands a placeholder where the binary path is. This is the argv itself, and the two callers that want it are a spawn failure and dfr agents.

Trait Implementations§

Source§

impl LlmBackend for CommandBackend

Source§

fn name(&self) -> &str

What to call this agent on screen, for a reviewer waiting on it. Read more
Source§

fn identity(&self) -> &str

Everything about this backend that could change the grouping, and nothing that could not. The grouping cache key hashes this (ADR 0009). Read more
Source§

fn complete(&self, prompt: &str) -> Result<String, LlmError>

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.