cdx-manager 0.24.5 → 0.24.7

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <br clear="left"/>
6
6
 
7
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.24.5-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
7
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.24.7-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
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.5
1
+ 0.24.7
@@ -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`)
@@ -0,0 +1,15 @@
1
+ # CDX Manager 0.24.7
2
+
3
+ Release date: 2026-10-29
4
+
5
+ This patch fixes command execution in isolated Codex workspace runs. (#65)
6
+
7
+ ## Workspace runs
8
+
9
+ - Drop Codex's nested sandbox when `--isolate-workspace` is enabled, avoiding the user namespace mapping failure that prevented commands from running. The outer workspace sandbox remains the isolation boundary. (#65)
10
+ - Remove Codex's sandbox mode and network overrides inside the isolated workspace and pass `--dangerously-bypass-approvals-and-sandbox`; the outer sandbox controls network access and keeps review runs read-only through its workspace mount. (#65)
11
+
12
+ ## Maintenance
13
+
14
+ - Record tray companion checksums for v0.24.6. (`87aba5d2cffdfb1a96e386abcdabe54df9ee4808`)
15
+ - Record release archive checksums for v0.24.6. (`6b92094143ea28bf0cc096aef88d1015565aba18`)
@@ -628,6 +628,28 @@
628
628
  "x86_64-pc-windows-msvc": "1fd621d4ba7175c868c4e598850c5f5ccd914c91aaef3d79024376082e8fe81d",
629
629
  "x86_64-unknown-linux-musl": "f4c3a6e7c2af75cd8214609971fe2cbef6f34e818796d70b661d9a9f7e9327af"
630
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
+ }
642
+ },
643
+ "v0.24.6": {
644
+ "github_tarball_sha256": "4b61861ae26fc7904e751571b65d0783c16dcf35d3b485379434469e27511fb4",
645
+ "github_zip_sha256": "6c5cfa2139eb53a20a8eca8624437a06b18392b0df7774d9c7c64a74b8066f59",
646
+ "tray_assets": {
647
+ "aarch64-apple-darwin": "7200f658e3413b2614a7ffa5d03e1dac173fc1a653dc659d92f2bef40f2bf5d4",
648
+ "aarch64-unknown-linux-musl": "b892a9a3bf009e5069dc798ab910b20e77ac9d03b9b575a80cb19af1dc13533a",
649
+ "x86_64-apple-darwin": "d1e1b619feab8eae1e2a25e17942d8d690fc1e6184f294599a15858a1f0c146e",
650
+ "x86_64-pc-windows-msvc": "0d125d8e0f7d9880a2ed9e0a794964f3d69ef3353b90a1d594b17f9392a0a0af",
651
+ "x86_64-unknown-linux-musl": "d6c369ace10a6d373366cd12e4e6cd7f2fd278fbce2fd9a13c5bf51047847441"
652
+ }
631
653
  }
632
654
  }
633
655
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.24.5",
3
+ "version": "0.24.7",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.24.5"
7
+ version = "0.24.7"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1212,7 +1212,10 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
1212
1212
  )
1213
1213
  launch = session.get("launch") or {}
1214
1214
  if launch.get("network_access"):
1215
- if session.get("provider") == PROVIDER_CODEX:
1215
+ if session.get("provider") == PROVIDER_CODEX and launch.get("isolate_workspace"):
1216
+ # The outer sandbox owns the network; Codex's own sandbox is dropped below.
1217
+ pass
1218
+ elif session.get("provider") == PROVIDER_CODEX:
1216
1219
  # Keep the workspace sandbox; append overrides before the prompt separator.
1217
1220
  index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
1218
1221
  spec["args"][index:index] = ["-c", 'sandbox_workspace_write.network_access=true', '-c', 'sandbox_workspace_write.writable_roots=["/cache", "/home/agent"]']
@@ -1226,6 +1229,17 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
1226
1229
  # The outer OS sandbox is the permission boundary for unattended Bash.
1227
1230
  index = spec["args"].index("--") if "--" in spec["args"] else len(spec["args"])
1228
1231
  spec["args"][index:index] = ["--permission-mode", "bypassPermissions"]
1232
+ elif session.get("provider") == PROVIDER_CODEX:
1233
+ # Codex's bubblewrap sandbox cannot nest inside ours (no uid map), so the
1234
+ # outer box is the boundary: drop `-s <mode>` and bypass the inner one.
1235
+ # A review run stays read-only through the outer mount.
1236
+ args = spec["args"]
1237
+ while "-s" in args:
1238
+ index = args.index("-s")
1239
+ del args[index:index + 2]
1240
+ if "--dangerously-bypass-approvals-and-sandbox" not in args:
1241
+ index = args.index("--") if "--" in args else len(args)
1242
+ args[index:index] = ["--dangerously-bypass-approvals-and-sandbox"]
1229
1243
  try:
1230
1244
  argv, child_env = sandbox_command(
1231
1245
  [spec["command"], *spec["args"]], Path(cwd),
@@ -11,6 +11,41 @@ from pathlib import Path
11
11
 
12
12
  CACHE = Path.home() / ".cdx" / "shared-cache"
13
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
+
14
49
 
15
50
  def clean_env(source: dict) -> dict:
16
51
  allowed = ("PATH", "LANG", "LC_ALL", "TERM", "TZ")
@@ -36,10 +71,20 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
36
71
  raise RuntimeError("Workspace must be a directory")
37
72
  tree = tree.resolve()
38
73
  CACHE.mkdir(parents=True, exist_ok=True)
39
- args = [bwrap, "--die-with-parent", "--new-session", "--unshare-all", "--cap-drop", "ALL"]
40
- # The provider needs transport even when its own tool sandbox denies network.
41
- if network or provider:
42
- args += ["--share-net"]
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"]
43
88
  args += ["--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/home/agent"]
44
89
  # An allowlist avoids overlooking another secret file or a credential symlink.
45
90
  paths = [Path(p) for p in ("/usr", "/bin", "/sbin", "/lib", "/lib64")]
@@ -51,7 +96,7 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
51
96
  args += ["--ro-bind", str(path), str(path)]
52
97
  for path in ("/etc/ssl", "/etc/resolv.conf", "/etc/hosts", "/etc/nsswitch.conf", "/etc/ld.so.cache",
53
98
  "/etc/fonts", "/etc/passwd", "/etc/group", "/etc/alternatives"):
54
- if Path(path).exists():
99
+ if Path(path).exists() and not (online and path == "/etc/resolv.conf"):
55
100
  args += ["--ro-bind", path, path]
56
101
  # Fail closed for a provider installed outside the explicitly permitted toolchains.
57
102
  resolved = Path(binary).resolve()
@@ -91,4 +136,10 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
91
136
  elif provider:
92
137
  raise RuntimeError("Unsupported isolated provider: " + provider)
93
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]
94
145
  return args, env