cdx-manager 0.25.8 → 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.
@@ -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
@@ -13,10 +13,12 @@ import tempfile
13
13
  import threading
14
14
  import time
15
15
  import types
16
+ from contextlib import ExitStack
16
17
  from datetime import datetime, timezone
17
18
 
18
19
  # Supplied by the controller before this module is executed.
19
20
  SANDBOX_SOURCE = globals().get('SANDBOX_SOURCE', '')
21
+ OLLAMA_SOURCE = globals().get('OLLAMA_SOURCE', '')
20
22
  OBSERVATION_MODULES = globals().get('OBSERVATION_MODULES', {})
21
23
  OBSERVATION_LIMITS = globals().get('OBSERVATION_LIMITS', (128 * 1024 * 1024, 1))
22
24
 
@@ -34,8 +36,8 @@ def observation_runtime():
34
36
  return sys.modules[package.__name__ + '.run_usage'], sys.modules[package.__name__ + '.run_observation']
35
37
 
36
38
 
37
- def version():
38
- return subprocess.check_output(["codex", "--version"], stderr=subprocess.DEVNULL, timeout=10).decode().strip()
39
+ def version(provider="codex"):
40
+ return subprocess.check_output(["agy" if provider == "antigravity" else provider, "--version"], stderr=subprocess.DEVNULL, timeout=10).decode().strip()
39
41
 
40
42
 
41
43
  def storage():
@@ -75,16 +77,26 @@ def isolation_probe_command(payload, home, env):
75
77
  # Execute exactly the local confinement implementation, shipped over stdin.
76
78
  namespace = {'__name__': 'cdx_workspace_sandbox'}
77
79
  exec(compile(SANDBOX_SOURCE, 'workspace_sandbox.py', 'exec'), namespace)
80
+ provider = payload.get('provider', 'codex')
78
81
  return namespace['sandbox_command'](
79
- ['codex', '--version'], pathlib.Path(payload['cwd']), network=payload['network'],
80
- writable=payload['writable'], source=env, auth_home=str(home), provider='codex',
81
- observation_home=str(home / 'sessions'))
82
+ ['agy' if provider == 'antigravity' else provider, '--version'], pathlib.Path(payload['cwd']), network=payload['network'],
83
+ writable=payload['writable'], source=env, auth_home=str(home), provider=provider,
84
+ observation_home=str(home) if provider in ('claude', 'antigravity') else str(home / 'sessions'),
85
+ claude_token_stdin=provider == 'claude')
82
86
 
83
87
 
84
88
  def probe(root, payload):
85
89
  if not payload.get('isolate_workspace'):
86
90
  print(version())
87
91
  return 0
92
+ if payload.get('provider') == 'antigravity':
93
+ namespace = {'__name__': 'cdx_workspace_sandbox'}
94
+ exec(compile(SANDBOX_SOURCE, 'workspace_sandbox.py', 'exec'), namespace)
95
+ try:
96
+ namespace['antigravity_token']()
97
+ except RuntimeError:
98
+ print('Antigravity worker is not signed in or its session file is unsafe.', file=sys.stderr)
99
+ return 120
88
100
  # Exercise mounts, namespaces and the nft setup with a dummy home before
89
101
  # the controller reads or sends a credential. Binary presence is insufficient.
90
102
  with tempfile.TemporaryDirectory(prefix='probe-', dir=root) as directory:
@@ -93,7 +105,7 @@ def probe(root, payload):
93
105
  os.chmod(home / 'auth.json', 0o600)
94
106
  (home / 'sessions').mkdir()
95
107
  command, env = isolation_probe_command(payload, home, dict(os.environ))
96
- result = subprocess.run(command, env=env, stdin=subprocess.DEVNULL,
108
+ result = subprocess.run(command, env=env, input=b'"probe-only"\n' if payload.get('provider') == 'claude' else b'',
97
109
  capture_output=True, timeout=20)
98
110
  if result.returncode:
99
111
  raise RuntimeError('Remote workspace isolation unavailable')
@@ -102,14 +114,21 @@ def probe(root, payload):
102
114
 
103
115
 
104
116
  def main():
105
- root = storage()
106
117
  payload = json.loads(sys.stdin.buffer.readline())
118
+ if payload.get('provider') == 'ollama':
119
+ namespace = {'__name__': 'cdx_ollama_remote'}
120
+ exec(compile(OLLAMA_SOURCE, 'ollama_remote.py', 'exec'), namespace)
121
+ return namespace['run'](payload, SANDBOX_SOURCE, probe=sys.argv[-1] == 'probe')
122
+ root = storage()
107
123
  if sys.argv[-1] == 'probe':
108
124
  return probe(root, payload)
109
- auth = payload['auth']
110
- if auth['tokens'].get('refresh_token') != '':
125
+ provider = payload.get('provider', 'codex')
126
+ agy = provider == 'antigravity'
127
+ claude = provider == 'claude'
128
+ auth = payload.get('auth')
129
+ if not (claude or agy) and auth['tokens'].get('refresh_token') != '':
111
130
  raise RuntimeError('Refusing a rotating remote credential')
112
- if version() != payload['version']:
131
+ if version(provider) != payload['version']:
113
132
  raise RuntimeError('Remote Codex CLI version mismatch')
114
133
  child = None
115
134
  stopped = threading.Event()
@@ -135,40 +154,70 @@ def main():
135
154
  signal.signal(signal.SIGHUP, stop)
136
155
  signal.signal(signal.SIGINT, stop)
137
156
  home, live = allocate(root)
157
+ auth_context = ExitStack()
138
158
  try:
139
159
  if stopped.is_set():
140
160
  return 130
141
161
  os.umask(0o077)
142
- (home / 'auth.json').write_text(json.dumps(auth))
143
- os.chmod(home / 'auth.json', 0o600)
144
- (home / 'config.toml').write_text('cli_auth_credentials_store = "file"\n' + payload['config'])
145
- env = {key: value for key, value in os.environ.items()
146
- if key not in ('OPENAI_API_KEY', 'OPENAI_ACCESS_TOKEN', 'CODEX_ACCESS_TOKEN', 'CODEX_API_KEY', 'CODEX_HOME')}
147
- env['CODEX_HOME'] = str(home)
148
- command = ['codex', *payload['args']]
149
- if payload.get('isolate_workspace'):
150
- (home / 'sessions').mkdir(mode=0o700)
151
- args = payload['args'][:]
152
- while '-s' in args:
153
- index = args.index('-s')
154
- del args[index:index + 2]
155
- if '--dangerously-bypass-approvals-and-sandbox' not in args:
156
- index = args.index('--') if '--' in args else len(args)
157
- args[index:index] = ['--dangerously-bypass-approvals-and-sandbox']
162
+ if agy:
163
+ namespace = {'__name__': 'cdx_workspace_sandbox'}
164
+ exec(compile(SANDBOX_SOURCE, 'workspace_sandbox.py', 'exec'), namespace)
165
+ try:
166
+ attempt_home = auth_context.enter_context(namespace['antigravity_attempt'](home))
167
+ except RuntimeError as error:
168
+ print(str(error), file=sys.stderr)
169
+ return 121 if 'busy' in str(error) else 120
170
+ command, env = namespace['sandbox_command'](
171
+ ['agy', *payload['args']], pathlib.Path(payload['cwd']), network=False,
172
+ writable=payload['writable'], source=dict(os.environ), provider='antigravity',
173
+ observation_home=attempt_home)
174
+ elif claude:
175
+ claude_home = home / 'claude-home'
176
+ claude_home.mkdir(mode=0o700)
177
+ if not payload.get('isolate_workspace') or '--bare' in payload['args']:
178
+ raise RuntimeError('Remote Claude requires isolated OAuth execution')
158
179
  namespace = {'__name__': 'cdx_workspace_sandbox'}
159
180
  exec(compile(SANDBOX_SOURCE, 'workspace_sandbox.py', 'exec'), namespace)
160
181
  command, env = namespace['sandbox_command'](
161
- ['codex', *args], pathlib.Path(payload['cwd']), network=payload['network'],
162
- writable=payload['writable'], source=env, auth_home=str(home), provider='codex',
163
- observation_home=str(home / 'sessions'))
182
+ ['claude', *payload['args']], pathlib.Path(payload['cwd']), network=payload['network'],
183
+ writable=payload['writable'], source=dict(os.environ), provider='claude',
184
+ observation_home=str(claude_home), claude_token_stdin=True)
185
+ else:
186
+ (home / 'auth.json').write_text(json.dumps(auth))
187
+ os.chmod(home / 'auth.json', 0o600)
188
+ (home / 'config.toml').write_text('cli_auth_credentials_store = "file"\n' + payload['config'])
189
+ env = {key: value for key, value in os.environ.items()
190
+ if key not in ('OPENAI_API_KEY', 'OPENAI_ACCESS_TOKEN', 'CODEX_ACCESS_TOKEN', 'CODEX_API_KEY', 'CODEX_HOME')}
191
+ env['CODEX_HOME'] = str(home)
192
+ command = ['codex', *payload['args']]
193
+ if payload.get('isolate_workspace'):
194
+ (home / 'sessions').mkdir(mode=0o700)
195
+ args = payload['args'][:]
196
+ while '-s' in args:
197
+ index = args.index('-s')
198
+ del args[index:index + 2]
199
+ if '--dangerously-bypass-approvals-and-sandbox' not in args:
200
+ index = args.index('--') if '--' in args else len(args)
201
+ args[index:index] = ['--dangerously-bypass-approvals-and-sandbox']
202
+ namespace = {'__name__': 'cdx_workspace_sandbox'}
203
+ exec(compile(SANDBOX_SOURCE, 'workspace_sandbox.py', 'exec'), namespace)
204
+ command, env = namespace['sandbox_command'](
205
+ ['codex', *args], pathlib.Path(payload['cwd']), network=payload['network'],
206
+ writable=payload['writable'], source=env, auth_home=str(home), provider='codex',
207
+ observation_home=str(home / 'sessions'))
164
208
  started_at = datetime.now(timezone.utc).isoformat()
165
209
  child = subprocess.Popen(command, cwd=payload['cwd'], env=env,
166
- stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
210
+ stdin=subprocess.PIPE if claude else subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
167
211
  start_new_session=True)
212
+ if claude:
213
+ child.stdin.write(json.dumps(payload['token']).encode() + b'\n')
214
+ child.stdin.close()
168
215
  if stopped.is_set():
169
216
  stop()
170
217
  threading.Thread(target=disconnected, daemon=True).start()
171
- secrets = [value for value in auth['tokens'].values() if isinstance(value, str) and value]
218
+ secrets = (namespace['antigravity_secrets']() if agy else
219
+ [payload['token']] if claude else
220
+ [value for value in auth['tokens'].values() if isinstance(value, str) and value])
172
221
 
173
222
  output_lock = threading.Lock()
174
223
  usage_reader, observation_reader = observation_runtime()
@@ -176,6 +225,8 @@ def main():
176
225
  stdout_path.touch(mode=0o600)
177
226
 
178
227
  def snapshot():
228
+ if claude or agy:
229
+ return
179
230
  # Read the same exact-thread evidence as the local supervisor before
180
231
  # volatile storage is removed. Only normalized metadata is exported.
181
232
  with output_lock:
@@ -229,7 +280,7 @@ def main():
229
280
  for thread in threads:
230
281
  thread.join()
231
282
  snapshot()
232
- if time.time() >= payload['expires_at']:
283
+ if time.time() >= payload.get('expires_at', float('inf')):
233
284
  print('Remote temporary credential expired; start a new run from the controller.', file=sys.stderr)
234
285
  return 123
235
286
  return 130 if stopped.is_set() else result
@@ -237,6 +288,7 @@ def main():
237
288
  stop()
238
289
  shutil.rmtree(home)
239
290
  live.close()
291
+ auth_context.close()
240
292
 
241
293
 
242
294
  if __name__ == '__main__':
@@ -244,5 +296,5 @@ if __name__ == '__main__':
244
296
  sys.exit(main())
245
297
  except Exception:
246
298
  # Never render exception values: input and provider errors may contain credentials.
247
- print('Remote worker setup or execution failed; check Python, Codex, cwd, /dev/shm and isolation dependencies.', file=sys.stderr)
299
+ print('Remote worker setup or execution failed; check Python, provider CLI, cwd, /dev/shm and isolation dependencies.', file=sys.stderr)
248
300
  sys.exit(126)
@@ -4,6 +4,7 @@ import sys
4
4
  from .cli_args import ARG_CODE_INVALID_VALUE
5
5
  from .errors import CdxArgumentError, CdxError
6
6
  from .provider_runtime import headless_permission_disables_network
7
+ from .run_registry import final_message
7
8
  from .run_usage import cost_fields, empty_usage, estimate_cost, extract_run_observation
8
9
 
9
10
  PROMPT_STDIN = "-"
@@ -169,6 +170,9 @@ def run_launch_payload(api_schema_version, parsed, session, artifacts, cwd=None,
169
170
  "ok": True,
170
171
  "action": "run",
171
172
  "launcher": "cdx",
173
+ **({"execution_mode": "text_generation",
174
+ "execution_note": "Ollama generates text only; it is not an autonomous coding agent and does not edit the repository."}
175
+ if parsed.get("remote") and session and session.get("provider") == "ollama" else {}),
172
176
  **({"remote": parsed["remote"]} if parsed.get("remote") else {}),
173
177
  "project": parsed.get("project"),
174
178
  "project_source": parsed.get("project_source", "unknown"),
@@ -222,6 +226,9 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
222
226
  "ok": bool(ok),
223
227
  "action": "run",
224
228
  "launcher": "cdx",
229
+ **({"execution_mode": "text_generation",
230
+ "execution_note": "Ollama generates text only; it is not an autonomous coding agent and does not edit the repository."}
231
+ if parsed.get("remote") and session and session.get("provider") == "ollama" else {}),
225
232
  **({"remote": parsed["remote"]} if parsed.get("remote") else {}),
226
233
  "project": parsed.get("project"),
227
234
  "project_source": parsed.get("project_source", "unknown"),
@@ -242,6 +249,8 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
242
249
  "transcript_path": run_info.get("transcript_path"),
243
250
  "stdout_path": run_info.get("stdout_path"),
244
251
  "stderr_path": run_info.get("stderr_path"),
252
+ **({"result": final_message({"artifacts": run_info})}
253
+ if parsed.get("remote") and session and session.get("provider") == "ollama" else {}),
245
254
  "usage": usage,
246
255
  "usage_model": usage_model,
247
256
  "usage_models": run_info.get("usage_models", [usage_model] if usage_model else []),
@@ -462,6 +462,11 @@ def _record_final_message(record):
462
462
  """The final assistant text one provider record carries, or None."""
463
463
  if not isinstance(record, dict):
464
464
  return None
465
+ if record.get("status") == "SUCCESS" and isinstance(record.get("response"), str):
466
+ return record["response"]
467
+ message = record.get("message")
468
+ if record.get("done") is True and isinstance(message, dict) and isinstance(message.get("content"), str):
469
+ return message["content"]
465
470
  # Claude: `--output-format json` ends with a `result` envelope.
466
471
  if record.get("type") == "result" and isinstance(record.get("result"), str):
467
472
  return record["result"]