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.
@@ -3,6 +3,7 @@
3
3
  Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
4
4
  """
5
5
 
6
+ import sys
6
7
  import time
7
8
  import uuid
8
9
 
@@ -53,6 +54,30 @@ def _refresh_result(session, diagnostic):
53
54
 
54
55
 
55
56
  def handle_auth(rest, ctx):
57
+ if rest and rest[0] == "claude-token":
58
+ json_flag, args = _parse_json_flag(rest[1:])
59
+ if len(args) != 1:
60
+ raise CdxError("Usage: cdx auth claude-token <profile> [--json]")
61
+ session = ctx["service"]["get_session"](args[0])
62
+ if not session or session["provider"] != PROVIDER_CLAUDE:
63
+ raise CdxError("Select an existing Claude profile for cdx auth claude-token.")
64
+ if ctx["stdin_is_tty"]:
65
+ raise CdxError("Read the Claude setup token from standard input, without an interactive terminal.")
66
+ from ..claude_remote_token import store_token
67
+ stream = ctx.get("prompt_stdin") or sys.stdin
68
+ token = stream.read(16385)
69
+ if isinstance(token, bytes):
70
+ try:
71
+ token = token.decode("utf-8")
72
+ except UnicodeDecodeError as error:
73
+ raise CdxError("Provide one Claude setup token on standard input.") from error
74
+ store_token(session, token)
75
+ message = f"Stored remote Claude setup token for {session['name']}"
76
+ if json_flag:
77
+ _write_json(ctx, _json_success("auth.claude-token", message))
78
+ else:
79
+ ctx["out"](message + "\n")
80
+ return 0
56
81
  parsed = _parse_auth_args(rest)
57
82
  target = parsed["target"]
58
83
  if target == "all":
@@ -53,6 +53,8 @@ def _format_update_all_result(result, use_color=False):
53
53
  return _warn(f"skipped: {result['name']} (blocked by {result['blocked_by']})", use_color)
54
54
  if result.get("returncode") == 0:
55
55
  versions = f" ({result['version_before']} -> {result['version_after']})" if "version_after" in result else ""
56
+ if result.get("warning"):
57
+ return _warn(f"done: {result['name']}{versions}\n {result['warning']['message']}", use_color)
56
58
  return _success(f"done: {result['name']}{versions}", use_color)
57
59
  detail = str(result.get("stderr") or result.get("stdout") or "No error output.").strip().replace("\n", " ")
58
60
  detail = detail[-500:]
@@ -709,7 +711,8 @@ def handle_update(rest, ctx):
709
711
  failed = [row for row in results if row.get("returncode") != 0]
710
712
  message = "Applied workstation update plan" if not failed else "Workstation update plan completed with failures"
711
713
  if parsed["json"]:
712
- _write_json(ctx, _json_success("update.all", message, plan=plan, results=results, failed=len(failed)))
714
+ warnings = [dict(row["warning"], name=row["name"]) for row in results if row.get("warning")]
715
+ _write_json(ctx, _json_success("update.all", message, warnings=warnings, plan=plan, results=results, failed=len(failed)))
713
716
  else:
714
717
  ctx["out"](f"{_success(message, ctx['use_color']) if not failed else _warn(message, ctx['use_color'])}\n")
715
718
  return 1 if failed else 0
@@ -713,16 +713,26 @@ def handle_run(rest, ctx):
713
713
  session = selected["session"]
714
714
  selection_row = selected.get("row") or {}
715
715
 
716
+ if parsed.get("remote") and session["provider"] == "antigravity":
717
+ parsed["isolate_workspace"] = True
716
718
  remote_version = None
717
719
  if parsed.get("remote"):
718
- if session["provider"] != "codex":
719
- raise CdxError("--remote is only available for Codex profiles.", code="remote_provider_unsupported")
720
+ if session["provider"] not in ("codex", "claude", "ollama", "antigravity"):
721
+ raise CdxError("--remote is only available for Codex, Claude, Ollama and Antigravity profiles.", code="remote_provider_unsupported")
720
722
  if parsed.get("continue_run"):
721
723
  raise CdxError("--remote requires a fresh run without --continue-run.")
724
+ if session["provider"] == "ollama" and not (parsed.get("model") or (session.get("launch") or {}).get("model")):
725
+ raise CdxError("Remote Ollama text generation requires --model or a configured local model.")
726
+ if session["provider"] == "claude":
727
+ from ..claude_remote_token import read_token
728
+ read_token(session)
729
+ parsed["isolate_workspace"] = True
722
730
  remote_version = preflight(
723
731
  parsed["remote"], {**os.environ, **(ctx.get("env") or {})},
724
732
  remote_prefix=parsed.get("remote_prefix"), isolate_workspace=parsed.get("isolate_workspace", False),
725
- cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review")
733
+ cwd=parsed["cwd"], network=parsed.get("network_access", False), writable=parsed.get("permission") != "review",
734
+ **({"provider": "ollama", "model": parsed.get("model") or (session.get("launch") or {}).get("model")}
735
+ if session["provider"] == "ollama" else {"provider": session["provider"]}))
726
736
  cwd = os.path.abspath(parsed["cwd"])
727
737
  if not parsed.get("remote") and not os.path.isdir(cwd):
728
738
  raise CdxError(f"Invalid cwd: {parsed['cwd']}", code="invalid_cwd")
@@ -1074,6 +1084,9 @@ def handle_run(rest, ctx):
1074
1084
  message = f"Every eligible session was rate limited: {', '.join(attempted)}."
1075
1085
  elif failover_error_code == "failover_limit_reached":
1076
1086
  message = f"Failover stopped after {MAX_FAILOVER_TRANSITIONS} transitions: {', '.join(attempted)}."
1087
+ elif run_info.get("provider_error"):
1088
+ failover_error_code = run_info["provider_error"]
1089
+ message = run_info["provider_message"]
1077
1090
  elif run_info.get("timed_out"):
1078
1091
  message = "Provider process timed out."
1079
1092
  else:
@@ -1,17 +1,11 @@
1
1
  """Best-effort usage readers for interactive provider sessions.
2
2
 
3
- Three providers report usage and one does not:
4
-
5
- - Claude and Codex write their own JSONL transcripts, resolved by the
6
- conversation id cdx already holds.
7
- - Ollama writes nothing. Its counts exist only in `--verbose` terminal output,
8
- so they are parsed out of the PTY capture cdx takes for every launch.
9
- - Antigravity has no retrievable source. Its conversation history is
10
- schema-less binary protobuf under `~/.gemini/antigravity-cli/conversations/`
11
- with no `.proto` available to decode field numbers into token counts, and its
12
- logs are plain glog with no usage-bearing line at any verbosity `agy --help`
13
- exposes. This was investigated and is a dead end, not an oversight -- do not
14
- spend the afternoon rediscovering it.
3
+ - Claude and Codex expose per-session token counts in JSONL transcripts.
4
+ - Ollama exposes counts in the verbose terminal output captured by cdx.
5
+ - Antigravity exposes quota through interactive statusLine stdin (collected by
6
+ antigravity_status), and per-run tokens through `agy -p --output-format json`
7
+ (read by run_usage). Interactive per-run token counts remain unavailable;
8
+ quota changes are not token measurements.
15
9
  """
16
10
 
17
11
  import json
@@ -0,0 +1,175 @@
1
+ """Worker-owned Ollama relay and a text-only HTTP client, shipped over SSH."""
2
+
3
+ import http.client
4
+ import http.server
5
+ import json
6
+ import os
7
+ import pathlib
8
+ import signal
9
+ import socketserver
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import threading
14
+ from contextlib import contextmanager
15
+
16
+ OLLAMA_HOST = '127.0.0.1'
17
+ OLLAMA_PORT = 11434
18
+ MAX_BODY = 8 * 1024 * 1024
19
+
20
+ CLIENT = r'''
21
+ import http.client
22
+ import json
23
+ import socket
24
+ import sys
25
+
26
+ class Connection(http.client.HTTPConnection):
27
+ def connect(self):
28
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
29
+ self.sock.settimeout(300)
30
+ self.sock.connect(sys.argv[1])
31
+
32
+ try:
33
+ payload = json.load(sys.stdin)
34
+ def request(path, body):
35
+ connection = Connection('localhost', timeout=300)
36
+ try:
37
+ connection.request('POST', path, json.dumps(body), {'Content-Type': 'application/json'})
38
+ response = connection.getresponse()
39
+ data = json.loads(response.read(8 * 1024 * 1024))
40
+ if response.status != 200 or not isinstance(data, dict) or data.get('error'):
41
+ raise ValueError('Ollama request failed')
42
+ return data
43
+ finally:
44
+ connection.close()
45
+ model = payload['model']
46
+ info = request('/api/show', {'model': model})
47
+ if info.get('remote_host') or info.get('remote_model'):
48
+ raise ValueError('A local model is required')
49
+ if 'capabilities' in info and 'completion' not in info['capabilities']:
50
+ raise ValueError('Text completion is required')
51
+ if payload.get('probe'):
52
+ print('ollama-http')
53
+ else:
54
+ result = request('/api/chat', {'model': model, 'stream': False,
55
+ 'messages': [{'role': 'user', 'content': payload['prompt']}]})
56
+ if result.get('done') is not True or not isinstance(result.get('message', {}).get('content'), str):
57
+ raise ValueError('Incomplete Ollama response')
58
+ print(json.dumps(result))
59
+ except Exception:
60
+ print('Ollama text generation failed: check the local model and worker loopback API.', file=sys.stderr)
61
+ sys.exit(1)
62
+ '''
63
+
64
+
65
+ @contextmanager
66
+ def relay(directory, model):
67
+ """Expose only a fixed loopback destination and a fixed model over a private socket."""
68
+ class Handler(http.server.BaseHTTPRequestHandler):
69
+ def log_message(self, *_):
70
+ pass
71
+
72
+ def do_POST(self):
73
+ try:
74
+ size = int(self.headers.get('Content-Length', '0'))
75
+ if not 0 < size <= MAX_BODY or self.headers.get('Transfer-Encoding'):
76
+ raise ValueError('Invalid body')
77
+ self.connection.settimeout(10)
78
+ data = json.loads(self.rfile.read(size))
79
+ if not isinstance(data, dict) or data.get('model') != model:
80
+ raise ValueError('Invalid model')
81
+ if self.path == '/api/show':
82
+ allowed = set(data) == {'model'}
83
+ elif self.path == '/api/chat':
84
+ messages = data.get('messages')
85
+ allowed = (set(data) == {'model', 'stream', 'messages'} and data.get('stream') is False
86
+ and isinstance(messages, list) and len(messages) == 1
87
+ and isinstance(messages[0], dict) and set(messages[0]) == {'role', 'content'}
88
+ and messages[0]['role'] == 'user' and isinstance(messages[0]['content'], str))
89
+ else:
90
+ allowed = False
91
+ if not allowed:
92
+ raise ValueError('Forbidden call')
93
+ except (ValueError, TypeError, OSError):
94
+ self.send_error(403, 'Relay call refused')
95
+ return
96
+ connection = http.client.HTTPConnection(OLLAMA_HOST, OLLAMA_PORT, timeout=300)
97
+ try:
98
+ # No proxies, redirects, forwarded headers or client-selected destination.
99
+ connection.request('POST', self.path, json.dumps(data), {'Content-Type': 'application/json'})
100
+ response = connection.getresponse()
101
+ body = response.read(MAX_BODY + 1)
102
+ if len(body) > MAX_BODY:
103
+ raise ValueError('Oversized response')
104
+ self.send_response(response.status)
105
+ self.send_header('Content-Type', 'application/json')
106
+ self.send_header('Content-Length', str(len(body)))
107
+ self.end_headers()
108
+ self.wfile.write(body)
109
+ except (OSError, ValueError, http.client.HTTPException):
110
+ self.send_error(502, 'Ollama unavailable')
111
+ finally:
112
+ connection.close()
113
+
114
+ class Server(socketserver.ThreadingUnixStreamServer):
115
+ daemon_threads = True
116
+
117
+ path = str(pathlib.Path(directory) / 'api.sock')
118
+ with Server(path, Handler) as server:
119
+ os.chmod(path, 0o600)
120
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
121
+ thread.start()
122
+ try:
123
+ yield path
124
+ finally:
125
+ server.shutdown()
126
+ thread.join()
127
+
128
+
129
+ def run(payload, sandbox_source, probe=False):
130
+ """Run a single HTTP completion; no shell tools or model files enter the sandbox."""
131
+ model = payload.get('model')
132
+ if not isinstance(model, str) or not model.strip():
133
+ raise RuntimeError('Remote Ollama requires an installed model')
134
+ with tempfile.TemporaryDirectory(prefix='cdx-ollama-') as directory, relay(directory, model) as path:
135
+ command = [sys.executable, '-I', '-c', CLIENT, path]
136
+ env = None
137
+ if payload.get('isolate_workspace'):
138
+ namespace = {'__name__': 'cdx_workspace_sandbox'}
139
+ exec(compile(sandbox_source, 'workspace_sandbox.py', 'exec'), namespace)
140
+ command = ['python3', '-I', '-c', CLIENT, '/run/cdx-ollama/api.sock']
141
+ command, env = namespace['sandbox_command'](
142
+ command, pathlib.Path(payload['cwd']), network=False, writable=False,
143
+ provider='ollama', ollama_relay=directory)
144
+ child = subprocess.Popen(command, cwd=payload['cwd'], env=env, stdin=subprocess.PIPE,
145
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
146
+
147
+ def stop(*_):
148
+ if child.poll() is None:
149
+ try:
150
+ os.killpg(child.pid, signal.SIGKILL)
151
+ except ProcessLookupError:
152
+ pass
153
+
154
+ def disconnected():
155
+ while os.read(0, 1):
156
+ pass
157
+ stop()
158
+
159
+ previous = {sig: signal.signal(sig, stop) for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT)}
160
+ if not probe:
161
+ threading.Thread(target=disconnected, daemon=True).start()
162
+ try:
163
+ output, error = child.communicate(json.dumps({'model': model, 'prompt': payload.get('prompt'), 'probe': probe}).encode(),
164
+ timeout=30 if probe else None)
165
+ if probe:
166
+ sys.stdout.write(output.decode())
167
+ else:
168
+ print(json.dumps({'stream': 'stdout', 'data': output.decode()}), flush=True)
169
+ sys.stderr.write(error.decode())
170
+ return child.returncode
171
+ finally:
172
+ stop()
173
+ child.wait()
174
+ for sig, handler in previous.items():
175
+ signal.signal(sig, handler)
@@ -753,6 +753,9 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
753
753
  "label": "claude",
754
754
  }, capture_transcript=capture_transcript, env=env)
755
755
  if session["provider"] == PROVIDER_ANTIGRAVITY:
756
+ from .antigravity_status import install
757
+
758
+ install(_get_auth_home(session))
756
759
  args = _launch_config_args(session)
757
760
  if initial_prompt:
758
761
  args += ["--prompt-interactive", initial_prompt]
@@ -1059,6 +1062,18 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
1059
1062
  "sensitive_args": [initial_prompt] if initial_prompt else [],
1060
1063
  }
1061
1064
 
1065
+ if session["provider"] == PROVIDER_ANTIGRAVITY:
1066
+ args = ["-p", initial_prompt or "", "--output-format", "json"]
1067
+ if model:
1068
+ args += ["--model", model]
1069
+ args += _launch_config_args(session)
1070
+ args += _extra_args(session)
1071
+ return {
1072
+ "command": "agy", "args": args,
1073
+ "options": {"cwd": cwd, "env": {**env, **_antigravity_env_overrides(_get_auth_home(session))}},
1074
+ "label": "antigravity", "sensitive_args": [initial_prompt] if initial_prompt else [],
1075
+ }
1076
+
1062
1077
  return _build_launch_spec(
1063
1078
  session,
1064
1079
  cwd=cwd,
package/src/remote_run.py CHANGED
@@ -1,4 +1,4 @@
1
- """Controller-side temporary Codex credentials and SSH transport."""
1
+ """Controller-owned remote provider credentials and SSH transport."""
2
2
 
3
3
  import base64
4
4
  import json
@@ -88,6 +88,7 @@ def worker_input():
88
88
  for name in ('errors', 'run_usage', 'run_observation')}
89
89
  source += 'OBSERVATION_MODULES = ' + repr(modules) + '\n'
90
90
  source += f'OBSERVATION_LIMITS = {MAX_TRANSCRIPT_BYTES, MAX_TRANSCRIPT_SCAN_SECONDS!r}\n'
91
+ source += 'OLLAMA_SOURCE = ' + repr((directory / 'ollama_remote.py').read_text()) + '\n'
91
92
  source += (directory / 'remote_worker.py').read_text()
92
93
  return json.dumps(source).encode() + b'\n'
93
94
 
@@ -109,21 +110,32 @@ def ssh_command(target, mode, remote_prefix=None):
109
110
  '-o', 'ServerAliveInterval=5', '-o', 'ServerAliveCountMax=1', target, command]
110
111
 
111
112
 
112
- def preflight(target, env=None, *, remote_prefix=None, isolate_workspace=False, cwd=None, network=False, writable=True):
113
+ def preflight(target, env=None, *, remote_prefix=None, isolate_workspace=False, cwd=None, network=False, writable=True, provider="codex", model=None):
113
114
  payload = {'isolate_workspace': isolate_workspace, 'cwd': cwd, 'network': network, 'writable': writable}
115
+ if provider in ('ollama', 'claude', 'antigravity'):
116
+ payload.update(provider=provider, model=model)
117
+ if provider in ('claude', 'antigravity'):
118
+ payload['isolate_workspace'] = True
114
119
  try:
115
- local = subprocess.check_output(['codex', '--version'], env=env, stderr=subprocess.DEVNULL, timeout=10).decode().strip()
120
+ local = ('ollama-http' if provider == 'ollama' else
121
+ subprocess.check_output(['agy' if provider == 'antigravity' else provider, '--version'], env=env, stderr=subprocess.DEVNULL, timeout=10).decode().strip())
116
122
  result = subprocess.run(ssh_command(target, 'probe', remote_prefix), env=env,
117
123
  input=worker_input() + json.dumps(payload).encode() + b'\n',
118
124
  capture_output=True, timeout=30)
119
125
  except (OSError, subprocess.TimeoutExpired) as error:
120
- raise CdxError('Remote worker unreachable or Codex CLI unavailable.', code='remote_unavailable') from error
126
+ raise CdxError('Remote worker unreachable or provider unavailable.', code='remote_unavailable') from error
121
127
  if result.returncode:
128
+ if provider == 'antigravity' and result.returncode == 120:
129
+ raise CdxError('Antigravity worker is not signed in or its session file is unsafe; sign in once on the worker.',
130
+ code='antigravity_not_signed_in')
131
+ if provider == 'ollama':
132
+ raise CdxError('Remote Ollama unavailable: check the installed local model, loopback API and isolation dependencies.',
133
+ code='remote_unavailable')
122
134
  message = ('Remote workspace isolation unavailable; requires bubblewrap, pasta, nftables, working namespaces and a valid checkout.'
123
- if isolate_workspace else 'Remote worker unreachable or missing Python 3, Codex or memory-backed /dev/shm.')
135
+ if payload['isolate_workspace'] else 'Remote worker unreachable or missing Python 3, provider CLI or memory-backed /dev/shm.')
124
136
  raise CdxError(message, code='remote_unavailable')
125
137
  if local != result.stdout.decode().strip():
126
- raise CdxError('Remote Codex CLI version mismatch; install the controller version on the worker.', code='remote_version_mismatch')
138
+ raise CdxError('Remote provider CLI version mismatch; install the controller version on the worker.', code='remote_version_mismatch')
127
139
  return local
128
140
 
129
141
 
@@ -149,27 +161,53 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
149
161
  _route_termination_to_interrupt,
150
162
  )
151
163
 
152
- home = session['authHome']
153
- with remote_auth_lock(home):
154
- auth = temporary_auth(home)
155
- if auth['tokens'].get('refresh_token') != '':
156
- raise CdxError('Refusing to send a refresh token to a worker.')
164
+ if session['provider'] == 'ollama':
165
+ launch = session.get('launch') or {}
166
+ spec = {'command': 'ollama-http', 'args': [], 'env': {}}
167
+ payload = {'provider': 'ollama', 'model': launch.get('model'), 'prompt': initial_prompt,
168
+ 'cwd': cwd, 'isolate_workspace': bool(launch.get('isolate_workspace'))}
169
+ elif session['provider'] == 'antigravity':
170
+ spec = _build_headless_launch_spec(session, cwd, env_override, initial_prompt)
171
+ payload = {'provider': 'antigravity', 'args': spec['args'], 'cwd': cwd,
172
+ 'version': version, 'isolate_workspace': True, 'network': False,
173
+ 'writable': (session.get('launch') or {}).get('permission') != 'review'}
174
+ elif session['provider'] == 'claude':
175
+ from .claude_remote_token import read_token
176
+ token = read_token(session)
157
177
  spec = _build_headless_launch_spec(session, cwd, env_override, initial_prompt)
158
- if (session.get('launch') or {}).get('network_access'):
159
- index = spec['args'].index('--') if '--' in spec['args'] else len(spec['args'])
160
- spec['args'][index:index] = ['-c', 'sandbox_workspace_write.network_access=true']
161
- config = nonsecret_config(home)
162
- launch_text = json.dumps(spec['args']) + config
163
- if any(value and value in launch_text for value in auth['tokens'].values()):
178
+ if token in json.dumps(spec['args']):
164
179
  raise CdxError('Refusing a credential embedded in the remote prompt or configuration.')
165
- payload = {'auth': auth, 'config': config, 'args': spec['args'], 'cwd': cwd,
166
- 'version': version, 'expires_at': token_expiry(auth['tokens']['access_token']),
167
- 'isolate_workspace': bool((session.get('launch') or {}).get('isolate_workspace')),
180
+ if '--bare' in spec['args']:
181
+ raise CdxError('Remote Claude OAuth runs do not support --bare.')
182
+ index = spec['args'].index('--') if '--' in spec['args'] else len(spec['args'])
183
+ # Bubblewrap owns tool permissions, matching isolated local headless runs.
184
+ spec['args'][index:index] = ['--permission-mode', 'bypassPermissions']
185
+ payload = {'provider': 'claude', 'token': token, 'args': spec['args'], 'cwd': cwd,
186
+ 'version': version, 'isolate_workspace': True,
168
187
  'network': bool((session.get('launch') or {}).get('network_access')),
169
188
  'writable': (session.get('launch') or {}).get('permission') != 'review'}
170
- rotating = json.loads((pathlib.Path(home) / 'auth.json').read_text())['tokens'].get('refresh_token')
171
- if rotating and rotating in json.dumps(payload):
172
- raise CdxError('Refusing to send a refresh token to a worker.')
189
+ else:
190
+ home = session['authHome']
191
+ with remote_auth_lock(home):
192
+ auth = temporary_auth(home)
193
+ if auth['tokens'].get('refresh_token') != '':
194
+ raise CdxError('Refusing to send a refresh token to a worker.')
195
+ spec = _build_headless_launch_spec(session, cwd, env_override, initial_prompt)
196
+ if (session.get('launch') or {}).get('network_access'):
197
+ index = spec['args'].index('--') if '--' in spec['args'] else len(spec['args'])
198
+ spec['args'][index:index] = ['-c', 'sandbox_workspace_write.network_access=true']
199
+ config = nonsecret_config(home)
200
+ launch_text = json.dumps(spec['args']) + config
201
+ if any(value and value in launch_text for value in auth['tokens'].values()):
202
+ raise CdxError('Refusing a credential embedded in the remote prompt or configuration.')
203
+ payload = {'auth': auth, 'config': config, 'args': spec['args'], 'cwd': cwd,
204
+ 'version': version, 'expires_at': token_expiry(auth['tokens']['access_token']),
205
+ 'isolate_workspace': bool((session.get('launch') or {}).get('isolate_workspace')),
206
+ 'network': bool((session.get('launch') or {}).get('network_access')),
207
+ 'writable': (session.get('launch') or {}).get('permission') != 'review'}
208
+ rotating = json.loads((pathlib.Path(home) / 'auth.json').read_text())['tokens'].get('refresh_token')
209
+ if rotating and rotating in json.dumps(payload):
210
+ raise CdxError('Refusing to send a refresh token to a worker.')
173
211
  paths = _headless_artifact_paths(session, run_id, attempt)
174
212
  paths['remote'] = target
175
213
  start = datetime.now(timezone.utc)
@@ -208,7 +246,7 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
208
246
  child.stdin.flush()
209
247
  deadline = time.monotonic() + timeout_seconds if timeout_seconds else None
210
248
  while child.poll() is None:
211
- if time.time() >= payload['expires_at']:
249
+ if time.time() >= payload.get('expires_at', float('inf')):
212
250
  credential_expired = True
213
251
  break
214
252
  with observation_lock:
@@ -259,7 +297,7 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
259
297
  error.cancelled = True
260
298
  error.run_info = {**info, 'returncode': 130}
261
299
  raise error
262
- if credential_expired or time.time() >= payload['expires_at'] or child.returncode == 123:
300
+ if credential_expired or time.time() >= payload.get('expires_at', float('inf')) or child.returncode == 123:
263
301
  error = CdxError('Remote temporary credential expired; start a new run from the controller.', code='remote_credential_expired')
264
302
  error.run_info = info
265
303
  raise error
@@ -269,4 +307,10 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
269
307
  raise error
270
308
  if timed_out or budget_exceeded:
271
309
  info['returncode'] = 125 if budget_exceeded else 124
310
+ if session['provider'] == 'antigravity':
311
+ from .run_usage import antigravity_outcome
312
+ info = antigravity_outcome(info)
313
+ if child.returncode in (120, 121):
314
+ info.update(returncode=1, provider_error='antigravity_not_signed_in' if child.returncode == 120 else 'antigravity_session_busy',
315
+ provider_message='Antigravity worker is not signed in.' if child.returncode == 120 else 'Antigravity worker session is busy.')
272
316
  return info