cdx-manager 0.24.4 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_24_5.md +16 -0
- package/changelogs/CHANGELOGS_0_24_6.md +16 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli_args.py +6 -1
- package/src/commands/runs.py +5 -4
- package/src/provider_runtime.py +27 -0
- package/src/run_command.py +1 -1
- package/src/workspace_sandbox.py +145 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.24.
|
|
1
|
+
0.24.6
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CDX Manager 0.24.5
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-29
|
|
4
|
+
|
|
5
|
+
This patch adds isolated workspace runs with opt-in network access. (#61)
|
|
6
|
+
|
|
7
|
+
## Workspace runs
|
|
8
|
+
|
|
9
|
+
- Add `cdx run --isolate-workspace` for Linux workspace confinement through bubblewrap, exposing the checkout, permitted toolchains, shared caches, and provider authentication while keeping the operator's home and host processes outside the boundary. (#61)
|
|
10
|
+
- Add `--network-access` to enable Codex's workspace-write network setting while retaining its workspace sandbox. (#61)
|
|
11
|
+
- Preserve both options across detached runs and failover, and advertise `workspace_isolation` and `workspace_network` capabilities in `cdx schema`. (#61)
|
|
12
|
+
|
|
13
|
+
## Maintenance
|
|
14
|
+
|
|
15
|
+
- Record tray companion checksums for v0.24.4. (`7465b2e8244e90032d1ab8b7ad7af6a26ffe805c`)
|
|
16
|
+
- Record release archive checksums for v0.24.4. (`a0afa21ccc816769fb0aac5883912efd90bf1028`)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CDX Manager 0.24.6
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-29
|
|
4
|
+
|
|
5
|
+
This patch gives isolated online workspace runs their own egress-only network. (#63)
|
|
6
|
+
|
|
7
|
+
## Workspace runs
|
|
8
|
+
|
|
9
|
+
- Use a dedicated pasta network namespace for online `--isolate-workspace` runs, including provider transport, with port relays and host gateway mapping disabled. (#63)
|
|
10
|
+
- Apply nftables output filtering to reject loopback, private, link-local, and tailnet destination ranges, and use public DNS resolvers. (#63)
|
|
11
|
+
- Fail closed when pasta (passt), nftables, or Python 3 is unavailable, without falling back to the host network. (#63)
|
|
12
|
+
|
|
13
|
+
## Maintenance
|
|
14
|
+
|
|
15
|
+
- Record tray companion checksums for v0.24.5. (`78ba34dd71a19ac392feda771737705a72c765e5`)
|
|
16
|
+
- Record release archive checksums for v0.24.5. (`f835adb7acc495133438c6e719fa452843109d58`)
|
|
@@ -617,6 +617,28 @@
|
|
|
617
617
|
"x86_64-pc-windows-msvc": "6da838217c01251b809bac1e188217dcfbb00c2102b901cfda43cf2fc0696aa2",
|
|
618
618
|
"x86_64-unknown-linux-musl": "c6651dd909d486c2b0bb209e0d813ab044dc7b5ffb1a07078c8ec7a69126e028"
|
|
619
619
|
}
|
|
620
|
+
},
|
|
621
|
+
"v0.24.4": {
|
|
622
|
+
"github_tarball_sha256": "279d60a1073f56ac1ccc361248c14668e9e018d81dd56e4eb177cb3f4bac4cf6",
|
|
623
|
+
"github_zip_sha256": "05dd935dc0e8e686a1dce96c0543b91757995a5f49ae2aed1155ddf97916e55d",
|
|
624
|
+
"tray_assets": {
|
|
625
|
+
"aarch64-apple-darwin": "9fdcddf98e9723f2af2007f0c0a183644b2200e599c33d354c097e2510a3901b",
|
|
626
|
+
"aarch64-unknown-linux-musl": "eef61a80c0d1107de9dfb980fb5914d85ec3b0a5d640ff559bf8e7faa4e857bd",
|
|
627
|
+
"x86_64-apple-darwin": "3296ee96b17442d8a2d8eaee6ceeba90dc498c74168973a6932793fa509afc66",
|
|
628
|
+
"x86_64-pc-windows-msvc": "1fd621d4ba7175c868c4e598850c5f5ccd914c91aaef3d79024376082e8fe81d",
|
|
629
|
+
"x86_64-unknown-linux-musl": "f4c3a6e7c2af75cd8214609971fe2cbef6f34e818796d70b661d9a9f7e9327af"
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
"v0.24.5": {
|
|
633
|
+
"github_tarball_sha256": "0e34b1c67f3357d1551a2cce1187efb84cfb737163e91b3ae4e448f9bd2fee08",
|
|
634
|
+
"github_zip_sha256": "0f1242f234392f003fe6ed61206d4b931d72887f875cd0a60b20199a2f5745a6",
|
|
635
|
+
"tray_assets": {
|
|
636
|
+
"aarch64-apple-darwin": "6847d3f6b66137157896ec901b47a3b48913f78390b3fff50d1d59c85151e748",
|
|
637
|
+
"aarch64-unknown-linux-musl": "6f167515cdc0914b1d04ceee0ea60e28de57d2321d4073912409a6d9266c6185",
|
|
638
|
+
"x86_64-apple-darwin": "16a9bfc33fe03b771bdb7ffd417ce222785062f342c1c599bdda2a3fc7200841",
|
|
639
|
+
"x86_64-pc-windows-msvc": "74124b2e40a58a58b55724ef453c4b543a83eefab9a5f92685b239c5d97e7f67",
|
|
640
|
+
"x86_64-unknown-linux-musl": "089e552a4dfae4080a2e76cedb6fe72fc8013a9cd08cf3c8b333f916198da57c"
|
|
641
|
+
}
|
|
620
642
|
}
|
|
621
643
|
}
|
|
622
644
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli_args.py
CHANGED
|
@@ -47,7 +47,7 @@ CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
|
|
|
47
47
|
SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
48
48
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
49
49
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
50
|
-
RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json"
|
|
50
|
+
RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json"
|
|
51
51
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
52
52
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
53
53
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
@@ -475,6 +475,7 @@ def cdx_schema():
|
|
|
475
475
|
# No `schema_version` here: the JSON envelope already carries it, and two
|
|
476
476
|
# keys of that name in one payload would be one too many.
|
|
477
477
|
return {
|
|
478
|
+
"capabilities": {"workspace_isolation": 1, "workspace_network": 1},
|
|
478
479
|
"enums": {
|
|
479
480
|
"permission": {
|
|
480
481
|
"accepted": list(RUN_PERMISSION_VALUES),
|
|
@@ -665,6 +666,8 @@ def _parse_run_args(args):
|
|
|
665
666
|
"--permission": {"key": "permission", "type": "str", "default": None, "transform": _normalize_run_permission},
|
|
666
667
|
"--timeout-seconds": {"key": "timeout_seconds", "type": "str", "default": None, "transform": _parse_run_timeout_seconds},
|
|
667
668
|
"--detach": {"key": "detach", "type": "bool", "default": False},
|
|
669
|
+
"--network-access": {"key": "network_access", "type": "bool", "default": False},
|
|
670
|
+
"--isolate-workspace": {"key": "isolate_workspace", "type": "bool", "default": False},
|
|
668
671
|
"--failover": {"key": "failover", "type": "bool", "default": False},
|
|
669
672
|
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
670
673
|
"--label": {"key": "label", "type": "str", "default": None},
|
|
@@ -716,6 +719,8 @@ def _parse_run_args(args):
|
|
|
716
719
|
"permission": parsed["permission"],
|
|
717
720
|
"timeout_seconds": parsed["timeout_seconds"],
|
|
718
721
|
"detach": parsed["detach"],
|
|
722
|
+
"network_access": parsed["network_access"],
|
|
723
|
+
"isolate_workspace": parsed["isolate_workspace"],
|
|
719
724
|
"failover": parsed["failover"],
|
|
720
725
|
"label": parsed["label"],
|
|
721
726
|
"max_parallel": parsed["max_parallel"],
|
package/src/commands/runs.py
CHANGED
|
@@ -206,8 +206,9 @@ def _detached_child_argv(parsed, prompt_path, session_name, cwd):
|
|
|
206
206
|
argv += ["--timeout-seconds", str(parsed["timeout_seconds"])]
|
|
207
207
|
# The child is the supervisor: it watches the run like any blocking one, so
|
|
208
208
|
# it applies the failover rule and records each move in the registry.
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
for key in ("network_access", "isolate_workspace", "failover"):
|
|
210
|
+
if parsed.get(key):
|
|
211
|
+
argv.append("--" + key.replace("_", "-"))
|
|
211
212
|
argv.append("--json")
|
|
212
213
|
return argv
|
|
213
214
|
|
|
@@ -572,7 +573,7 @@ def handle_run(rest, ctx):
|
|
|
572
573
|
raise CdxError(f"Invalid cwd: {parsed['cwd']}", code="invalid_cwd")
|
|
573
574
|
prompt = read_run_prompt(parsed, stdin=ctx.get("prompt_stdin"))
|
|
574
575
|
_consume_detached_prompt_file(parsed, detached_run_id)
|
|
575
|
-
launch_updates = {}
|
|
576
|
+
launch_updates = {key: parsed.get(key, False) for key in ("network_access", "isolate_workspace")}
|
|
576
577
|
if parsed.get("model"):
|
|
577
578
|
launch_updates["model"] = parsed["model"]
|
|
578
579
|
if parsed.get("permission"):
|
|
@@ -643,7 +644,7 @@ def handle_run(rest, ctx):
|
|
|
643
644
|
launch = None
|
|
644
645
|
# A provider's own background agent has no cdx process watching it,
|
|
645
646
|
# so nothing could fail it over; --failover keeps the in-house child.
|
|
646
|
-
if not parsed.get("failover") and supports_native_background(
|
|
647
|
+
if not parsed.get("isolate_workspace") and not parsed.get("network_access") and not parsed.get("failover") and supports_native_background(
|
|
647
648
|
run_session.get("provider"),
|
|
648
649
|
env=ctx.get("env"),
|
|
649
650
|
spawn_sync=ctx.get("spawn_text"),
|
package/src/provider_runtime.py
CHANGED
|
@@ -1210,6 +1210,33 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
1210
1210
|
env_override=env_override,
|
|
1211
1211
|
initial_prompt=initial_prompt,
|
|
1212
1212
|
)
|
|
1213
|
+
launch = session.get("launch") or {}
|
|
1214
|
+
if launch.get("network_access"):
|
|
1215
|
+
if session.get("provider") == PROVIDER_CODEX:
|
|
1216
|
+
# Keep the workspace sandbox; append overrides before the prompt separator.
|
|
1217
|
+
index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
|
|
1218
|
+
spec["args"][index:index] = ["-c", 'sandbox_workspace_write.network_access=true', '-c', 'sandbox_workspace_write.writable_roots=["/cache", "/home/agent"]']
|
|
1219
|
+
elif session.get("provider") != PROVIDER_CLAUDE:
|
|
1220
|
+
raise CdxError("Network override is unsupported for this provider")
|
|
1221
|
+
if launch.get("isolate_workspace"):
|
|
1222
|
+
from pathlib import Path
|
|
1223
|
+
|
|
1224
|
+
from .workspace_sandbox import sandbox_command
|
|
1225
|
+
if session.get("provider") == PROVIDER_CLAUDE:
|
|
1226
|
+
# The outer OS sandbox is the permission boundary for unattended Bash.
|
|
1227
|
+
index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
|
|
1228
|
+
spec["args"][index:index] = ["--permission-mode", "bypassPermissions"]
|
|
1229
|
+
try:
|
|
1230
|
+
argv, child_env = sandbox_command(
|
|
1231
|
+
[spec["command"], *spec["args"]], Path(cwd),
|
|
1232
|
+
network=launch.get("network_access", False),
|
|
1233
|
+
writable=launch.get("permission") != "review",
|
|
1234
|
+
source=spec["options"]["env"], auth_home=_get_auth_home(session),
|
|
1235
|
+
provider=session.get("provider"))
|
|
1236
|
+
except (OSError, RuntimeError) as error:
|
|
1237
|
+
raise CdxError(str(error)) from error
|
|
1238
|
+
spec.update(command=argv[0], args=argv[1:])
|
|
1239
|
+
spec["options"]["env"] = child_env
|
|
1213
1240
|
paths = _headless_artifact_paths(session, run_id=run_id)
|
|
1214
1241
|
start_time = datetime.now(timezone.utc)
|
|
1215
1242
|
command = spec["command"]
|
package/src/run_command.py
CHANGED
|
@@ -133,7 +133,7 @@ def run_warnings(parsed, session, selection_row=None):
|
|
|
133
133
|
})
|
|
134
134
|
provider = session.get("provider") if session else parsed.get("provider")
|
|
135
135
|
permission = run_effective_permission(parsed, session)
|
|
136
|
-
if headless_permission_disables_network(provider, permission):
|
|
136
|
+
if not parsed.get("network_access") and headless_permission_disables_network(provider, permission):
|
|
137
137
|
described = repr(permission) if permission else "its default (none set)"
|
|
138
138
|
warnings.append({
|
|
139
139
|
"code": "network_disabled_by_permission",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Linux workspace confinement shared with the cdx-manager integration patch.
|
|
2
|
+
|
|
3
|
+
Expose tools, caches and one checkout, never the operator's home or host processes.
|
|
4
|
+
Provider authentication is the only credential deliberately passed to the provider.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
CACHE = Path.home() / ".cdx" / "shared-cache"
|
|
13
|
+
|
|
14
|
+
# Installed before any untrusted command, in the namespace owned by pasta.
|
|
15
|
+
NETWORK_RULES = """table inet mission_egress {
|
|
16
|
+
chain output {
|
|
17
|
+
type filter hook output priority 0; policy accept;
|
|
18
|
+
oif "lo" accept
|
|
19
|
+
ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
|
|
20
|
+
169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16 } reject
|
|
21
|
+
ip6 daddr { ::1, fc00::/7, fe80::/10 } reject
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
PUBLIC_RESOLV_CONF = "nameserver 1.1.1.1\nnameserver 9.9.9.9\n"
|
|
26
|
+
|
|
27
|
+
# Keep the trusted setup outside the agent's filesystem/PID/user namespaces.
|
|
28
|
+
NETWORK_SETUP = """
|
|
29
|
+
import pathlib
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
try:
|
|
34
|
+
subprocess.run([sys.argv[1], "-f", "-"], input=RULES, text=True,
|
|
35
|
+
check=True, timeout=15)
|
|
36
|
+
with tempfile.TemporaryDirectory(prefix="mission-dns-") as directory:
|
|
37
|
+
resolver = pathlib.Path(directory) / "resolv.conf"
|
|
38
|
+
resolver.write_text(RESOLVER)
|
|
39
|
+
args = sys.argv[2:]
|
|
40
|
+
index = args.index("--")
|
|
41
|
+
args[index:index] = ["--ro-bind", str(resolver), "/etc/resolv.conf"]
|
|
42
|
+
status = subprocess.call(args)
|
|
43
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
44
|
+
print("Isolated mission network setup failed: " + str(error), file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
sys.exit(status)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clean_env(source: dict) -> dict:
|
|
51
|
+
allowed = ("PATH", "LANG", "LC_ALL", "TERM", "TZ")
|
|
52
|
+
env = {key: source[key] for key in allowed if key in source}
|
|
53
|
+
env.update(HOME="/home/agent", XDG_CONFIG_HOME="/home/agent/.config",
|
|
54
|
+
XDG_CACHE_HOME="/cache", GIT_CONFIG_NOSYSTEM="1",
|
|
55
|
+
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0", GIT_OPTIONAL_LOCKS="0",
|
|
56
|
+
GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="credential.helper", GIT_CONFIG_VALUE_0="",
|
|
57
|
+
GH_CONFIG_DIR="/home/agent/.config/gh", NPM_CONFIG_CACHE="/cache/npm",
|
|
58
|
+
PIP_CACHE_DIR="/cache/pip", UV_CACHE_DIR="/cache/uv",
|
|
59
|
+
PLAYWRIGHT_BROWSERS_PATH="/cache/playwright", CI="1")
|
|
60
|
+
return env
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sandbox_command(command: list[str], tree: Path, *, network: bool, writable: bool = True,
|
|
64
|
+
source: dict | None = None, auth_home: str = "", provider: str = "") -> tuple[list[str], dict]:
|
|
65
|
+
source = os.environ if source is None else source
|
|
66
|
+
binary = shutil.which(command[0], path=source.get("PATH"))
|
|
67
|
+
bwrap = shutil.which("bwrap", path=source.get("PATH"))
|
|
68
|
+
if not binary or not bwrap:
|
|
69
|
+
raise RuntimeError("Workspace isolation requires bubblewrap and " + command[0])
|
|
70
|
+
if tree.is_symlink() or not tree.is_dir():
|
|
71
|
+
raise RuntimeError("Workspace must be a directory")
|
|
72
|
+
tree = tree.resolve()
|
|
73
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
# Provider transport uses the same restricted egress, even for read-only tools.
|
|
75
|
+
online = bool(network or provider)
|
|
76
|
+
if online:
|
|
77
|
+
pasta = shutil.which("pasta", path=source.get("PATH"))
|
|
78
|
+
nft = shutil.which("nft", path=os.defpath + ":/usr/sbin:/sbin")
|
|
79
|
+
python = shutil.which("python3", path=source.get("PATH"))
|
|
80
|
+
if not pasta or not nft or not python:
|
|
81
|
+
raise RuntimeError("Isolated mission networking requires pasta (passt), nftables and python3; no shared-network fallback")
|
|
82
|
+
args = [bwrap, "--die-with-parent", "--new-session"]
|
|
83
|
+
# pasta already created the network namespace. A new child user namespace and
|
|
84
|
+
# dropped capabilities prevent the agent from changing its ancestor's nft rules.
|
|
85
|
+
args += (["--unshare-user", "--unshare-ipc", "--unshare-pid", "--unshare-uts", "--unshare-cgroup"]
|
|
86
|
+
if online else ["--unshare-all"])
|
|
87
|
+
args += ["--cap-drop", "ALL"]
|
|
88
|
+
args += ["--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/home/agent"]
|
|
89
|
+
# An allowlist avoids overlooking another secret file or a credential symlink.
|
|
90
|
+
paths = [Path(p) for p in ("/usr", "/bin", "/sbin", "/lib", "/lib64")]
|
|
91
|
+
home = Path.home()
|
|
92
|
+
paths += [home / ".nvm", home / ".local/bin", home / ".local/share/claude",
|
|
93
|
+
home / ".local/share/uv"]
|
|
94
|
+
for path in paths:
|
|
95
|
+
if path.exists():
|
|
96
|
+
args += ["--ro-bind", str(path), str(path)]
|
|
97
|
+
for path in ("/etc/ssl", "/etc/resolv.conf", "/etc/hosts", "/etc/nsswitch.conf", "/etc/ld.so.cache",
|
|
98
|
+
"/etc/fonts", "/etc/passwd", "/etc/group", "/etc/alternatives"):
|
|
99
|
+
if Path(path).exists() and not (online and path == "/etc/resolv.conf"):
|
|
100
|
+
args += ["--ro-bind", path, path]
|
|
101
|
+
# Fail closed for a provider installed outside the explicitly permitted toolchains.
|
|
102
|
+
resolved = Path(binary).resolve()
|
|
103
|
+
if not any(resolved.is_relative_to(p.resolve()) for p in paths if p.exists()):
|
|
104
|
+
raise RuntimeError("Executable is outside the permitted toolchains: " + str(resolved))
|
|
105
|
+
args += ["--bind", str(CACHE), "/cache", "--bind" if writable else "--ro-bind", str(tree), str(tree)]
|
|
106
|
+
marker = tree / ".git"
|
|
107
|
+
if marker.is_file():
|
|
108
|
+
text = marker.read_text().strip()
|
|
109
|
+
if marker.is_symlink() or not text.startswith("gitdir: "):
|
|
110
|
+
raise RuntimeError("Invalid worktree Git marker")
|
|
111
|
+
gitdir = (tree / text.removeprefix("gitdir: ")).resolve()
|
|
112
|
+
common = (gitdir / (gitdir / "commondir").read_text().strip()).resolve()
|
|
113
|
+
# A writable marker must never turn into an arbitrary host-directory mount.
|
|
114
|
+
if (common.name != ".git" or gitdir.parent != common / "worktrees"
|
|
115
|
+
or common.is_relative_to(tree) or common.is_relative_to(CACHE.resolve())
|
|
116
|
+
or Path((gitdir / "gitdir").read_text().strip()).resolve() != marker):
|
|
117
|
+
raise RuntimeError("Invalid external worktree Git directory")
|
|
118
|
+
args += ["--ro-bind", str(marker), str(marker), "--ro-bind", str(common), str(common),
|
|
119
|
+
"--tmpfs", str(common / "worktrees"), "--ro-bind", str(gitdir), str(gitdir),
|
|
120
|
+
"--ro-bind", "/dev/null", str(common / "config"),
|
|
121
|
+
"--tmpfs", str(common / "hooks")]
|
|
122
|
+
if (gitdir / "config.worktree").exists():
|
|
123
|
+
args += ["--ro-bind", "/dev/null", str(gitdir / "config.worktree")]
|
|
124
|
+
elif marker.exists():
|
|
125
|
+
raise RuntimeError("Expected an isolated git worktree")
|
|
126
|
+
env = clean_env(source)
|
|
127
|
+
if provider == "codex":
|
|
128
|
+
env["CODEX_HOME"] = "/home/agent/.codex"
|
|
129
|
+
args += ["--ro-bind", str(Path(auth_home) / "auth.json"), "/home/agent/.codex/auth.json"]
|
|
130
|
+
elif provider == "claude":
|
|
131
|
+
token = source.get("CLAUDE_CODE_OAUTH_TOKEN")
|
|
132
|
+
if not token:
|
|
133
|
+
raise RuntimeError("Isolated Claude runs require a cdx OAuth setup token")
|
|
134
|
+
env.update(CLAUDE_CODE_OAUTH_TOKEN=token, CLAUDE_CODE_SUBPROCESS_ENV_SCRUB="1",
|
|
135
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1")
|
|
136
|
+
elif provider:
|
|
137
|
+
raise RuntimeError("Unsupported isolated provider: " + provider)
|
|
138
|
+
args += ["--chdir", str(tree), "--", binary, *command[1:]]
|
|
139
|
+
if online:
|
|
140
|
+
setup = "RULES = " + repr(NETWORK_RULES) + "\nRESOLVER = " + repr(PUBLIC_RESOLV_CONF) + "\n" + NETWORK_SETUP
|
|
141
|
+
# Disable both directions explicitly: automatic host-loopback splicing
|
|
142
|
+
# bypasses output filtering unless -T/-U are also disabled.
|
|
143
|
+
args = [pasta, "--config-net", "--no-map-gw", "-t", "none", "-u", "none",
|
|
144
|
+
"-T", "none", "-U", "none", "-q", "--", python, "-I", "-c", setup, nft, *args]
|
|
145
|
+
return args, env
|