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.
@@ -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"]
package/src/run_usage.py CHANGED
@@ -10,6 +10,10 @@ definitions without saying so.
10
10
 
11
11
  The definitions below are therefore normative, not a suggestion:
12
12
 
13
+ Antigravity exception: the native input/cache/thinking relationship is not
14
+ established by the measured CLI output. Preserve its counters and reported
15
+ total rather than inventing a subset relationship or recomputing that total.
16
+
13
17
  input_tokens Uncached input only. Claude reports it this way
14
18
  natively; Codex does not, so its cache-inclusive count
15
19
  is reduced here rather than left for a reader to guess.
@@ -64,9 +68,9 @@ TOKEN_USAGE_KEYS = (
64
68
  "total_tokens",
65
69
  )
66
70
  USAGE_KEYS = (*TOKEN_USAGE_KEYS, "cache_hit_ratio")
67
- #: Derived values are never read from a provider.
71
+ #: Derived values are never read from a provider, except Antigravity's native total.
68
72
  DERIVED_USAGE_KEYS = ("cached_input_tokens", "total_tokens", "cache_hit_ratio")
69
- SUPPORTED_PROVIDERS = {"claude", "codex"}
73
+ SUPPORTED_PROVIDERS = {"claude", "codex", "ollama", "antigravity"}
70
74
 
71
75
 
72
76
  def empty_usage():
@@ -456,6 +460,8 @@ def coerce_usage(usage, provider=None):
456
460
  reasoning_tokens=usage.get("reasoning_tokens"),
457
461
  cache_creation_1h_tokens=usage.get("cache_creation_1h_tokens"),
458
462
  )
463
+ if provider == "antigravity":
464
+ record["total_tokens"] = _first_int(usage.get("total_tokens"))
459
465
  if record["cached_input_tokens"] is None:
460
466
  # A legacy record carrying only the fused figure: keep it visible rather
461
467
  # than dropping a number someone already recorded, but leave the split
@@ -635,10 +641,25 @@ def _usage_from_dict(value, provider=None):
635
641
  tokens. The fix is not arithmetic in this function: it is that the split
636
642
  parts go to `normalize_usage`, which owns every derived figure.
637
643
  """
644
+ if provider == "ollama":
645
+ prompt = _first_int(value.get("prompt_eval_count"))
646
+ output = _first_int(value.get("eval_count"))
647
+ return {**normalize_usage(input_tokens=prompt, output_tokens=output),
648
+ "prompt_eval_count": prompt, "eval_count": output}
638
649
  usage = value.get("usage") if isinstance(value.get("usage"), dict) else value
639
650
  if not isinstance(usage, dict):
640
651
  return empty_usage()
641
652
 
653
+ if provider == "antigravity":
654
+ # agy reports thinking separately. Without a documented cache subset
655
+ # relationship, retain its input count and its measured total verbatim.
656
+ record = normalize_usage(
657
+ input_tokens=usage.get("input_tokens"), output_tokens=usage.get("output_tokens"),
658
+ cache_read_tokens=usage.get("cache_read_tokens"), reasoning_tokens=usage.get("thinking_tokens"),
659
+ )
660
+ record["total_tokens"] = _first_int(usage.get("total_tokens"))
661
+ return record
662
+
642
663
  cache_creation = _first_int(usage.get("cache_creation_input_tokens"))
643
664
  if cache_creation is None:
644
665
  cache_creation = _first_int(usage.get("cache_write_input_tokens"))
@@ -720,3 +741,43 @@ def _int_value(*values):
720
741
 
721
742
  def _has_usage(usage):
722
743
  return isinstance(usage, dict) and any(usage.get(key) is not None for key in USAGE_KEYS)
744
+
745
+
746
+ def antigravity_outcome(info):
747
+ """Fail closed on missing completion, authentication errors and refused tools."""
748
+ if info.get("timed_out") or info.get("budget_exceeded"):
749
+ return info
750
+ try:
751
+ with open(info["stdout_path"], encoding="utf-8") as handle:
752
+ records = _parse_json_records(handle.read())
753
+ with open(info["stderr_path"], encoding="utf-8") as handle:
754
+ error_text = handle.read().lower()
755
+ except OSError:
756
+ records, error_text = [], ""
757
+ def objects(value):
758
+ if isinstance(value, dict):
759
+ yield value
760
+ for child in value.values():
761
+ yield from objects(child)
762
+ elif isinstance(value, list):
763
+ for child in value:
764
+ yield from objects(child)
765
+
766
+ statuses = [record.get("status") for record in records if isinstance(record, dict) and "status" in record]
767
+ errors = [record.get("error") for record in objects(records) if record.get("error")]
768
+ text = json.dumps(errors if statuses and statuses[-1] == "SUCCESS" else records).lower() + error_text
769
+ expired = any(word in text for word in ("invalid_grant", "unauthenticated", "session expired", "token expired",
770
+ "login required", "sign in", "not authenticated", "authentication required"))
771
+ refused = any(record.get(key) for record in objects(records)
772
+ for key in ("permission_denials", "refused_actions", "denied_actions"))
773
+ refused = refused or any(str(record.get("status", "")).upper() in ("REFUSED", "DENIED", "PERMISSION_DENIED")
774
+ for record in objects(records))
775
+ if expired:
776
+ code, message = "antigravity_session_expired", "Antigravity worker session has expired; sign in again on the worker."
777
+ elif refused:
778
+ code, message = "antigravity_action_refused", "Antigravity refused one or more requested actions."
779
+ elif not statuses or statuses[-1] != "SUCCESS":
780
+ code, message = "antigravity_status_failed", "Antigravity did not report JSON status SUCCESS."
781
+ else:
782
+ return info
783
+ return {**info, "returncode": info.get("returncode") or 1, "provider_error": code, "provider_message": message}
@@ -644,6 +644,10 @@ def create_session(base_dir, env, store, name, provider=DEFAULT_PROVIDER):
644
644
  _seed_codex_auth_from_global(auth_home, env=env)
645
645
  if normalized_provider == PROVIDER_CLAUDE:
646
646
  _ensure_claude_attribution_disabled(auth_home)
647
+ if normalized_provider == "antigravity":
648
+ from .antigravity_status import install
649
+
650
+ install(auth_home)
647
651
  now = _local_now_iso()
648
652
  session = {
649
653
  "name": name,
@@ -359,6 +359,12 @@ def session_paths(session):
359
359
 
360
360
  def _status_row_from_session(store, s):
361
361
  status = s.get("lastStatus")
362
+ antigravity = None
363
+ if s["provider"] == "antigravity":
364
+ from .antigravity_status import read_status
365
+
366
+ antigravity = read_status(s.get("authHome") or s.get("sessionRoot") or "")
367
+ status = antigravity
362
368
  enabled = s.get("enabled", True) is not False
363
369
  row_status = status if enabled else None
364
370
  runtime = _session_runtime(store, s["name"])
@@ -401,6 +407,13 @@ def _status_row_from_session(store, s):
401
407
  "updated_at": _to_local_iso(s.get("lastStatusAt")),
402
408
  "last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
403
409
  }
410
+ if antigravity is not None:
411
+ row.update({key: antigravity[key] for key in (
412
+ "quota", "quota_status", "quota_stale", "reading_age_seconds", "plan_tier", "updated_at",
413
+ "reset_5h_at_iso", "reset_week_at_iso",
414
+ )})
415
+ if enabled and antigravity["quota_stale"]:
416
+ row["status"] = "stale"
404
417
  row["runs_out_at_iso"] = _runs_out(row)
405
418
  return row
406
419
 
@@ -434,6 +447,9 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
434
447
  cache_only=False,
435
448
  status_timeout_seconds=None,):
436
449
  current_status = session.get("lastStatus")
450
+ if session["provider"] == "antigravity":
451
+ # The local statusLine file is the only Antigravity quota source.
452
+ return None
437
453
  if session.get("enabled", True) is False:
438
454
  return current_status
439
455
  if cache_only:
@@ -369,6 +369,7 @@ def _format_status_detail(row, use_color=False):
369
369
  f"{_style('Session:', '1', use_color)} {_format_session_name(row)}",
370
370
  f"{_style('Provider:', '1', use_color)} {row.get('provider') or 'n/a'}",
371
371
  f"{_style('Status:', '1', use_color)} {_style(row.get('status') or ('enabled' if row.get('enabled', True) else 'disabled'), '2' if row.get('enabled', True) is False else '32', use_color)}",
372
+ *([f"{_style('Plan:', '1', use_color)} {row['plan_tier']}"] if row.get("plan_tier") else []),
372
373
  f"{_style('Auth:', '1', use_color)} {_style(auth, auth_color, use_color)}",
373
374
  *([f"{_style('Auth type:', '1', use_color)} {_auth_type_label(row)[0]}"] if row.get("auth_type") else []),
374
375
  f"{_style('Available:', '1', use_color)} {_style_pct(row.get('available_pct'), use_color)}",
package/src/update_all.py CHANGED
@@ -241,9 +241,15 @@ def apply_update_all_plan(plan, service, env=None, runner=None, progress=None):
241
241
  advanced = (_newer(after, item["version"]) and
242
242
  (not item["latest_version"] or after == item["latest_version"] or _newer(after, item["latest_version"])))
243
243
  if not (advanced or (after and after == item["version"] and reports_current)):
244
- result["returncode"] = 1
245
- result["reason"] = "Update verification failed: installed version did not advance to the expected release and updater did not confirm it is current."
246
- result["stderr"] = (result["stderr"] + "\n" + result["reason"]).strip()
244
+ if item["name"] == PROVIDER_ANTIGRAVITY and not _newer(item["latest_version"], item["version"]):
245
+ result["warning"] = {
246
+ "code": "update_unverified",
247
+ "message": "agy update exited successfully, but currency could not be verified: no expected newer CLI version is known. Status remains unknown.",
248
+ }
249
+ else:
250
+ result["returncode"] = 1
251
+ result["reason"] = "Update verification failed: installed version did not advance to the expected release and updater did not confirm it is current."
252
+ result["stderr"] = (result["stderr"] + "\n" + result["reason"]).strip()
247
253
  results.append(result)
248
254
  if result["returncode"] == 0 and step.get("id"):
249
255
  completed.add(step["id"])