Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ plain shell in the container.
`claude` starts in the permission mode set in your host `~/.claude/settings.json`,
which is mounted into the container. The container keeps the agent away from
the rest of the host file system, but the network is unrestricted and the
project and `~/.claude`, credentials included, are mounted read-write. Pass
`--skip-permissions` only when that is acceptable.
project and the host config of every supported agent, credentials included,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Non-blocking: this paragraph is about claude and --skip-permissions, but readers most need to know that a plain shell now gets every agent's credentials read-write. A sentence under Usage, after "Omit it for a plain shell in the container", would say so. With per-agent mounts this line can go back to naming ~/.claude and ~/.claude.json.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Why do "readers most need to know that a plain shell now gets every agent's credentials read-write"? The script is called "agentic-sandbox".

Removing per-agent mounts is the point in this CL.

are mounted read-write. Pass `--skip-permissions` only when that is acceptable.

### Sandbox image inputs

Expand Down
79 changes: 41 additions & 38 deletions scripts/agentic-sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,45 +127,46 @@ def _identity_args() -> list[str]:
return args


def _agent_mount_args(agent: str | None) -> list[str]:
"""Seed and mount host config so an agent CLI keeps its auth state."""
def _seed_agent_config() -> None:
"""Create default host config files and directories for agent mounts."""
# Use os.open with restrictive permissions in _seed_config_file to avoid
# exposing credentials to other local users on multi-user systems.
if agent == "agy":
gemini_config_dir = os.path.expanduser("~/.gemini")
os.makedirs(gemini_config_dir, mode=0o700, exist_ok=True)
_seed_config_file(
os.path.join(gemini_config_dir, "trustedFolders.json"),
b'{"/workspace": "TRUST_FOLDER"}',
)
_seed_config_file(
os.path.join(gemini_config_dir, "settings.json"),
b'{"selectedAuthType": "oauth-personal"}',
)
return ["-v", f"{gemini_config_dir}:/home/vscode/.gemini"]

if agent == "claude":
host_claude_dir = os.path.expanduser("~/.claude")
host_claude_json = os.path.expanduser("~/.claude.json")
os.makedirs(host_claude_dir, mode=0o700, exist_ok=True)
# Ensure the file exists on the host so Docker doesn't create it as a
# directory.
if os.path.exists(host_claude_json):
if not os.path.isfile(host_claude_json):
sys.exit(
f"Expected {host_claude_json} to be a regular file, but found "
"a different filesystem object. Remove or rename it and rerun."
)
else:
_seed_config_file(host_claude_json, b"")
return [
"-v",
f"{host_claude_dir}:/home/vscode/.claude",
"-v",
f"{host_claude_json}:/home/vscode/.claude.json",
]

return []
gemini_config_dir = os.path.expanduser("~/.gemini")
os.makedirs(gemini_config_dir, mode=0o700, exist_ok=True)
_seed_config_file(
os.path.join(gemini_config_dir, "trustedFolders.json"),
b'{"/workspace": "TRUST_FOLDER"}',
)
_seed_config_file(
os.path.join(gemini_config_dir, "settings.json"),
b'{"selectedAuthType": "oauth-personal"}',
)

host_claude_dir = os.path.expanduser("~/.claude")
host_claude_json = os.path.expanduser("~/.claude.json")
os.makedirs(host_claude_dir, mode=0o700, exist_ok=True)
# Ensure the file exists on the host so Docker doesn't create it as a
# directory.
if os.path.exists(host_claude_json):
if not os.path.isfile(host_claude_json):
sys.exit(
f"Expected {host_claude_json} to be a regular file, but found "
"a different filesystem object. Remove or rename it and rerun."
)
else:
_seed_config_file(host_claude_json, b"")


def _agent_mount_args() -> list[str]:
"""Mount host config so agent CLIs keep their auth state."""
return [
"-v",
f"{os.path.expanduser('~/.gemini')}:/home/vscode/.gemini",
"-v",
f"{os.path.expanduser('~/.claude')}:/home/vscode/.claude",
"-v",
f"{os.path.expanduser('~/.claude.json')}:/home/vscode/.claude.json",
]


def main() -> None:
Expand Down Expand Up @@ -213,6 +214,8 @@ def main() -> None:
if args.skip_permissions and args.agent != "claude":
parser.error("--skip-permissions requires the claude agent")

_seed_agent_config()

def log(msg: str) -> None:
if args.verbose:
print(msg)
Expand Down Expand Up @@ -273,7 +276,7 @@ def log(msg: str) -> None:
if args.offline:
run_args.extend(["--network", "none"])

run_args.extend(_agent_mount_args(args.agent))
run_args.extend(_agent_mount_args())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking: this mounts all three paths whichever agent is chosen:

  • claude --skip-permissions now runs with ~/.gemini, Gemini OAuth tokens included, mounted read-write, with open network and no prompts. agy gets ~/.claude and ~/.claude.json.
  • --offline gets every agent's credentials, but with --network none no agent can authenticate, so the mounts only add exposure. Before this PR the plain shell mounted none, offline or not.

Non-blocking: seeding has moved too. _seed_agent_config() now runs for every agent, before git rev-parse and docker build, so a run that fails still writes ~/.gemini/settings.json (selectedAuthType: oauth-personal), ~/.gemini/trustedFolders.json and an empty ~/.claude.json into the host home. agy and the plain shell now exit when ~/.claude.json is a directory. And with seeding split from mounting, the two functions build the same paths separately, and the mounts are only safe once seeding has run.

Keeping main's _agent_mount_args(agent), which seeds and mounts together, and changing only this call site fixes all of these:

    if args.agent is not None:
        run_args.extend(_agent_mount_args(args.agent))
    elif not args.offline:
        for agent in AGENT_CLIS:
            run_args.extend(_agent_mount_args(agent))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The aim of this CL is to allow any supported agent to be used from the sandbox. There is no problem to 'fix' here.

run_args.extend(_identity_args())

if "TERM" in os.environ:
Expand Down
Loading