Skip to content

Add --auto-write flag to relax file-write confirmation in --files mode#266

Merged
alexkroman merged 5 commits into
mainfrom
claude/gifted-hawking-03sdhe
Jun 23, 2026
Merged

Add --auto-write flag to relax file-write confirmation in --files mode#266
alexkroman merged 5 commits into
mainfrom
claude/gifted-hawking-03sdhe

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Adds --auto-write DIR option to assembly live that auto-approves writes under specified subdirectories while keeping other writes gated for confirmation. This reduces friction in hands-free voice interactions where every keypress is disruptive.

Summary

When --files is enabled, all file writes and command executions require human confirmation. The new --auto-write flag (repeatable) designates subdirectories where writes auto-approve without interruption, while writes elsewhere still pause for approval. This is implemented via path-scoped interrupt predicates that integrate with deepagents' FilesystemPermission rules.

Key Changes

  • New module aai_cli/agent_cascade/write_gate.py: Translates --auto-write paths into deepagents-compatible interrupt predicates. Converts a list of allowed subtrees into FilesystemPermission rules (allow under each subtree, interrupt everywhere else), then derives per-tool when predicates for write_file and edit_file. The execute tool remains unconditionally gated since it can't be path-scoped.

  • CLI option in aai_cli/commands/agent_cascade/__init__.py: Added --auto-write repeatable option that accepts relative directory paths.

  • Path normalization in aai_cli/commands/agent_cascade/_exec.py:

    • _normalize_auto_write() converts variants (scratch, ./scratch, /scratch) to virtual roots (/scratch)
    • Rejects escapes (..), home (~), and whole-tree aliases (empty, ., /)
    • _resolve_auto_write_paths() validates that --auto-write is only used with --files
  • Config threading in aai_cli/agent_cascade/config.py: Added auto_write_paths field to CascadeConfig to carry normalized paths through the cascade engine.

  • Brain integration in aai_cli/agent_cascade/brain.py: Updated _graph_kwargs() to call write_interrupt_on() with the configured paths, replacing the previous all-or-nothing dict.fromkeys(_WRITE_TOOLS, True) with path-scoped interrupt configs. Subagents inherit the same gate to maintain consistent approval policy.

  • Subagent update in aai_cli/agent_cascade/subagents.py: Relaxed type hint on interrupt_on parameter from dict[str, bool] to dict[str, object] to accommodate InterruptOnConfig maps alongside plain True values.

Implementation Details

  • First-match-wins rule ordering: Each --auto-write path contributes both the node (/scratch) and its subtree (/scratch/**) to the allow rules, which precede a catch-all /** interrupt rule. This ensures writes under an allowed subtree match first and auto-approve.

  • Virtual path semantics: The sandboxed backend runs in virtual_mode, so all paths are cwd-rooted and start with /. Auto-write paths are normalized to this same virtual root for consistency.

  • Execute always gated: Command execution cannot be path-scoped (deepagents derives interrupt predicates for file tools only), so execute is unconditionally set to True in the interrupt map.

  • Backward compatible: With no --auto-write paths, the behavior is identical to before—every write is gated. The feature is opt-in and only applies when --files is enabled.

Tests

Comprehensive test coverage added in tests/test_agent_cascade_brain.py and tests/test_agent_cascade_files.py:

  • Path normalization and validation (rejects escapes, home, whole-tree aliases)
  • Config threading through the headless run path
  • Interrupt predicate behavior (auto-approve under allowed subtrees, gate elsewhere)
  • Multiple subtree support
  • Rule ordering verification

https://claude.ai/code/session_014ikey42C3eD4eKXkwmKFqy

…ssion

Replace the all-or-nothing write gate (interrupt_on=dict.fromkeys(_WRITE_TOOLS,
True)) with a path-scoped policy expressed as deepagents FilesystemPermission
rules: a new --auto-write DIR (repeatable) auto-approves writes under that cwd
subtree while writes anywhere else still pause for confirmation — less
keypress friction for hands-free voice turns.

The policy (allow under each --auto-write subtree, interrupt everywhere else)
lives in a new agent_cascade/write_gate.py and is translated to the per-tool
`when` predicates HumanInTheLoopMiddleware consumes. We can't use
create_deep_agent(permissions=…) — deepagents raises NotImplementedError when
permissions meet our execute-capable SandboxedShellBackend — so we derive the
interrupt_on ourselves and keep `execute` unconditionally gated (it can't be
path-scoped). Empty --auto-write gates every write, identical to prior behavior.

--auto-write is normalized to a cwd-rooted virtual root (rejecting ../~/whole-tree
aliases) and requires --files. The general-purpose subagent inherits the same
gate mapping so its mutations prompt through the same approver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ikey42C3eD4eKXkwmKFqy
@alexkroman alexkroman enabled auto-merge June 23, 2026 15:45
Comment thread aai_cli/agent_cascade/write_gate.py Outdated
Comment on lines +56 to +66
from deepagents.middleware import _fs_interrupt

# deepagents' permission→interrupt translator is the same one its public `create_deep_agent`
# uses, but it lives in a private module. We can't reach it via `create_deep_agent(permissions=…)`
# because that raises NotImplementedError against an execute-capable backend (ours), so we call
# the translator directly. The name is held in a variable (not a literal getattr / direct
# import) so the static checker doesn't bind to the private symbol while we deliberately reuse
# this internal — deepagents is version-pinned in uv.lock, so it can't shift under us silently.
translator = "_build_interrupt_on_from_permissions"
build = getattr(_fs_interrupt, translator)
interrupt_on: dict[str, object] = dict(build(_auto_write_rules(auto_write_paths)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing the private symbol name in 'translator' then calling getattr(_fs_interrupt, translator) hides the direct binding and can impede code review or static analysis; prefer an explicit import/reference so the call site is clear.

Show fix
Suggested change
from deepagents.middleware import _fs_interrupt
# deepagents' permission→interrupt translator is the same one its public `create_deep_agent`
# uses, but it lives in a private module. We can't reach it via `create_deep_agent(permissions=…)`
# because that raises NotImplementedError against an execute-capable backend (ours), so we call
# the translator directly. The name is held in a variable (not a literal getattr / direct
# import) so the static checker doesn't bind to the private symbol while we deliberately reuse
# this internal — deepagents is version-pinned in uv.lock, so it can't shift under us silently.
translator = "_build_interrupt_on_from_permissions"
build = getattr(_fs_interrupt, translator)
interrupt_on: dict[str, object] = dict(build(_auto_write_rules(auto_write_paths)))
from deepagents.middleware._fs_interrupt import _build_interrupt_on_from_permissions
# deepagents' permission→interrupt translator is the same one its public `create_deep_agent`
# uses, but it lives in a private module. We can't reach it via `create_deep_agent(permissions=…)`
# because that raises NotImplementedError against an execute-capable backend (ours), so we call
# the translator directly. deepagents is version-pinned in uv.lock, so it can't shift under us
# silently.
interrupt_on: dict[str, object] = dict(_build_interrupt_on_from_permissions(_auto_write_rules(auto_write_paths)))
Details

✨ AI Reasoning
​A newly added function constructs a private symbol name in a string and uses getattr to access it at runtime. This hides the direct import/use of a private API from static analysis and readers. While the comment claims this is intentional to avoid static binding to a private symbol, the pattern is equivalent to a lightweight obfuscation technique because it defers binding via a string and dynamic lookup rather than a direct import. Such dynamic access can conceal behavior and make audits harder. The relevant change introduces the translator variable and getattr usage where the private translator is invoked.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

claude added 2 commits June 23, 2026 15:53
…ernal

Address the Aikido review on PR #266: the getattr-via-variable indirection into
deepagents' private _build_interrupt_on_from_permissions read as obfuscation.
Remove it entirely. --auto-write is directory-prefix semantics and only the two
exact-path write tools are gated (reads stay ungated), so the gate is now a
transparent, fully-typed `when` predicate that prefix-matches the write target's
posix path segments against the auto-write roots — no private import, no getattr,
no glob/FilesystemPermission machinery.

Whole-segment matching (so /scratchpad isn't treated as under /scratch) and a
fail-safe to gating for non-string or `..`-containing targets are covered by new
tests; behavior is otherwise identical (empty --auto-write gates every write).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ikey42C3eD4eKXkwmKFqy
…-03sdhe

# Conflicts:
#	aai_cli/AGENTS.md
#	tests/__snapshots__/test_snapshots_help_run.ambr
@alexkroman alexkroman added this pull request to the merge queue Jun 23, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jun 23, 2026
…-03sdhe

# Conflicts:
#	aai_cli/AGENTS.md
#	aai_cli/agent_cascade/config.py
#	aai_cli/commands/agent_cascade/_exec.py
@alexkroman alexkroman enabled auto-merge June 23, 2026 16:12
@alexkroman alexkroman added this pull request to the merge queue Jun 23, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jun 23, 2026
…-03sdhe

# Conflicts:
#	aai_cli/AGENTS.md
#	aai_cli/agent_cascade/brain.py
#	aai_cli/agent_cascade/subagents.py
#	tests/test_agent_cascade_subagents.py
@alexkroman alexkroman added this pull request to the merge queue Jun 23, 2026
Merged via the queue into main with commit befccb7 Jun 23, 2026
20 checks passed
@alexkroman alexkroman deleted the claude/gifted-hawking-03sdhe branch June 23, 2026 16:31
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.

2 participants