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.
- package/README.md +31 -6
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_8.md +15 -0
- package/changelogs/CHANGELOGS_0_25_9.md +22 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +33 -1
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +69 -0
- package/docs/operations.md +39 -0
- package/docs/sessions-and-notifications.md +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/antigravity_status.py +161 -0
- package/src/claude_remote_token.py +45 -0
- package/src/cli.py +2 -0
- package/src/cli_args.py +2 -0
- package/src/commands/auth.py +25 -0
- package/src/commands/maintenance.py +4 -1
- package/src/commands/runs.py +16 -3
- package/src/interactive_usage.py +6 -12
- package/src/ollama_remote.py +175 -0
- package/src/provider_runtime.py +15 -0
- package/src/remote_run.py +69 -25
- package/src/remote_worker.py +85 -33
- package/src/run_command.py +9 -0
- package/src/run_registry.py +5 -0
- package/src/run_usage.py +63 -2
- package/src/session_service.py +4 -0
- package/src/session_status.py +16 -0
- package/src/status_view.py +1 -0
- package/src/update_all.py +9 -3
- package/src/workspace_sandbox.py +147 -14
package/src/remote_worker.py
CHANGED
|
@@ -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(["
|
|
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
|
-
['
|
|
80
|
-
writable=payload['writable'], source=env, auth_home=str(home), provider=
|
|
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,
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
['
|
|
162
|
-
writable=payload['writable'], source=
|
|
163
|
-
observation_home=str(
|
|
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 = [
|
|
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
|
|
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,
|
|
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)
|
package/src/run_command.py
CHANGED
|
@@ -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 []),
|
package/src/run_registry.py
CHANGED
|
@@ -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}
|
package/src/session_service.py
CHANGED
|
@@ -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,
|
package/src/session_status.py
CHANGED
|
@@ -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:
|
package/src/status_view.py
CHANGED
|
@@ -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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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"])
|