cdx-manager 0.25.7 → 0.25.9

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.
@@ -5,9 +5,12 @@ Provider authentication is the only credential deliberately passed to the provid
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
+ import ipaddress
8
9
  import json
9
10
  import os
10
11
  import shutil
12
+ import socket
13
+ import stat
11
14
  import tempfile
12
15
  from contextlib import contextmanager
13
16
  from pathlib import Path
@@ -28,7 +31,17 @@ NETWORK_RULES = """table inet mission_egress {
28
31
  PUBLIC_RESOLV_CONF = "nameserver 1.1.1.1\nnameserver 9.9.9.9\n"
29
32
 
30
33
  # Keep the trusted setup outside the agent's filesystem/PID/user namespaces.
31
- NETWORK_SETUP = """
34
+ CLAUDE_STDIN_EXEC = """
35
+ import json
36
+ import os
37
+ import sys
38
+ token = json.loads(sys.stdin.buffer.readline())
39
+ os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = token
40
+ os.dup2(os.open(os.devnull, os.O_RDONLY), 0)
41
+ os.execv(sys.argv[1], sys.argv[1:])
42
+ """
43
+
44
+ NETWORK_SETUP = r"""
32
45
  import pathlib
33
46
  import subprocess
34
47
  import sys
@@ -68,24 +81,82 @@ def private_auth_home(auth):
68
81
  yield directory
69
82
 
70
83
 
84
+ ANTIGRAVITY_TOKEN_PATH = ".gemini/antigravity-cli/antigravity-oauth-token"
85
+ # Candidate endpoints extracted from agy 1.4.12; live acceptance is pending.
86
+ ANTIGRAVITY_DESTINATIONS = ("oauth2.googleapis.com", "cloudcode-pa.googleapis.com",
87
+ "daily-cloudcode-pa.googleapis.com")
88
+
89
+
90
+ def antigravity_token():
91
+ path = Path.home() / ANTIGRAVITY_TOKEN_PATH
92
+ try:
93
+ info = path.lstat()
94
+ except FileNotFoundError:
95
+ raise RuntimeError("Antigravity worker is not signed in; sign in once on the worker.") from None
96
+ if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
97
+ or stat.S_IMODE(info.st_mode) != 0o600 or not info.st_size
98
+ or any(parent.is_symlink() for parent in path.parents)):
99
+ raise RuntimeError("Antigravity worker session file is unsafe; restore an owner-only regular token file.")
100
+ return path
101
+
102
+
103
+ def antigravity_secrets():
104
+ text = antigravity_token().read_text().strip()
105
+
106
+ def values(value):
107
+ if isinstance(value, dict):
108
+ return [item for child in value.values() for item in values(child)]
109
+ if isinstance(value, list):
110
+ return [item for child in value for item in values(child)]
111
+ return [value] if isinstance(value, str) and value else []
112
+
113
+ try:
114
+ return [text, *values(json.loads(text))]
115
+ except ValueError:
116
+ return [text]
117
+
118
+
119
+ @contextmanager
120
+ def antigravity_attempt(parent="/dev/shm"):
121
+ import fcntl
122
+
123
+ location = Path(parent).resolve()
124
+ mounts = [row.split() for row in Path('/proc/mounts').read_text().splitlines()]
125
+ enclosing = [row for row in mounts if Path(row[1]) == location or Path(row[1]) in location.parents]
126
+ if not enclosing or max(enclosing, key=lambda row: len(row[1]))[2] != 'tmpfs':
127
+ raise RuntimeError('Antigravity attempts require memory-backed storage')
128
+ token = antigravity_token()
129
+ # Lock the file itself without modifying it; every cdx attempt uses this inode.
130
+ with token.open("rb") as lock:
131
+ try:
132
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
133
+ except BlockingIOError:
134
+ raise RuntimeError("Antigravity worker session is busy with another attempt.") from None
135
+ with tempfile.TemporaryDirectory(prefix="cdx-agy-", dir=parent) as directory:
136
+ yield directory
137
+
138
+
71
139
  def clean_env(source: dict) -> dict:
72
140
  allowed = ("PATH", "LANG", "LC_ALL", "TERM", "TZ")
73
141
  env = {key: source[key] for key in allowed if key in source}
142
+ env["PATH"] = source.get("PATH", os.defpath) + os.pathsep + str(Path.home() / ".local/bin")
74
143
  env.update(HOME="/home/agent", XDG_CONFIG_HOME="/home/agent/.config",
75
144
  XDG_CACHE_HOME="/cache", GIT_CONFIG_NOSYSTEM="1",
76
145
  GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0", GIT_OPTIONAL_LOCKS="0",
77
146
  GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="credential.helper", GIT_CONFIG_VALUE_0="",
78
147
  GH_CONFIG_DIR="/home/agent/.config/gh", NPM_CONFIG_CACHE="/cache/npm",
79
148
  PIP_CACHE_DIR="/cache/pip", UV_CACHE_DIR="/cache/uv",
149
+ RTK_DB_PATH="/cache/rtk/history.db",
80
150
  # Cache users have different mount paths; their GC cannot identify each other's browsers.
81
151
  PLAYWRIGHT_BROWSERS_PATH="/cache/playwright", PLAYWRIGHT_SKIP_BROWSER_GC="1", CI="1")
82
152
  return env
83
153
 
84
154
 
85
155
  def sandbox_command(command: list[str], tree: Path, *, network: bool, writable: bool = True,
86
- source: dict | None = None, auth_home: str = "", provider: str = "", observation_home: str = "") -> tuple[list[str], dict]:
156
+ source: dict | None = None, auth_home: str = "", provider: str = "", observation_home: str = "", ollama_relay: str = "", claude_token_stdin: bool = False) -> tuple[list[str], dict]:
87
157
  source = os.environ if source is None else source
88
- binary = shutil.which(command[0], path=source.get("PATH"))
158
+ env = clean_env(source)
159
+ binary = shutil.which(command[0], path=env["PATH"])
89
160
  bwrap = shutil.which("bwrap", path=source.get("PATH"))
90
161
  if not binary or not bwrap:
91
162
  raise RuntimeError("Workspace isolation requires bubblewrap and " + command[0])
@@ -93,8 +164,9 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
93
164
  raise RuntimeError("Workspace must be a directory")
94
165
  tree = tree.resolve()
95
166
  CACHE.mkdir(parents=True, exist_ok=True)
167
+ (CACHE / "rtk").mkdir(exist_ok=True)
96
168
  # Provider transport uses the same restricted egress, even for read-only tools.
97
- online = bool(network or provider)
169
+ online = bool(network or (provider and provider != "ollama"))
98
170
  if online:
99
171
  pasta = shutil.which("pasta", path=source.get("PATH"))
100
172
  nft = shutil.which("nft", path=os.defpath + ":/usr/sbin:/sbin")
@@ -108,6 +180,10 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
108
180
  if online else ["--unshare-all"])
109
181
  args += ["--cap-drop", "ALL"]
110
182
  args += ["--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/home/agent"]
183
+ if (provider == "claude" and claude_token_stdin) or provider == "antigravity":
184
+ if not observation_home:
185
+ raise RuntimeError("Provider requires a volatile attempt home")
186
+ args += ["--bind", observation_home, "/home/agent"]
111
187
  # An allowlist avoids overlooking another secret file or a credential symlink.
112
188
  paths = [Path(p) for p in ("/usr", "/bin", "/sbin", "/lib", "/lib64")]
113
189
  home = Path.home()
@@ -137,15 +213,24 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
137
213
  or common.is_relative_to(tree) or common.is_relative_to(CACHE.resolve())
138
214
  or Path((gitdir / "gitdir").read_text().strip()).resolve() != marker):
139
215
  raise RuntimeError("Invalid external worktree Git directory")
140
- args += ["--ro-bind", str(marker), str(marker), "--ro-bind", str(common), str(common),
141
- "--tmpfs", str(common / "worktrees"), "--ro-bind", str(gitdir), str(gitdir),
142
- "--ro-bind", "/dev/null", str(common / "config"),
143
- "--tmpfs", str(common / "hooks")]
144
- if (gitdir / "config.worktree").exists():
145
- args += ["--ro-bind", "/dev/null", str(gitdir / "config.worktree")]
216
+ # Use sandbox-owned parents rather than traversing private host homes.
217
+ # Do not expose repository config (remote URLs/helpers/includes), hooks,
218
+ # other worktrees, or the source checkout. Git remains read-only.
219
+ private_git = Path("/home/agent/.git")
220
+ args += ["--ro-bind", str(marker), str(marker), "--dir", str(private_git)]
221
+ # Combine this worktree's HEAD/index with shared history, without any
222
+ # commondir/gitdir pointers back into the host filesystem.
223
+ for origin, names in (
224
+ (gitdir, ["HEAD", "index", *[p.name for p in gitdir.glob("sharedindex.*")]]),
225
+ (common, ["objects", "refs", "packed-refs", "shallow", *[p.name for p in common.glob("sharedindex.*")]]),
226
+ ):
227
+ for name in names:
228
+ if (origin / name).exists():
229
+ args += ["--ro-bind", str(origin / name), str(private_git / name)]
230
+ env.update(GIT_DIR=str(private_git), GIT_COMMON_DIR=str(private_git),
231
+ GIT_WORK_TREE=str(tree))
146
232
  elif marker.exists():
147
233
  raise RuntimeError("Expected an isolated git worktree")
148
- env = clean_env(source)
149
234
  if provider == "codex":
150
235
  auth_path = Path(auth_home) / "auth.json"
151
236
  if json.loads(auth_path.read_text()).get('tokens', {}).get('refresh_token') != '':
@@ -157,15 +242,63 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
157
242
  args += ["--ro-bind", str(Path(auth_home) / "auth.json"), "/home/agent/.codex/auth.json"]
158
243
  elif provider == "claude":
159
244
  token = source.get("CLAUDE_CODE_OAUTH_TOKEN")
160
- if not token:
245
+ if not token and not claude_token_stdin:
161
246
  raise RuntimeError("Isolated Claude runs require a cdx OAuth setup token")
162
- env.update(CLAUDE_CODE_OAUTH_TOKEN=token, CLAUDE_CODE_SUBPROCESS_ENV_SCRUB="1",
247
+ if claude_token_stdin:
248
+ env["XDG_CACHE_HOME"] = "/home/agent/.cache"
249
+ python = shutil.which("python3", path=os.defpath)
250
+ if not python:
251
+ raise RuntimeError("Remote Claude requires Python 3")
252
+ command = [python, "-I", "-c", CLAUDE_STDIN_EXEC, binary, *command[1:]]
253
+ binary = python
254
+ else:
255
+ env["CLAUDE_CODE_OAUTH_TOKEN"] = token
256
+ env.update(CLAUDE_CODE_SUBPROCESS_ENV_SCRUB="1",
163
257
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1")
258
+ elif provider == "antigravity":
259
+ token = antigravity_token()
260
+ args += ["--ro-bind", str(token), "/home/agent/" + ANTIGRAVITY_TOKEN_PATH]
261
+ env["AGY_CLI_DISABLE_AUTO_UPDATE"] = "true"
262
+ env["XDG_CACHE_HOME"] = "/home/agent/.cache"
263
+ elif provider == "ollama":
264
+ if not ollama_relay:
265
+ raise RuntimeError("Isolated Ollama requires a supervisor-owned relay")
266
+ args += ["--ro-bind", ollama_relay, "/run/cdx-ollama"]
164
267
  elif provider:
165
268
  raise RuntimeError("Unsupported isolated provider: " + provider)
166
269
  args += ["--chdir", str(tree), "--", binary, *command[1:]]
167
270
  if online:
168
- setup = "RULES = " + repr(NETWORK_RULES) + "\nRESOLVER = " + repr(PUBLIC_RESOLV_CONF) + "\n" + NETWORK_SETUP
271
+ rules = NETWORK_RULES
272
+ if provider == "claude" and claude_token_stdin and not network:
273
+ # Resolve on the trusted worker before pasta replaces the DNS namespace.
274
+ addresses = {entry[4][0] for entry in socket.getaddrinfo("api.anthropic.com", 443, type=socket.SOCK_STREAM)}
275
+ inference = []
276
+ for family, values in (("ip", sorted(address for address in addresses if ":" not in address)),
277
+ ("ip6", sorted(address for address in addresses if ":" in address))):
278
+ if values:
279
+ inference.append(f" {family} daddr {{ {', '.join(values)} }} tcp dport 443 accept")
280
+ if not inference:
281
+ raise RuntimeError("No Anthropic inference addresses")
282
+ rules = rules.replace("policy accept", "policy drop").replace(
283
+ " oif", " ct state established,related accept\n oif")
284
+ rules = rules.replace(" }\n}", " ip daddr { 1.1.1.1, 9.9.9.9 } udp dport 53 accept\n"
285
+ " ip daddr { 1.1.1.1, 9.9.9.9 } tcp dport 53 accept\n"
286
+ + "\n".join(inference) + "\n }\n}")
287
+ if provider == "antigravity":
288
+ addresses = {entry[4][0] for host in ANTIGRAVITY_DESTINATIONS
289
+ for entry in socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM)}
290
+ if not addresses or any(not ipaddress.ip_address(address).is_global for address in addresses):
291
+ raise RuntimeError("Antigravity destinations must resolve to public addresses")
292
+ rules = NETWORK_RULES.replace("policy accept", "policy drop")
293
+ inference = []
294
+ for family in ("ip", "ip6"):
295
+ values = sorted(a for a in addresses if (":" in a) == (family == "ip6"))
296
+ if values:
297
+ inference.append(f" {family} daddr {{ {', '.join(values)} }} tcp dport 443 accept")
298
+ rules = rules.replace(" }\n}", " ip daddr { 1.1.1.1, 9.9.9.9 } udp dport 53 accept\n"
299
+ " ip daddr { 1.1.1.1, 9.9.9.9 } tcp dport 53 accept\n"
300
+ + "\n".join(inference) + "\n }\n}")
301
+ setup = "RULES = " + repr(rules) + "\nRESOLVER = " + repr(PUBLIC_RESOLV_CONF) + "\n" + NETWORK_SETUP
169
302
  # Disable both directions explicitly: automatic host-loopback splicing
170
303
  # bypasses output filtering unless -T/-U are also disabled.
171
304
  args = [pasta, "--config-net", "--no-map-gw", "-t", "none", "-u", "none",