From d0cf4f2dddc17012014b039b4bf8b5432dddc9c5 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Wed, 11 Nov 2026 07:04:51 +0000 Subject: [PATCH 1/5] Broker scoped Orca worker mail inside nono --- .github/workflows/check.yaml | 2 +- .mise.toml | 7 + README.md | 20 +- home/dot_agents/hooks/executable_secret-guard | 2 +- home/dot_config/fish/functions/claude.fish | 2 +- home/dot_config/fish/functions/codex.fish | 2 +- home/dot_config/fish/functions/opencode.fish | 2 +- home/dot_config/fish/functions/pi.fish | 2 +- home/dot_local/bin/executable_nono-agent | 236 ++++++++++++++++++ tests/nono-agent.test.mjs | 144 +++++++++++ tests/secret-guard.test.mjs | 12 + 11 files changed, 416 insertions(+), 15 deletions(-) create mode 100755 home/dot_local/bin/executable_nono-agent create mode 100644 tests/nono-agent.test.mjs diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 3948fe4..d117dd9 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -26,6 +26,6 @@ jobs: - name: Set Up Shared Tools & Tasks uses: ./.github/actions/mise with: - tools: actionlint chezmoi gitleaks hk jq node node@24 npm:renovate oxfmt shellcheck + tools: actionlint chezmoi gitleaks hk jq node node@24 npm:renovate oxfmt python ruff shellcheck - name: Run Checks run: mise run check diff --git a/.mise.toml b/.mise.toml index 9895d15..6184307 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,6 +1,7 @@ [tasks.check] depends = [ "check:actions", + "check:agent-launcher", "check:mise", "check:templates", "check:updates", @@ -8,6 +9,12 @@ depends = [ "test", ] +[tasks."check:agent-launcher"] +run = [ + "ruff check home/dot_local/bin/executable_nono-agent", + "ruff format --check home/dot_local/bin/executable_nono-agent", +] + [tasks."check:mise"] run = ''' set -eu diff --git a/README.md b/README.md index c8790c4..b8e9c63 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ so copy those edits back into `home/`. ## Agent Sandboxes -The Fish wrappers for Claude Code, Codex, OpenCode and Pi run them through -[nono](https://github.com/nolabs-ai/nono) with the profiles in +The Fish wrappers for Claude Code, Codex, OpenCode and Pi use `nono-agent` to run +them through [nono](https://github.com/nolabs-ai/nono) with the profiles in `home/dot_config/nono/profiles`, built from one shared template. Each extends the upstream profile for its agent, adds Mise tools and read access to the shared agent guidance, and allows writes only in the directory the agent starts in and @@ -62,20 +62,22 @@ its repository's Git directory, so agents can commit in worktrees. Start an agent in `~/Projects` to work across projects. Pull each upstream profile once with `nono pull nolabs-ai/AGENT`; Mise does not update them, so check with `nono outdated` and run `nono update`, then `chezmoi apply`: the Codex -template moves nono's Codex settings out of the block nono inserts. +template moves nono's Codex settings out of the block nono inserts. Nono is the +sandbox boundary; the launcher disables Codex's and Claude Code's nested sandboxes. Sandboxed agents read only the shared Git configuration and the `GitHub Agents` gh configuration, so they commit unsigned and push over HTTPS. They cannot reach -1Password, SSH, cluster credentials or Orca's CLI, which would let them run -commands outside the sandbox; Orca still shows their status. Run `command AGENT` +1Password, SSH, cluster credentials or unrestricted Orca control. Run `command AGENT` for work that needs those, such as OpenTofu plans and applies, host work, Kubernetes and Talos administration, chezmoi and Xcode builds. Orca launches agents through these wrappers when its agent command for each one -is set to `fish -c "AGENT $argv" --`. Orca workers report back through its CLI, so -start them unsandboxed: create a terminal running `command AGENT` with -`orca terminal create --command` and give it the task with -`orca orchestration worker-start --terminal`. +is set to `fish -c "AGENT $argv" --`. On Linux, the launcher gives Orca workers a +separate nono command policy for mailbox operations and the two Orca skill guides. +Only the brokered CLI receives its runtime metadata and socket access; terminal +control and foreign-mailbox routing stay denied. The policy is generated from the +installed Orca launcher for each session and removed on exit. Workers can also +read their main checkout's `todo.md` without receiving access to the whole checkout. Agent logins, sessions and history stay on each machine: sign in to each agent once per machine. `CLAUDE_CONFIG_DIR` keeps Claude's state file inside `~/.claude`, diff --git a/home/dot_agents/hooks/executable_secret-guard b/home/dot_agents/hooks/executable_secret-guard index 64b8274..f0e7138 100755 --- a/home/dot_agents/hooks/executable_secret-guard +++ b/home/dot_agents/hooks/executable_secret-guard @@ -7,7 +7,7 @@ # Output that gitleaks cannot scan is withheld rather than passed through. set -euo pipefail -blocked='\bop (read|inject)\b|\bop item get\b.*--(reveal|fields)|\bfnox get\b|(^|[;&|[:space:]])printenv\b|(^|[;&|][[:space:]]*)env[[:space:]]*($|[;&|>])|security find-(generic|internet)-password.*-w|\.env($|[^.[:alnum:]_-]|\.)|(^|[/[:space:]"'"'"'=])secrets\.yaml|auth\.json|\.config/op/|rclone\.conf|\.kube/config|\.talos/config' +blocked='(^|[^[:alnum:]_-])op (read|inject)\b|\bop item get\b.*--(reveal|fields)|\bfnox get\b|(^|[;&|[:space:]])printenv\b|(^|[;&|][[:space:]]*)env[[:space:]]*($|[;&|>])|security find-(generic|internet)-password.*-w|\.env($|[^.[:alnum:]_-]|\.)|(^|[/[:space:]"'"'"'=])secrets\.yaml|auth\.json|\.config/op/|rclone\.conf|\.kube/config|\.talos/config' withheld="[secret-guard withheld this output because it could not be scanned for secrets]" diff --git a/home/dot_config/fish/functions/claude.fish b/home/dot_config/fish/functions/claude.fish index a8be8b2..a52d98a 100644 --- a/home/dot_config/fish/functions/claude.fish +++ b/home/dot_config/fish/functions/claude.fish @@ -1,3 +1,3 @@ function claude --wraps claude - nono run --allow-cwd --allow (git rev-parse --path-format=absolute --git-common-dir 2>/dev/null; or pwd) --profile claude -- claude $argv + nono-agent claude $argv end diff --git a/home/dot_config/fish/functions/codex.fish b/home/dot_config/fish/functions/codex.fish index e973c11..e35794f 100644 --- a/home/dot_config/fish/functions/codex.fish +++ b/home/dot_config/fish/functions/codex.fish @@ -1,3 +1,3 @@ function codex --wraps codex - nono run --allow-cwd --allow (git rev-parse --path-format=absolute --git-common-dir 2>/dev/null; or pwd) --profile codex -- codex $argv + nono-agent codex $argv end diff --git a/home/dot_config/fish/functions/opencode.fish b/home/dot_config/fish/functions/opencode.fish index b832256..4e3f1dd 100644 --- a/home/dot_config/fish/functions/opencode.fish +++ b/home/dot_config/fish/functions/opencode.fish @@ -1,3 +1,3 @@ function opencode --wraps opencode - nono run --allow-cwd --allow (git rev-parse --path-format=absolute --git-common-dir 2>/dev/null; or pwd) --profile opencode -- opencode $argv + nono-agent opencode $argv end diff --git a/home/dot_config/fish/functions/pi.fish b/home/dot_config/fish/functions/pi.fish index 660aeda..82b2fcf 100644 --- a/home/dot_config/fish/functions/pi.fish +++ b/home/dot_config/fish/functions/pi.fish @@ -1,3 +1,3 @@ function pi --wraps pi - nono run --allow-cwd --allow (git rev-parse --path-format=absolute --git-common-dir 2>/dev/null; or pwd) --profile pi -- pi $argv + nono-agent pi $argv end diff --git a/home/dot_local/bin/executable_nono-agent b/home/dot_local/bin/executable_nono-agent new file mode 100755 index 0000000..5161db7 --- /dev/null +++ b/home/dot_local/bin/executable_nono-agent @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Launch agents under nono, with a separate command policy for Orca worker mail.""" + +import json +import os +import shlex +import subprocess +import sys +import tempfile +from pathlib import Path + +MAIL_OPTIONS = { + ("orchestration", "ask"): { + "--options": True, + "--question": True, + "--resume": True, + "--timeout-ms": True, + "--wait": False, + }, + ("orchestration", "check"): { + "--all": False, + "--format": False, + "--peek": False, + "--timeout-ms": True, + "--types": True, + "--unread": False, + "--wait": False, + }, + ("orchestration", "send"): { + "--body": True, + "--dispatch-id": True, + "--files-modified": True, + "--outcome": True, + "--phase": True, + "--priority": True, + "--report-path": True, + "--subject": True, + "--task-id": True, + "--type": True, + }, + ("skills", "get", "orca-cli"): { + "--full": False, + "--reference": True, + "--references": False, + }, + ("skills", "get", "orchestration"): { + "--full": False, + "--reference": True, + "--references": False, + }, +} + + +def orca_policy(launcher): + lines = launcher.read_text().splitlines() + runtime = shlex.split(next(line for line in lines if line.startswith("exec "))) + if len(runtime) != 4 or runtime[-1] != "$@": + raise ValueError( + "Unsupported Orca CLI launcher; expected Node and a CLI entry point." + ) + node, cli = (Path(value) for value in runtime[1:3]) + assignment = next( + line for line in lines if line.startswith("export ORCA_USER_DATA_PATH=") + ) + data = Path(shlex.split(assignment)[1].split("=", 1)[1]) + environment = { + "allow_vars": ["HOME", "LANG", "TERM", "XDG_*"], + "set_vars": { + **{ + key: value + for key, value in os.environ.items() + if key.startswith("ORCA_") + }, + "ORCA_USER_DATA_PATH": str(data), + }, + } + return { + "commands": { + "orca": { + "executable": str(Path(sys.executable).resolve()), + "can_use": ["orca-node"], + "from": { + "session": { + "sandbox": { + "argv_prepend": [ + str(Path(__file__).resolve()), + "--orca-mail", + ], + "environment": { + **environment, + "set_vars": { + **environment["set_vars"], + "PYTHONHOME": sys.prefix, + }, + }, + "fs_read": [ + str(Path(sys.executable).resolve().parent.parent) + ], + "fs_read_file": [str(Path(__file__).resolve())], + }, + "invocation_policy": { + "default": "deny", + "allow": [ + {"argv": {"prefix": list(prefix)}} + for prefix in MAIL_OPTIONS + ], + }, + } + }, + }, + "orca-node": { + "executable": str(node), + "from": { + "session": "deny", + "orca": { + "argv_prepend": [str(cli)], + "environment": environment, + "fs_read": [str(node.parent.parent), str(cli.parents[2])], + "fs_read_file": [ + str(data / "orca-runtime.json"), + "/etc/ssl/openssl.cnf", + ], + "unix_socket_bind": [str(data / "*.sock")], + }, + }, + }, + # Keep the launcher's interpreter under the original session policy. + # The execute gate otherwise skips group-writable packaged Python builds. + "python-runtime": { + "executable": str(Path(sys.executable).resolve()), + "allow_direct_exec_bypass": [str(Path(sys.executable).resolve())], + "from": {"session": "deny"}, + }, + } + } + + +def validate_mail(args): + prefix = tuple(args[:3] if args[:2] == ["skills", "get"] else args[:2]) + options = { + "--help": False, + "--json": False, + "--retry-request": True, + } | MAIL_OPTIONS[prefix] + index = len(prefix) + while index < len(args): + name, equals, value = args[index].partition("=") + takes_value = options[name] + if takes_value and not equals: + index += 1 + value = args[index] + if takes_value: + if not value or value.startswith("-"): + raise ValueError( + "Mailbox option values must not start with another option." + ) + if name == "--type" and value not in ( + "escalation", + "heartbeat", + "status", + "worker_done", + ): + raise ValueError( + "Only worker status and completion messages are permitted." + ) + elif equals: + raise ValueError("Mailbox switches do not take values.") + index += 1 + return args + + +def mail(args): + try: + validate_mail(args) + except (IndexError, KeyError, ValueError) as error: + print(f"Orca worker mail denied: {error}", file=sys.stderr) + return 126 + return subprocess.call(["orca-node", *args]) + + +def launch(agent, args): + grants = ["--allow-cwd"] + common = subprocess.run( + ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], + capture_output=True, + text=True, + check=False, + ) + if common.returncode == 0: + grants.extend(["--allow", common.stdout.strip()]) + if agent == "codex": + args = ["--sandbox", "danger-full-access", *args] + elif agent == "claude": + args = ["--settings", json.dumps({"sandbox": {"enabled": False}}), *args] + command = ["nono", "run", *grants] + if sys.platform != "linux" or "ORCA_WORKTREE_ID" not in os.environ: + return subprocess.call([*command, "--profile", agent, "--", agent, *args]) + + profile = { + "extends": agent, + "command_policies": orca_policy(Path(os.environ["ORCA_CLI_COMMAND"])), + "environment": {"set_vars": {"ORCA_CLI_COMMAND": "orca"}}, + } + if common.returncode == 0: + listing = subprocess.check_output( + ["git", "worktree", "list", "--porcelain"], text=True + ) + main = Path( + next( + line.removeprefix("worktree ") + for line in listing.splitlines() + if line.startswith("worktree ") + ) + ) + if (main / "todo.md").is_file(): + profile["filesystem"] = {"read_file": [str(main / "todo.md")]} + + # Keep generated authority outside every agent-writable directory. + state = ( + Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) + / "nono" + / "runtime-profiles" + ) + state.mkdir(parents=True, exist_ok=True, mode=0o700) + with tempfile.TemporaryDirectory(prefix="orca-", dir=state) as directory: + path = Path(directory) / "profile.json" + path.write_text(json.dumps(profile)) + return subprocess.call([*command, "--profile", str(path), "--", agent, *args]) + + +if __name__ == "__main__": + sys.exit( + mail(sys.argv[2:]) + if sys.argv[1] == "--orca-mail" + else launch(sys.argv[1], sys.argv[2:]) + ) diff --git a/tests/nono-agent.test.mjs b/tests/nono-agent.test.mjs new file mode 100644 index 0000000..362934d --- /dev/null +++ b/tests/nono-agent.test.mjs @@ -0,0 +1,144 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const launcher = fileURLToPath( + new URL("../home/dot_local/bin/executable_nono-agent", import.meta.url), +); + +test("agent launch retains nono and removes its temporary Orca authority after exit", () => { + const directory = mkdtempSync(join(tmpdir(), "nono-agent-")); + try { + const bin = join(directory, "bin"); + const main = join(directory, "main"); + mkdirSync(bin); + mkdirSync(main); + writeFileSync(join(main, "todo.md"), "- [ ] Fixture task\n"); + for (const [name, source] of Object.entries({ + git: `const args = process.argv.slice(2); +if (args[0] === "rev-parse") console.log(${JSON.stringify(join(main, ".git"))}); +else console.log(${JSON.stringify(`worktree ${main}\n`)});`, + nono: `const fs = require("node:fs"); +const args = process.argv.slice(2); +const profilePath = args[args.indexOf("--profile") + 1]; +const profile = profilePath === "codex" ? null : JSON.parse(fs.readFileSync(profilePath, "utf8")); +console.log(JSON.stringify({ args, profile, profilePath }));`, + })) { + const path = join(bin, name); + writeFileSync(path, `#!/usr/bin/env node\n${source}\n`); + chmodSync(path, 0o755); + } + const orca = join(directory, "orca"); + writeFileSync( + orca, + `export ORCA_USER_DATA_PATH='${directory}/orca-data'\nexec '/usr/bin/node' '${directory}/orca/out/cli/index.js' "$@"\n`, + ); + const environment = { + ...process.env, + ORCA_CLI_COMMAND: orca, + ORCA_TERMINAL_ID: "bound-worker", + ORCA_WORKTREE_ID: "fixture", + PATH: `${bin}:${process.env.PATH}`, + XDG_CONFIG_HOME: join(directory, "config"), + }; + const run = (agent, args, env = environment) => { + const process = spawnSync("python3", [launcher, agent, ...args], { encoding: "utf8", env }); + assert.equal(process.status, 0, process.stderr); + return JSON.parse(process.stdout); + }; + const codex = run("codex", ["--model", "fixture"]); + assert.deepEqual(codex.args.slice(-6), [ + "--", + "codex", + "--sandbox", + "danger-full-access", + "--model", + "fixture", + ]); + assert.equal(codex.profile.extends, "codex"); + assert.deepEqual(codex.profile.filesystem, { read_file: [join(main, "todo.md")] }); + assert.equal( + codex.profile.command_policies.commands.orca.from.session.invocation_policy.default, + "deny", + ); + assert.equal(existsSync(codex.profilePath), false); + const brokerEnvironment = + codex.profile.command_policies.commands["orca-node"].from.orca.environment; + assert.equal(brokerEnvironment.set_vars.ORCA_TERMINAL_ID, "bound-worker"); + assert.equal(brokerEnvironment.allow_vars.includes("ORCA_*"), false); + const claude = run("claude", []); + assert.deepEqual(JSON.parse(claude.args.at(-1)), { sandbox: { enabled: false } }); + assert.equal(existsSync(claude.profilePath), false); + const ordinaryEnvironment = { ...environment }; + delete ordinaryEnvironment.ORCA_WORKTREE_ID; + assert.equal(run("codex", [], ordinaryEnvironment).profile, null); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("worker mail rejects routing overrides before invoking the privileged CLI", () => { + const directory = mkdtempSync(join(tmpdir(), "nono-mail-")); + try { + const backend = join(directory, "orca-node"); + writeFileSync( + backend, + "#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)));\n", + ); + chmodSync(backend, 0o755); + const run = (args) => + spawnSync("python3", [launcher, "--orca-mail", ...args], { + encoding: "utf8", + env: { ...process.env, PATH: `${directory}:${process.env.PATH}` }, + }); + for (const args of [ + ["orchestration", "check", "--peek", "--json"], + ["orchestration", "ask", "--question=A worker question"], + [ + "orchestration", + "send", + "--type", + "worker_done", + "--outcome", + "succeeded", + "--body", + "Test passed", + ], + ["skills", "get", "orchestration", "--reference", "references/messaging-and-gates.md"], + ]) { + const result = run(args); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), args); + } + for (const args of [ + ["terminal", "list"], + ["orchestration", "send", "--type=dispatch"], + ["orchestration", "ask", "--question", "--terminal=other"], + ["orchestration", "check", "--peek=true"], + ["orchestration", "send", "--body"], + ...[ + "--dispatch-capability", + "--environment", + "--from", + "--pairing-code", + "--run", + "--terminal", + "--to", + ].flatMap((flag) => [ + ["orchestration", "check", flag, "other"], + ["orchestration", "check", `${flag}=other`], + ]), + ]) { + const result = run(args); + assert.equal(result.status, 126, `${args.join(" ")}: ${result.stderr}`); + assert.equal(result.stdout, ""); + assert.match(result.stderr, /Orca worker mail denied/); + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/tests/secret-guard.test.mjs b/tests/secret-guard.test.mjs index 67b83d7..7ba0867 100644 --- a/tests/secret-guard.test.mjs +++ b/tests/secret-guard.test.mjs @@ -20,6 +20,18 @@ const run = (args, input, environment = env) => const post = (stdout, environment) => run(["post", "claude"], JSON.stringify({ tool_response: { stdout } }), environment); +test("permission diagnostics remain available without allowing secret retrieval", () => { + const pre = (command) => run(["pre"], JSON.stringify({ tool_input: { command } })); + assert.equal(pre("nono why --self --path /workspace --op read"), ""); + for (const command of [ + "op read op://vault/item/password", + "/usr/bin/op read op://vault/item/password", + "op inject -i template", + ]) { + assert.throws(() => pre(command), { status: 2 }); + } +}); + test("post redacts secrets and passes clean output through", () => { const redacted = JSON.parse(post(`token=${token}`)); assert.equal(redacted.hookSpecificOutput.updatedToolOutput.stdout, "token=[REDACTED]\n"); From 2dbd503e5cfcedc1d0c882993bd974d4716090e2 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Wed, 11 Nov 2026 07:15:24 +0000 Subject: [PATCH 2/5] Preserve installed tool execution in scoped workers --- home/dot_local/bin/executable_nono-agent | 23 ++++++++++++++++++++++- tests/nono-agent.test.mjs | 8 ++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/home/dot_local/bin/executable_nono-agent b/home/dot_local/bin/executable_nono-agent index 5161db7..3831667 100755 --- a/home/dot_local/bin/executable_nono-agent +++ b/home/dot_local/bin/executable_nono-agent @@ -74,7 +74,28 @@ def orca_policy(launcher): "ORCA_USER_DATA_PATH": str(data), }, } + installed = json.loads( + subprocess.check_output(["mise", "ls", "--installed", "--json"]) + ) + versions = [ + f"{tool}@{version['version']}" + for tool, releases in installed.items() + for version in releases + ] + # Nono's execute gate otherwise only discovers launch-time PATH entries. + directories = subprocess.check_output( + ["mise", "bin-paths", *versions], text=True + ).splitlines() + directories.append( + subprocess.check_output(["git", "--exec-path"], text=True).strip() + ) + directories.extend( + str(Path(release["install_path"]) / "codex-path") + for release in installed.get("codex", []) + if (Path(release["install_path"]) / "codex-path").is_dir() + ) return { + "executable_dirs": sorted(set(directories)), "commands": { "orca": { "executable": str(Path(sys.executable).resolve()), @@ -131,7 +152,7 @@ def orca_policy(launcher): "allow_direct_exec_bypass": [str(Path(sys.executable).resolve())], "from": {"session": "deny"}, }, - } + }, } diff --git a/tests/nono-agent.test.mjs b/tests/nono-agent.test.mjs index 362934d..635ae32 100644 --- a/tests/nono-agent.test.mjs +++ b/tests/nono-agent.test.mjs @@ -21,7 +21,11 @@ test("agent launch retains nono and removes its temporary Orca authority after e for (const [name, source] of Object.entries({ git: `const args = process.argv.slice(2); if (args[0] === "rev-parse") console.log(${JSON.stringify(join(main, ".git"))}); +else if (args[0] === "--exec-path") console.log(${JSON.stringify(join(directory, "git-core"))}); else console.log(${JSON.stringify(`worktree ${main}\n`)});`, + mise: `const args = process.argv.slice(2); +if (args[0] === "ls") console.log(JSON.stringify({ node: [{ version: "24", install_path: ${JSON.stringify(join(directory, "node"))} }] })); +else console.log(${JSON.stringify(join(directory, "node/bin"))});`, nono: `const fs = require("node:fs"); const args = process.argv.slice(2); const profilePath = args[args.indexOf("--profile") + 1]; @@ -60,6 +64,10 @@ console.log(JSON.stringify({ args, profile, profilePath }));`, "fixture", ]); assert.equal(codex.profile.extends, "codex"); + assert.deepEqual(codex.profile.command_policies.executable_dirs, [ + join(directory, "git-core"), + join(directory, "node/bin"), + ]); assert.deepEqual(codex.profile.filesystem, { read_file: [join(main, "todo.md")] }); assert.equal( codex.profile.command_policies.commands.orca.from.session.invocation_policy.default, From bbdfb6138a7a7e2a160805bbd168046e7d4eded0 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Wed, 11 Nov 2026 23:07:24 +0000 Subject: [PATCH 3/5] Select agent permission modes for the sandbox boundary --- README.md | 7 ++++++- home/.chezmoitemplates/codex.toml | 2 ++ home/dot_local/bin/executable_nono-agent | 11 +++++++++-- tests/nono-agent.test.mjs | 10 +++++++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b8e9c63..1b3f0c8 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ Start an agent in `~/Projects` to work across projects. Pull each upstream profile once with `nono pull nolabs-ai/AGENT`; Mise does not update them, so check with `nono outdated` and run `nono update`, then `chezmoi apply`: the Codex template moves nono's Codex settings out of the block nono inserts. Nono is the -sandbox boundary; the launcher disables Codex's and Claude Code's nested sandboxes. +sandbox boundary. Inside it, Codex and Claude Code bypass their own approval +prompts and nested sandboxes; OpenCode uses its supported `--auto` mode, which +retains explicit denies. Pi has no tool approval mode. Outside nono, Codex defaults +to workspace-write with automatic approval review and Claude Code to `auto`. +Use `command opencode --auto` for OpenCode without nono; it has no persistent +auto-approval setting. Sandboxed agents read only the shared Git configuration and the `GitHub Agents` gh configuration, so they commit unsigned and push over HTTPS. They cannot reach diff --git a/home/.chezmoitemplates/codex.toml b/home/.chezmoitemplates/codex.toml index 5fb65b1..df6c1d2 100644 --- a/home/.chezmoitemplates/codex.toml +++ b/home/.chezmoitemplates/codex.toml @@ -1,7 +1,9 @@ +approval_policy = "on-request" approvals_reviewer = "auto_review" check_for_update_on_startup = false model_reasoning_effort = "medium" plan_mode_reasoning_effort = "medium" +sandbox_mode = "workspace-write" service_tier = "default" suppress_unstable_features_warning = true diff --git a/home/dot_local/bin/executable_nono-agent b/home/dot_local/bin/executable_nono-agent index 3831667..34546b6 100755 --- a/home/dot_local/bin/executable_nono-agent +++ b/home/dot_local/bin/executable_nono-agent @@ -210,9 +210,16 @@ def launch(agent, args): if common.returncode == 0: grants.extend(["--allow", common.stdout.strip()]) if agent == "codex": - args = ["--sandbox", "danger-full-access", *args] + args = ["--dangerously-bypass-approvals-and-sandbox", *args] elif agent == "claude": - args = ["--settings", json.dumps({"sandbox": {"enabled": False}}), *args] + args = [ + "--dangerously-skip-permissions", + "--settings", + json.dumps({"sandbox": {"enabled": False}}), + *args, + ] + elif agent == "opencode": + args = ["--auto", *args] command = ["nono", "run", *grants] if sys.platform != "linux" or "ORCA_WORKTREE_ID" not in os.environ: return subprocess.call([*command, "--profile", agent, "--", agent, *args]) diff --git a/tests/nono-agent.test.mjs b/tests/nono-agent.test.mjs index 635ae32..2142ccd 100644 --- a/tests/nono-agent.test.mjs +++ b/tests/nono-agent.test.mjs @@ -55,11 +55,10 @@ console.log(JSON.stringify({ args, profile, profilePath }));`, return JSON.parse(process.stdout); }; const codex = run("codex", ["--model", "fixture"]); - assert.deepEqual(codex.args.slice(-6), [ + assert.deepEqual(codex.args.slice(-5), [ "--", "codex", - "--sandbox", - "danger-full-access", + "--dangerously-bypass-approvals-and-sandbox", "--model", "fixture", ]); @@ -80,7 +79,12 @@ console.log(JSON.stringify({ args, profile, profilePath }));`, assert.equal(brokerEnvironment.allow_vars.includes("ORCA_*"), false); const claude = run("claude", []); assert.deepEqual(JSON.parse(claude.args.at(-1)), { sandbox: { enabled: false } }); + assert.ok(claude.args.includes("--dangerously-skip-permissions")); assert.equal(existsSync(claude.profilePath), false); + const opencode = run("opencode", []); + assert.deepEqual(opencode.args.slice(-3), ["--", "opencode", "--auto"]); + const pi = run("pi", []); + assert.deepEqual(pi.args.slice(-2), ["--", "pi"]); const ordinaryEnvironment = { ...environment }; delete ordinaryEnvironment.ORCA_WORKTREE_ID; assert.equal(run("codex", [], ordinaryEnvironment).profile, null); From 6b4f1234967a80e3ee364acf99152a828c98642c Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Wed, 11 Nov 2026 23:09:12 +0000 Subject: [PATCH 4/5] Document Orca permission inheritance for sandboxed launchers --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b3f0c8..49d86b4 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,11 @@ for work that needs those, such as OpenTofu plans and applies, host work, Kubernetes and Talos administration, chezmoi and Xcode builds. Orca launches agents through these wrappers when its agent command for each one -is set to `fish -c "AGENT $argv" --`. On Linux, the launcher gives Orca workers a +is set to `fish -c "AGENT $argv" --` and its permission mode is **Own**. +Own lets the wrapper select the mode; Orca's Ask and Auto presets add approval +flags that conflict with Codex's sandboxed bypass flag. Use **Auto** for native +chats and terminal agents launched without these wrappers. +On Linux, the launcher gives Orca workers a separate nono command policy for mailbox operations and the two Orca skill guides. Only the brokered CLI receives its runtime metadata and socket access; terminal control and foreign-mailbox routing stay denied. The policy is generated from the From d445362d0b6fe052829447ad501c7e8a0efcd732 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Thu, 12 Nov 2026 02:18:56 +0000 Subject: [PATCH 5/5] Preserve existing Codex bypass arguments --- README.md | 7 ++++--- home/dot_local/bin/executable_nono-agent | 6 +++++- tests/nono-agent.test.mjs | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49d86b4..f0e77a1 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,10 @@ Kubernetes and Talos administration, chezmoi and Xcode builds. Orca launches agents through these wrappers when its agent command for each one is set to `fish -c "AGENT $argv" --` and its permission mode is **Own**. -Own lets the wrapper select the mode; Orca's Ask and Auto presets add approval -flags that conflict with Codex's sandboxed bypass flag. Use **Auto** for native -chats and terminal agents launched without these wrappers. +Own lets the wrapper select the mode. An existing Codex Full Access flag is +preserved without duplication; Orca's Ask and Auto presets add conflicting +approval flags. Use **Auto** for native chats and terminal agents launched +without these wrappers. On Linux, the launcher gives Orca workers a separate nono command policy for mailbox operations and the two Orca skill guides. Only the brokered CLI receives its runtime metadata and socket access; terminal diff --git a/home/dot_local/bin/executable_nono-agent b/home/dot_local/bin/executable_nono-agent index 34546b6..bdc56a5 100755 --- a/home/dot_local/bin/executable_nono-agent +++ b/home/dot_local/bin/executable_nono-agent @@ -209,7 +209,11 @@ def launch(agent, args): ) if common.returncode == 0: grants.extend(["--allow", common.stdout.strip()]) - if agent == "codex": + if ( + agent == "codex" + and "--dangerously-bypass-approvals-and-sandbox" not in args + and "--yolo" not in args + ): args = ["--dangerously-bypass-approvals-and-sandbox", *args] elif agent == "claude": args = [ diff --git a/tests/nono-agent.test.mjs b/tests/nono-agent.test.mjs index 2142ccd..f149281 100644 --- a/tests/nono-agent.test.mjs +++ b/tests/nono-agent.test.mjs @@ -62,6 +62,11 @@ console.log(JSON.stringify({ args, profile, profilePath }));`, "--model", "fixture", ]); + const bypassFlags = ["--dangerously-bypass-approvals-and-sandbox", "--yolo"]; + for (const flag of bypassFlags) { + const fullAccess = run("codex", [flag, "--model", "fixture"]); + assert.equal(fullAccess.args.filter((arg) => bypassFlags.includes(arg)).length, 1); + } assert.equal(codex.profile.extends, "codex"); assert.deepEqual(codex.profile.command_policies.executable_dirs, [ join(directory, "git-core"),