An agent node on the Claude CLI now delegates instead of refusing - #58
Merged
Merged
Conversation
`AgentNode` used to raise `AgentConfigError` when handed the Claude CLI: the
CLI has no tool-calling wire format, so GraphARC cannot run its own gated loop
over it. That was accurate and useless — the CLI *is* a complete agent, and
refusing to use it meant the subscription backend could do no agentic work
inside a graph at all.
It now hands the whole loop to Claude Code's headless agent, reusing the same
machinery `grapharc agent --executor claude-cli` already used. That path's
subprocess-and-parse core is extracted as `delegate_task()`, so the CLI command
and the node cannot drift on what actually gets spawned.
This is a real widening of the trust boundary and is treated as one:
- `DelegatedToolUseWarning` at construction, naming each thing given up — every
tool Claude Code has, not checked by this graph's policy, not confined by the
sandbox, token figure self-reported. Its own category, so
`-W error::grapharc.harness.agent.DelegatedToolUseWarning` restores the old
refusal for anyone who wants it.
- **Every trace event from a delegated node carries `executor=delegated`**, plus
the permission mode and `governed_by`. A warning is gone by the time someone
reads the run back; without this, a JSONL reader six months later sees an
agent node that completed and cannot tell that its tool calls never reached
the permission engine. That is the project's own claim about its traces, so
the marking is the part that keeps it true.
One thing found by running it rather than reasoning about it: omitting
`--allowedTools` does **not** mean "every tool". It leaves Claude Code's own
gating in place, and headless there is nobody to approve a Write — the first
delegated node came back reporting it could not create the file. Only
`--permission-mode bypassPermissions` means what "everything Claude Code has"
was meant to mean, so that is what a delegated node runs under, named in the
warning and recorded in the trace. `delegate_task` takes the two as separate
arguments because conflating them is the trap.
Detection is on `_llm_type == "grapharc-claude-cli"`, not on "does this model
lack `bind_tools`" — `ScriptedChatModel` lacks it too, and matching that way
would have silently delegated every mocked agent in the suite to a real
subprocess. A test pins that.
Verified live end to end: a node given `get_model("claude-cli")` created a file
in its workspace using Claude Code's own tools (2 turns, target_met), and the
trace shows `executor=delegated` with `permission_mode=bypassPermissions` on
every event. Docs updated in README's limits and `stdlib`'s module docstring,
both of which asserted the old refusal. Full suite green on 3.12; ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.
AgentNodeused to raiseAgentConfigErrorwhen handed the Claude CLI: the CLI has no tool-calling wire format, so GraphARC cannot run its own gated loop over it. That was accurate and useless — the CLI is a complete agent, and refusing to use it meant the subscription backend could do no agentic work inside a graph at all.It now hands the whole loop to Claude Code's headless agent, reusing the same machinery
grapharc agent --executor claude-clialready used. That path's subprocess-and-parse core is extracted asdelegate_task(), so the CLI command and the node cannot drift on what actually gets spawned.This is a real widening of the trust boundary and is treated as one:
DelegatedToolUseWarningat construction, naming each thing given up — every tool Claude Code has, not checked by this graph's policy, not confined by the sandbox, token figure self-reported. Its own category, so-W error::grapharc.harness.agent.DelegatedToolUseWarningrestores the old refusal for anyone who wants it.executor=delegated, plus the permission mode andgoverned_by. A warning is gone by the time someone reads the run back; without this, a JSONL reader six months later sees an agent node that completed and cannot tell that its tool calls never reached the permission engine. That is the project's own claim about its traces, so the marking is the part that keeps it true.One thing found by running it rather than reasoning about it: omitting
--allowedToolsdoes not mean "every tool". It leaves Claude Code's own gating in place, and headless there is nobody to approve a Write — the first delegated node came back reporting it could not create the file. Only--permission-mode bypassPermissionsmeans what "everything Claude Code has" was meant to mean, so that is what a delegated node runs under, named in the warning and recorded in the trace.delegate_tasktakes the two as separate arguments because conflating them is the trap.Detection is on
_llm_type == "grapharc-claude-cli", not on "does this model lackbind_tools" —ScriptedChatModellacks it too, and matching that way would have silently delegated every mocked agent in the suite to a real subprocess. A test pins that.Verified live end to end: a node given
get_model("claude-cli")created a file in its workspace using Claude Code's own tools (2 turns, target_met), and the trace showsexecutor=delegatedwithpermission_mode=bypassPermissionson every event. Docs updated in README's limits andstdlib's module docstring, both of which asserted the old refusal. Full suite green on 3.12; ruff clean.