cdx-manager 0.25.8 → 0.26.0
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 +100 -6
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_9.md +22 -0
- package/changelogs/CHANGELOGS_0_26_0.md +31 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +27 -1
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +69 -0
- 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 +14 -1
- package/src/cli_args.py +9 -4
- package/src/cli_commands.py +3 -2
- package/src/cli_helpers.py +1 -0
- package/src/commands/auth.py +25 -0
- package/src/commands/conversation.py +34 -0
- package/src/commands/maintenance.py +4 -1
- package/src/commands/runs.py +16 -3
- package/src/config.py +15 -0
- package/src/conversation.py +181 -0
- 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 +8 -1
- package/src/session_status.py +17 -0
- package/src/status_view.py +1 -0
- package/src/update_all.py +9 -3
- package/src/workspace_sandbox.py +126 -6
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
|
@@ -42,6 +42,27 @@ cdx work
|
|
|
42
42
|
|
|
43
43
|
CDX checks authentication before interactive launches. Handoff keeps the source transcript in place and asks the receiving agent to verify the conversation and workspace before acting. No account switch or prompt submission happens just because you run `cdx next`.
|
|
44
44
|
|
|
45
|
+
## Automatic mission concurrency per profile
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cdx set work --max-parallel 2
|
|
49
|
+
cdx show work
|
|
50
|
+
cdx unset work --max-parallel
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`--max-parallel` accepts an integer from 0 to 5 and stores `launch.max_parallel`
|
|
54
|
+
next to `priority` and `policy`. Zero excludes the profile from automatic
|
|
55
|
+
missions. An absent value adds no profile-specific ceiling: the scheduling
|
|
56
|
+
client applies its own rule. Existing configurations need no migration.
|
|
57
|
+
`cdx unset work --all` also removes the limit.
|
|
58
|
+
|
|
59
|
+
The scheduling client is responsible for enforcing this metadata. It is
|
|
60
|
+
separate from the existing per-command `cdx run --max-parallel` limit.
|
|
61
|
+
`cdx --json` (also `cdx ls --json`) includes the optional field in each
|
|
62
|
+
session's `launch` object; `cdx status --json` exposes `max_parallel` on each
|
|
63
|
+
row (`null` when absent). `cdx show` is an alias for `cdx config` and displays
|
|
64
|
+
`Max parallel` as `default` when unset.
|
|
65
|
+
|
|
45
66
|
## Cache efficiency and fix rounds
|
|
46
67
|
|
|
47
68
|
`cdx history --json` records uncached input, cache reads, cache writes, output tokens, and `usage.cache_hit_ratio` beside the existing token totals. The ratio is cache reads divided by all input tokens, excluding output. Missing provider splits stay `null`, and text views show `cache n/d` instead of claiming a zero hit rate.
|
|
@@ -89,7 +110,7 @@ Recovery does not require `--failover`, which still controls account quota exhau
|
|
|
89
110
|
|
|
90
111
|
Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). CDX Manager is [MIT licensed](LICENSE).
|
|
91
112
|
|
|
92
|
-
### Run Codex on an SSH worker
|
|
113
|
+
### Run Codex or Ollama on an SSH worker
|
|
93
114
|
|
|
94
115
|
```sh
|
|
95
116
|
cdx run work --remote agent@worker --cwd /srv/project --prompt 'Run the checks' --json
|
|
@@ -106,12 +127,12 @@ not synchronized. Use `--project` to explicitly attribute a remote checkout to a
|
|
|
106
127
|
project. Remote runs do not retry or fail over automatically, including when
|
|
107
128
|
`--failover` is supplied.
|
|
108
129
|
|
|
109
|
-
|
|
130
|
+
For Codex: a POSIX controller with `ssh` and Codex on PATH, and a Linux worker
|
|
110
131
|
with Python 3, memory-backed `/dev/shm`, and exactly the same `codex --version`.
|
|
111
132
|
Configure SSH keys, host trust and aliases in advance; the connection uses batch
|
|
112
133
|
mode and bounded connection/keepalive timeouts. Provider refusal, unreachable
|
|
113
|
-
workers and version mismatches fail before sending credentials.
|
|
114
|
-
|
|
134
|
+
workers and version mismatches fail before sending credentials. Codex requires an OAuth
|
|
135
|
+
profile. Remote continuation is refused because the temporary home
|
|
115
136
|
does not preserve conversation rollouts. Codex's native permission sandbox still
|
|
116
137
|
applies to ordinary remote runs.
|
|
117
138
|
|
|
@@ -123,7 +144,32 @@ Only a short, double-quoted Python bootstrap is sent in the SSH command line;
|
|
|
123
144
|
the worker and shared sandbox source are framed on stdin before the payload.
|
|
124
145
|
Neither source size nor prompt size consumes cmd.exe's 8191-character command limit.
|
|
125
146
|
|
|
126
|
-
|
|
147
|
+
Remote Ollama profiles also support headless text generation:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cdx run local --remote worker --cwd /srv/project --model qwen3:8b --prompt-file question.txt --isolate-workspace --json
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Here `local` is an existing Ollama profile. The named model must already be
|
|
154
|
+
installed on the worker's unauthenticated `127.0.0.1:11434` Ollama API; CDX
|
|
155
|
+
never downloads a missing model and rejects cloud-backed models. The prompt
|
|
156
|
+
file is read on the launching host and sent as content over SSH. This path
|
|
157
|
+
uses `/api/show` and `/api/chat` with `stream: false`, without an Ollama CLI or
|
|
158
|
+
Codex installation. Results identify `execution_mode: "text_generation"` and
|
|
159
|
+
include generated text in `result`, plus `prompt_eval_count` / `eval_count`
|
|
160
|
+
and normalized input/output token counts in `usage`.
|
|
161
|
+
|
|
162
|
+
Ollama is not an autonomous coding agent and does not edit the repository.
|
|
163
|
+
With `--isolate-workspace`, the checkout is read-only and the client has its
|
|
164
|
+
own network namespace without network access, including when `--network-access`
|
|
165
|
+
is supplied. A private Unix socket connects it to a supervisor-owned relay
|
|
166
|
+
with the fixed loopback destination and only the selected model's capability
|
|
167
|
+
and non-streaming inference calls allowed. Pull, delete, push, arbitrary URLs,
|
|
168
|
+
proxying and redirects are refused. No `~/.ollama` directory or model files
|
|
169
|
+
are mounted. The relay is removed on completion or cancellation. Remote
|
|
170
|
+
continuation remains unsupported; an explicit model or profile model is required.
|
|
171
|
+
|
|
172
|
+
Codex `--remote` supports `--isolate-workspace` using the same local isolation mechanism:
|
|
127
173
|
bubblewrap, pasta (passt), nftables, Python, the checkout/toolchain allowlist,
|
|
128
174
|
and an actual namespace/network setup using a dummy credential. Missing dependencies,
|
|
129
175
|
namespace denial or invalid checkouts fail before sending real credentials, without
|
|
@@ -193,3 +239,51 @@ The empty-refresh-token behavior was measured with Codex CLI 0.166.0. Real-host
|
|
|
193
239
|
acceptance should verify that the installed matching CLI does not attempt a
|
|
194
240
|
refresh with the temporary home, that the controller token chain stays unchanged
|
|
195
241
|
for a fresh credential, and that cleanup works after disconnect and SIGTERM.
|
|
242
|
+
|
|
243
|
+
### Read an interactive conversation
|
|
244
|
+
|
|
245
|
+
`cdx conversation <name> [--conversation ID] [--cursor CURSOR] [--limit N] --json`
|
|
246
|
+
returns a read-only JSON page for the profile's current conversation. An explicit
|
|
247
|
+
conversation ID uses an exact transcript lookup; a profile without a recorded ID
|
|
248
|
+
uses the existing latest-transcript resolver. This fallback is best effort.
|
|
249
|
+
|
|
250
|
+
The normal cdx JSON envelope includes `status`, `reason`, `provider`, `session`,
|
|
251
|
+
`entries`, `next_cursor`, and `has_more`. Each entry has exactly `role`
|
|
252
|
+
(`user`, `assistant`, or `tool`), `text`, `tool_name`, `tool_summary`, `at`, and
|
|
253
|
+
`id`. Missing values are `null`; tool text is empty. Entries follow file and
|
|
254
|
+
content-block order, with stable IDs for the same file position. Timestamps are
|
|
255
|
+
provider timestamps when valid. Tool calls/results have a name and a short
|
|
256
|
+
summary; arguments, results, attachments, and reasoning blocks are omitted.
|
|
257
|
+
Result names are recovered from up to 100 recent calls, otherwise `tool`.
|
|
258
|
+
Common credential patterns and secret assignments in text are redacted without
|
|
259
|
+
opening credential stores. Review conversation text before sharing: arbitrary
|
|
260
|
+
unlabelled sensitive prose cannot be identified automatically.
|
|
261
|
+
|
|
262
|
+
Pass `next_cursor` unchanged as `--cursor` to fetch the next page or poll for new
|
|
263
|
+
entries. `--limit` bounds entries (default 100, range 1–1000). Reading seeks to
|
|
264
|
+
the saved byte position rather than replaying history, and scans at most 8 MiB
|
|
265
|
+
per call. A page may be empty while still returning a usable cursor. Poll on a
|
|
266
|
+
timer: `has_more` means unread bytes, which can include an incomplete final line
|
|
267
|
+
that will only become readable after the provider appends its newline.
|
|
268
|
+
|
|
269
|
+
Codex response items and Claude-compatible JSONL transcripts (Claude and
|
|
270
|
+
Antigravity) are supported through the existing transcript resolver. Missing or
|
|
271
|
+
unreadable files return `status: "unavailable"` with a reason. Ollama's terminal
|
|
272
|
+
capture does not reliably identify message roles and returns
|
|
273
|
+
`status: "unsupported"`, including when no capture exists. A transcript record
|
|
274
|
+
larger than 1 MiB returns `unavailable`. A replaced/truncated file or invalid
|
|
275
|
+
cursor returns `reset_required`; discard the cursor and reload the conversation.
|
|
276
|
+
Cursors are opaque, versioned, and bound to a provider and file. Treat transcripts
|
|
277
|
+
as append-only; reset the client if a provider rewrites earlier history.
|
|
278
|
+
|
|
279
|
+
Python callers can use the same page contract:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from cdx_manager.conversation import read_conversation
|
|
283
|
+
|
|
284
|
+
page = read_conversation("claude", "/path/to/profile/home", conversation_id="session-id", limit=100)
|
|
285
|
+
next_page = read_conversation(
|
|
286
|
+
"claude", "/path/to/profile/home", conversation_id="session-id",
|
|
287
|
+
cursor=page["next_cursor"], limit=100,
|
|
288
|
+
)
|
|
289
|
+
```
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.26.0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# CDX Manager 0.25.9
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-03
|
|
4
|
+
|
|
5
|
+
This release expands isolated remote provider support and improves Antigravity
|
|
6
|
+
quota, usage reporting, and update feedback.
|
|
7
|
+
|
|
8
|
+
## Remote runs
|
|
9
|
+
|
|
10
|
+
- Integrate worker-owned Antigravity headless runs with mandatory isolation, a volatile home, restricted inference networking, and redacted output. Keep worker credentials on the worker and preserve local profile authentication. Live worker acceptance measurements remain required before claiming production support. (#98)
|
|
11
|
+
- Enable isolated remote Claude runs with a dedicated OAuth setup token stored on the controller and transported through stdin. Use a fresh memory-backed home per attempt, restrict inference traffic by default, and retain usage reporting and output redaction. (#95)
|
|
12
|
+
- Add remote Ollama text generation with an explicitly selected installed worker model through a restricted local relay. Limit requests to model inspection and non-streaming chat; keep the checkout read-only and report generated text and token usage. This mode does not edit repositories or act as an autonomous coding agent. (#94)
|
|
13
|
+
|
|
14
|
+
## Antigravity status and updates
|
|
15
|
+
|
|
16
|
+
- Show Antigravity quota, reset times, and reading age from a local statusLine integration that preserves existing commands. Record native headless token counters, and identify stale or expired quota readings. (#96)
|
|
17
|
+
- Keep successful `agy update` commands successful when no newer version can be verified. Report an `update_unverified` warning while retaining unknown latest-version status; non-zero updater exits still fail. (#93)
|
|
18
|
+
|
|
19
|
+
## Maintenance
|
|
20
|
+
|
|
21
|
+
- Record tray companion checksums for v0.25.8. (`d0c8d33b671234cf648da2b21b7bb0bfb0ce302e`)
|
|
22
|
+
- Record release archive checksums for v0.25.8. (`20fb80c7d115500c8da5be433526e195063258d9`)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# CDX Manager 0.26.0
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-03
|
|
4
|
+
|
|
5
|
+
This release adds optional profile concurrency settings for automatic missions
|
|
6
|
+
and incremental JSON access to interactive conversations.
|
|
7
|
+
|
|
8
|
+
## Automatic mission concurrency
|
|
9
|
+
|
|
10
|
+
- Add an optional per-profile `launch.max_parallel` setting through `cdx set
|
|
11
|
+
<name> --max-parallel N`, accepting integers from 0 to 5. Zero excludes a
|
|
12
|
+
profile from automatic missions; an unset value leaves the scheduling client's
|
|
13
|
+
default in effect. Expose the setting in list JSON, status JSON, and profile
|
|
14
|
+
settings, and clear it with `cdx unset <name> --max-parallel`. Scheduling
|
|
15
|
+
clients enforce this setting; the existing `cdx run --max-parallel` limit
|
|
16
|
+
remains separate. (#101)
|
|
17
|
+
|
|
18
|
+
## Interactive conversation reading
|
|
19
|
+
|
|
20
|
+
- Add `cdx conversation <name> --json` and the reusable `read_conversation`
|
|
21
|
+
Python function to read Codex, Claude, and Antigravity transcripts
|
|
22
|
+
incrementally with bounded reads and opaque pagination cursors. Return
|
|
23
|
+
normalized messages and tool activity summaries while omitting tool bodies,
|
|
24
|
+
attachments, metadata, and reasoning, and redacting recognized credentials.
|
|
25
|
+
Report missing transcripts as unavailable and Ollama terminal captures as
|
|
26
|
+
unsupported; require a cursor reset after rotation or truncation. (#100)
|
|
27
|
+
|
|
28
|
+
## Maintenance
|
|
29
|
+
|
|
30
|
+
- Record tray companion checksums for v0.25.9. (`6d0a825b9649da9aead3379e6411e3959cd30a77`)
|
|
31
|
+
- Record release archive checksums for v0.25.9. (`e3682ad8aed14d75c3c686a0c6027f53cfe81cd3`)
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
## Antigravity quota and run usage
|
|
4
|
+
|
|
5
|
+
- Install a local Antigravity statusLine command in new profiles and on interactive launch, merging settings and relaying the original stdin to any existing command so its output is preserved. Store only the reading time, plan tier and allowlisted quota fields in an atomic mode-600 profile file; never access Google internal endpoints or antigravity-oauth-token.
|
|
6
|
+
- Show Gemini 5-hour and weekly remaining quota, reset times and reading age in status. Include all bucket names, including third-party buckets, in JSON. Mark readings older than 24 hours stale and suppress availability after a bucket's reset has passed.
|
|
7
|
+
- Launch headless Antigravity with `agy -p --output-format json` and record its input, output, thinking, cache-read and reported total counters. Preserve the native total without assuming an undocumented cache or thinking subset relationship. Interactive quota is not per-run token usage.
|
|
8
|
+
- Cover synthetic native payloads, missing/unknown buckets, stale and expired readings, settings merges, existing command output, private file permissions and headless observation.
|
|
9
|
+
|
|
10
|
+
## Worker-owned Antigravity headless runs
|
|
11
|
+
|
|
12
|
+
- Allow Antigravity profiles in remote runs with mandatory bubblewrap, explicit `agy -p --output-format json`, an owner-only worker token mounted read-only, and a volatile HOME. Never transport worker credentials to the controller; redact worker output before SSH forwarding. Hold a worker token lock across each attempt and disable automatic CLI updates.
|
|
13
|
+
- Restrict inference to a conservative Google endpoint allowlist plus public DNS, including when tool network access is requested; retain private-range denial. The candidate list and read-only token policy require the live measurements documented in the mission fragment before claiming production acceptance.
|
|
14
|
+
- Fail on absent/unsafe worker login, expired sessions, missing/non-success JSON status and refused actions. Normalize JSON usage through `run_usage` and preserve existing run artifacts/history. Cover fake worker success, refusal, expiry, secret redaction, locking, cleanup, local profile authentication and mount/network construction.
|
|
15
|
+
|
|
16
|
+
## Remote Claude headless runs
|
|
17
|
+
|
|
18
|
+
- Add `cdx auth claude-token <profile>` to store a dedicated `claude setup-token` value from stdin in a controller-only mode-600 profile file. Missing or unsafe tokens stop remote runs with this command as the remedy; worker login is unnecessary.
|
|
19
|
+
- Allow remote Claude JSON headless execution with mandatory bubblewrap isolation, a fresh memory-backed HOME per attempt and no local profile mounts. Carry the token through SSH stdin and a sandbox stdin exec shim, exposing it only in Claude's environment, never worker credential files, argv or persisted output. Keep subprocess environment scrubbing, remove competing authentication/cloud/proxy sources, disable non-essential traffic and refuse `--bare`.
|
|
20
|
+
- Allow inference HTTPS to resolved `api.anthropic.com` addresses and DNS by default; retain public tool-network access only when requested. Cover fake-worker usage parsing, output redaction, absent/unsafe token files, stdin storage, isolation construction and volatile cleanup after success, failure and timeout.
|
|
21
|
+
|
|
3
22
|
## Isolated mission Git and RTK
|
|
4
23
|
|
|
5
24
|
- Give local and remote isolated linked worktrees a read-only Git view with their own HEAD/index and shared objects/refs, without traversing private host parents or exposing repository/global configuration, hooks, SSH keys or Git push credentials. Preserve supervisor-owned commits and pushes.
|
|
@@ -8,10 +27,11 @@
|
|
|
8
27
|
|
|
9
28
|
## Truthful workstation updates
|
|
10
29
|
|
|
30
|
+
- Keep a zero-exit `agy update` successful when no expected newer CLI version is known, even if its installed version does not advance and its output cannot confirm currency. Preserve `status: unknown` and `latest_version: null`, expose an `update_unverified` warning in JSON and text, and continue failing non-zero updater exits. No additional version source is assumed.
|
|
11
31
|
- Stop querying the editor release feed for the Antigravity CLI: its versions do not match `agy`. Keep `latest_version: null` and `status: unknown`, while still running the native `agy update` with `--yes` or confirmation. No read-only latest-version check is documented by `agy update --help`.
|
|
12
32
|
- Accept a successful native updater's `You are already on the latest version.` message when the installed version is unchanged, or verify a version increase against the published version when known. Preserve updater failures and unknown inventory status. Cover the real Antigravity output line, version increases, failures, and JSON/text uncertainty.
|
|
13
33
|
|
|
14
|
-
- Detect advertised native updater subcommands in both stdout and stderr; use Antigravity's known `agy update` command directly. Verify the installed CLI version after updating,
|
|
34
|
+
- Detect advertised native updater subcommands in both stdout and stderr; use Antigravity's known `agy update` command directly. Verify the installed CLI version after updating, retain strict verification for providers with a known expected release, and expose before/after versions without hiding major upgrades.
|
|
15
35
|
- Fall back from empty or failed Ollama vendor responses (including Linux HTTP 204) to the latest stable `ollama/ollama` GitHub release. Keep Linux updates manual with `update_available`, a null command, and a service-safety reason; never replace a potentially active binary or restart models automatically.
|
|
16
36
|
|
|
17
37
|
- Verify latest versions from npm, RTK GitHub releases, and Ollama vendor update endpoints with short read-only probes. Report `unknown` on unavailable evidence; reserve `up_to_date` for identical known versions, including Ponytail revisions.
|
|
@@ -57,3 +77,9 @@
|
|
|
57
77
|
- Keep snapshots independent of controller refresh during long missions, prohibit authentication writeback, enforce their original expiry, and remove temporary storage after success, failure and stop. Non-isolated local authentication remains unchanged.
|
|
58
78
|
- Reject rotating credentials at the sandbox boundary. Test snapshot contents, permissions, cleanup, refresh lock ownership, concurrent controller refresh, remote isolation through fake SSH and missing dependency refusal.
|
|
59
79
|
- The separately maintained remote-ops `scripts/workspace_sandbox.py` requires the same credential guard and run-private lifecycle integration; it is not shipped by this repository.
|
|
80
|
+
|
|
81
|
+
## Remote Ollama text generation
|
|
82
|
+
|
|
83
|
+
- Allow Ollama profiles in `cdx run --remote` with an explicitly selected, already installed local worker model and no authentication or Ollama/Codex CLI requirement. Read prompts on the launching host and use the HTTP capability/chat APIs with `stream: false`; retain generated text and prompt/evaluation token counters in run results and usage.
|
|
84
|
+
- Add a worker-supervised private Unix socket relay restricted to the fixed `127.0.0.1:11434` destination, selected model, `/api/show` and non-streaming `/api/chat`. Refuse model management, alternate destinations and cloud-backed models. Isolated clients have no network access and mount neither Ollama profiles nor model files; their checkout is read-only.
|
|
85
|
+
- State in CLI help and results that this is text generation, not an autonomous coding agent, with no repository edits. Cover success, usage, controller prompt content, missing models, forbidden relay calls, unreachable servers and sandbox relay construction with a fake HTTP API.
|
|
@@ -771,6 +771,28 @@
|
|
|
771
771
|
"x86_64-pc-windows-msvc": "be2e6ff8607351019447c6fbf0622254c2f8d8db81581007af71b74932e37f50",
|
|
772
772
|
"x86_64-unknown-linux-musl": "32de07569929ade136cf75ed8f0716e0f3b96a9c53c6632c628e13782d223518"
|
|
773
773
|
}
|
|
774
|
+
},
|
|
775
|
+
"v0.25.8": {
|
|
776
|
+
"github_tarball_sha256": "4a3e3eeb654a4590ea033e6d99337a57b9f255161ee2b9c9347e8def0d257868",
|
|
777
|
+
"github_zip_sha256": "ad2c7e8d1528f36f4be66320970522385ba3c0a473ab893475b174609ec05630",
|
|
778
|
+
"tray_assets": {
|
|
779
|
+
"aarch64-apple-darwin": "46e4e9ce2ce54d974dd3b77364fc97595b4d4388787c2e7486245374d342ff87",
|
|
780
|
+
"aarch64-unknown-linux-musl": "74afc63053dc3f6ca3fae010d8afc64c917cd37e71cc005dd8b9d3e9383b99e6",
|
|
781
|
+
"x86_64-apple-darwin": "990b305bf32d24e0126cb6f3fe42cd9858a6cd5dc460f41d0fb4419516324a40",
|
|
782
|
+
"x86_64-pc-windows-msvc": "9577f9dff7d457094276376f262904dad8c9aff95ed2ca87b834c9c900fb5493",
|
|
783
|
+
"x86_64-unknown-linux-musl": "db7e322037041c2a898043f0c7a691c22cab6a794aab5b2439a06ac91e92fa39"
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
"v0.25.9": {
|
|
787
|
+
"github_tarball_sha256": "cd6d881144cefbd82b0929f375f471ed076e2f23be60744895aee54de5aead12",
|
|
788
|
+
"github_zip_sha256": "ccd28db3f526a081ed75cefc016f54d7eadf0adcaab401c8aed77d477cf917d3",
|
|
789
|
+
"tray_assets": {
|
|
790
|
+
"aarch64-apple-darwin": "4f90576e9e8db1ec08ed856a91a02f4049c7f7186e6dccac1908ba43c72df6fd",
|
|
791
|
+
"aarch64-unknown-linux-musl": "dcdf50751982f78805492eca059d83238f5d2190b97455b2d96d91283b764cdc",
|
|
792
|
+
"x86_64-apple-darwin": "289818bd5d692a6ba5035f10886318179bb2baa98e22a05e7abd751ea64d19ed",
|
|
793
|
+
"x86_64-pc-windows-msvc": "32889713859cf23d4d82647638d909d62075b92d8e6571e32118ebfa208d81b2",
|
|
794
|
+
"x86_64-unknown-linux-musl": "9e94b799fe4b01d95aeb172c1eba28dc011be69cc2b39b5743f0d81986eaa80f"
|
|
795
|
+
}
|
|
774
796
|
}
|
|
775
797
|
}
|
|
776
798
|
}
|
package/docs/cli.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
| `cdx label <name> --clear [--json]` | Clear a session label |
|
|
20
20
|
| `cdx login <name> [--json]` | Re-authenticate a session (logout + login) |
|
|
21
21
|
| `cdx logout <name> [--json]` | Log out of a session |
|
|
22
|
+
| `cdx auth claude-token <profile> [--json]` | Store a dedicated remote Claude setup token from stdin in a private controller profile file |
|
|
22
23
|
| `cdx auth refresh <name\|all> [--json]` | Make a locked, non-generation Codex auth probe; reports `login_required` when interactive login is needed |
|
|
23
24
|
| `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
|
|
24
25
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
@@ -240,6 +241,74 @@ cdx run \
|
|
|
240
241
|
|
|
241
242
|
The result includes `launcher: "cdx"`, `run_id`, selected `session`, `provider`, `exit_code`, `duration_seconds`, absolute `transcript_path`, `stdout_path`, `stderr_path`, and normalized usage token fields. Codex headless runs use `codex exec --json`; Claude headless runs use `claude --print --output-format json`. Token counts are `null` when the provider does not expose a supported JSON or JSONL usage shape.
|
|
242
243
|
|
|
244
|
+
Remote Claude profiles require a dedicated long-lived token produced by `claude setup-token`
|
|
245
|
+
on the launching host. Supply only that token on standard input to
|
|
246
|
+
`cdx auth claude-token <profile>` (never put it in command arguments). This stores
|
|
247
|
+
`.cdx-remote-claude-token` with mode `0600` in the controller profile; ordinary
|
|
248
|
+
interactive Claude credentials and ambient API keys are not a fallback. A missing
|
|
249
|
+
or unsafe file stops before SSH and names this command.
|
|
250
|
+
|
|
251
|
+
Run `cdx run <profile> --remote worker --cwd /srv/project --prompt-file task.md --json`.
|
|
252
|
+
Claude always uses bubblewrap isolation, even without `--isolate-workspace`, and
|
|
253
|
+
requires matching Claude CLI versions on the controller and Linux worker, Python 3,
|
|
254
|
+
bubblewrap, pasta, nftables, working namespaces and memory-backed `/dev/shm`.
|
|
255
|
+
No worker login or profile mount is needed. Each attempt sends the token over SSH
|
|
256
|
+
stdin, then a private pipe through the sandbox to an exec shim that sets it only
|
|
257
|
+
in Claude's environment. It is never a worker credential file or command argument;
|
|
258
|
+
provider output is redacted before persistence. Fresh volatile HOME storage holds
|
|
259
|
+
`.claude`, `.claude.json`, caches and traces and is removed on exit, failure or stop.
|
|
260
|
+
`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` and
|
|
261
|
+
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` remain set; API keys, proxy tokens,
|
|
262
|
+
proxy URLs and cloud-provider variables are excluded from the sandbox environment.
|
|
263
|
+
`--bare` is refused because it ignores OAuth. The outer sandbox owns tool permissions,
|
|
264
|
+
including a read-only checkout for `--permission review`.
|
|
265
|
+
|
|
266
|
+
Without `--network-access`, the trusted setup resolves `api.anthropic.com` and allows
|
|
267
|
+
HTTPS to those addresses plus public DNS, with other external destinations dropped.
|
|
268
|
+
`--network-access` retains the existing public tool-network policy. Address resolution
|
|
269
|
+
is fixed for the attempt; restart a run if the inference service changes addresses.
|
|
270
|
+
Usage and observed models come from Claude's JSON result. Fresh runs are required;
|
|
271
|
+
remote `--continue-run` remains unsupported.
|
|
272
|
+
|
|
273
|
+
Remote Antigravity profiles use the worker's existing login, created once by the
|
|
274
|
+
operator in that worker user's normal HOME. Run
|
|
275
|
+
`cdx run agy-profile --remote worker --cwd /srv/project --prompt-file task.md --json`.
|
|
276
|
+
Matching `agy` versions are required on both hosts. Bubblewrap is mandatory;
|
|
277
|
+
each attempt has a memory-backed HOME and mounts only the worker's mode-600
|
|
278
|
+
`.gemini/antigravity-cli/antigravity-oauth-token` authentication file, read-only.
|
|
279
|
+
The installation is read-only. No token is copied into the volatile HOME or sent
|
|
280
|
+
to the controller. A worker token lock excludes concurrent cdx attempts;
|
|
281
|
+
independent native agy launches must be kept out of the same worker account slot.
|
|
282
|
+
`AGY_CLI_DISABLE_AUTO_UPDATE=true` is set. Runs use `-p --output-format json`,
|
|
283
|
+
with closed stdin, and require JSON status `SUCCESS` without refused actions.
|
|
284
|
+
Missing login and expired-session errors require operator login on the worker;
|
|
285
|
+
cdx does not launch an authentication command. Usage is read from the JSON result.
|
|
286
|
+
|
|
287
|
+
The conservative HTTPS candidate list is `oauth2.googleapis.com`,
|
|
288
|
+
`cloudcode-pa.googleapis.com` and `daily-cloudcode-pa.googleapis.com`, resolved
|
|
289
|
+
on the worker before isolation, plus DNS to `1.1.1.1`/`9.9.9.9`. Other public
|
|
290
|
+
traffic is dropped even with `--network-access`; private ranges remain denied.
|
|
291
|
+
This list has **not** been accepted against live inference in the mission sandbox.
|
|
292
|
+
The token remains read-only pending a measured receipt showing whether refresh
|
|
293
|
+
writes it in place. See
|
|
294
|
+
`logics/runbook/fragments/2026-11-03-antigravity-44674fc58c.md` for evidence and
|
|
295
|
+
the host measurement command. Local Antigravity headless runs retain their profile HOME
|
|
296
|
+
and credentials; the worker login, token lock and volatile HOME policy apply only
|
|
297
|
+
to remote runs.
|
|
298
|
+
|
|
299
|
+
Remote Ollama profiles use the worker's unauthenticated HTTP API at
|
|
300
|
+
`127.0.0.1:11434`, with `/api/show` followed by `/api/chat` (`stream: false`).
|
|
301
|
+
Use `cdx run local --remote worker --cwd /srv/project --model qwen3:8b --prompt-file question.txt --isolate-workspace --json`.
|
|
302
|
+
The prompt file is read on the launching host; the model must already exist on
|
|
303
|
+
the worker. This is text generation only, not an autonomous coding agent, and
|
|
304
|
+
it does not edit the repository. The result includes `execution_mode`, an
|
|
305
|
+
`execution_note`, generated text in `result`, and `prompt_eval_count` / `eval_count`
|
|
306
|
+
alongside normalized `usage` counters. Missing models and unreachable APIs fail
|
|
307
|
+
without pulling models or authenticating. Isolated clients use a private Unix
|
|
308
|
+
socket relay restricted to capability and non-streaming inference calls for the
|
|
309
|
+
selected model, with a read-only checkout and no network or model-file mounts.
|
|
310
|
+
`--network-access` does not enable networking for this text-only client.
|
|
311
|
+
|
|
243
312
|
Known headless and completed interactive token usage is persisted in launch history and can be aggregated later. Interactive records are best-effort local data: Codex reads its latest cumulative rollout snapshot, Claude reads its assistant usage records, and Ollama — which writes no transcript of its own — is launched with `--verbose` so its per-response token counts land in the terminal capture cdx already takes. Antigravity reports no usage: its history is schema-less binary protobuf and its logs carry no usage line, so those sessions show no tokens by nature rather than by omission.
|
|
244
313
|
|
|
245
314
|
`--continue-run RUN_ID` resumes the exact provider conversation from a finished Claude or Codex run, inferring its profile and pinning its recorded model and effort. Existing targets and settings may be supplied only if they match. The parent transcript must still exist. `parent_run_id` links the new run to its parent in run results, the registry, and history; `conversation_id` identifies the provider conversation. Detached continuations preserve these settings through the cdx supervisor. Codex continuation usage is measured between exact transcript snapshots before and after the run, since exec resume can report conversation totals. If either boundary is unavailable, per-run usage stays unknown with `usage_attribution: "continuation_usage_unavailable"` in history instead of counting earlier turns again. Continuation does not use automatic capacity recovery or model fallback, and cannot be combined with `--failover` or `--isolate-workspace`.
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Local Antigravity statusLine capture; no credential or network access."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
STATE_NAME = "cdx-quota.json"
|
|
15
|
+
MARKER = "--cdx-antigravity-status"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _directory(home):
|
|
19
|
+
return os.path.join(home, ".gemini", "antigravity-cli")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _date(value):
|
|
23
|
+
try:
|
|
24
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
25
|
+
return parsed if parsed.tzinfo is not None else None
|
|
26
|
+
except (ValueError, TypeError):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _quota(value):
|
|
31
|
+
result = {}
|
|
32
|
+
if not isinstance(value, dict):
|
|
33
|
+
return result
|
|
34
|
+
for name, bucket in value.items():
|
|
35
|
+
if not isinstance(bucket, dict):
|
|
36
|
+
continue
|
|
37
|
+
clean = {}
|
|
38
|
+
fraction = bucket.get("remaining_fraction")
|
|
39
|
+
if isinstance(fraction, (int, float)) and not isinstance(fraction, bool) and math.isfinite(fraction) and 0 <= fraction <= 1:
|
|
40
|
+
clean["remaining_fraction"] = fraction
|
|
41
|
+
reset = _date(bucket.get("reset_time"))
|
|
42
|
+
if reset:
|
|
43
|
+
clean["reset_time"] = reset.isoformat()
|
|
44
|
+
seconds = bucket.get("reset_in_seconds")
|
|
45
|
+
if isinstance(seconds, (int, float)) and not isinstance(seconds, bool) and math.isfinite(seconds) and seconds >= 0:
|
|
46
|
+
clean["reset_in_seconds"] = seconds
|
|
47
|
+
result[name] = clean
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def capture(payload, state_path, now=None):
|
|
52
|
+
"""Persist only an allowlist, atomically with private permissions."""
|
|
53
|
+
if not isinstance(payload, dict):
|
|
54
|
+
return
|
|
55
|
+
now = now or datetime.now(timezone.utc)
|
|
56
|
+
state = {"time": now.isoformat(),
|
|
57
|
+
"plan_tier": payload.get("plan_tier") if isinstance(payload.get("plan_tier"), str) else None,
|
|
58
|
+
"quota": _quota(payload.get("quota"))}
|
|
59
|
+
directory = os.path.dirname(state_path)
|
|
60
|
+
os.makedirs(directory, exist_ok=True)
|
|
61
|
+
fd, temporary = tempfile.mkstemp(prefix=".cdx-quota-", dir=directory)
|
|
62
|
+
try:
|
|
63
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
64
|
+
json.dump(state, handle)
|
|
65
|
+
handle.write("\n")
|
|
66
|
+
os.replace(temporary, state_path)
|
|
67
|
+
finally:
|
|
68
|
+
if os.path.exists(temporary):
|
|
69
|
+
os.unlink(temporary)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def install(home):
|
|
73
|
+
"""Merge the hook without replacing unrelated settings or user output."""
|
|
74
|
+
from .agent_notify import _read_json, _write_json
|
|
75
|
+
|
|
76
|
+
path = os.path.join(_directory(home), "settings.json")
|
|
77
|
+
settings = _read_json(path)
|
|
78
|
+
if settings is None:
|
|
79
|
+
return False
|
|
80
|
+
status = settings.get("statusLine") or {}
|
|
81
|
+
if not isinstance(status, dict) or (status and status.get("type") != "command"):
|
|
82
|
+
return False
|
|
83
|
+
previous = status.get("command") or ""
|
|
84
|
+
if not isinstance(previous, str):
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
parts = shlex.split(previous, posix=os.name != "nt")
|
|
88
|
+
except ValueError:
|
|
89
|
+
parts = []
|
|
90
|
+
own_command = (len(parts) >= 3 and os.path.basename(parts[1].strip('"')) == "antigravity_status.py"
|
|
91
|
+
and parts[2] == MARKER)
|
|
92
|
+
if own_command:
|
|
93
|
+
index = parts.index("--previous") + 1 if "--previous" in parts else len(parts)
|
|
94
|
+
encoded = parts[index] if index < len(parts) else ""
|
|
95
|
+
else:
|
|
96
|
+
encoded = base64.urlsafe_b64encode(previous.encode()).decode()
|
|
97
|
+
args = [sys.executable, os.path.abspath(__file__), MARKER,
|
|
98
|
+
"--state", os.path.join(_directory(home), STATE_NAME)]
|
|
99
|
+
if encoded:
|
|
100
|
+
args += ["--previous", encoded]
|
|
101
|
+
command = subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args)
|
|
102
|
+
merged = {**status, "type": "command", "command": command}
|
|
103
|
+
if settings.get("statusLine") == merged:
|
|
104
|
+
return True
|
|
105
|
+
return _write_json(path, {**settings, "statusLine": merged})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_status(home, now=None):
|
|
109
|
+
"""Expose every bucket; use only fresh Gemini windows for availability."""
|
|
110
|
+
now = now or datetime.now(timezone.utc)
|
|
111
|
+
try:
|
|
112
|
+
with open(os.path.join(_directory(home), STATE_NAME), encoding="utf-8") as handle:
|
|
113
|
+
state = json.load(handle)
|
|
114
|
+
except (OSError, ValueError):
|
|
115
|
+
state = {}
|
|
116
|
+
if not isinstance(state, dict):
|
|
117
|
+
state = {}
|
|
118
|
+
at = _date(state.get("time"))
|
|
119
|
+
age = max(0, (now - at).total_seconds()) if at else None
|
|
120
|
+
stale = age is not None and age > 24 * 60 * 60
|
|
121
|
+
quota = _quota(state.get("quota"))
|
|
122
|
+
for bucket in quota.values():
|
|
123
|
+
reset = _date(bucket.get("reset_time"))
|
|
124
|
+
valid = at is not None and not stale and reset is not None and reset > now
|
|
125
|
+
bucket["status"] = "stale" if stale else "current" if valid else "unknown"
|
|
126
|
+
if not valid:
|
|
127
|
+
bucket["remaining_fraction"] = None
|
|
128
|
+
result = {"updated_at": at.isoformat() if at else None, "reading_age_seconds": age,
|
|
129
|
+
"quota_stale": stale, "quota_status": "stale" if stale else "current" if at else "unknown",
|
|
130
|
+
"plan_tier": state.get("plan_tier"), "quota": quota}
|
|
131
|
+
for window, name in (("5h", "gemini-5h"), ("week", "gemini-weekly")):
|
|
132
|
+
bucket = quota.get(name, {})
|
|
133
|
+
reset = _date(bucket.get("reset_time"))
|
|
134
|
+
valid = at is not None and not stale and reset is not None and reset > now
|
|
135
|
+
fraction = bucket.get("remaining_fraction") if valid else None
|
|
136
|
+
result[f"remaining_{window}_pct"] = fraction * 100 if fraction is not None else None
|
|
137
|
+
result[f"reset_{window}_at"] = reset.astimezone().strftime("%b %d %H:%M") if valid else None
|
|
138
|
+
result[f"reset_{window}_at_iso"] = reset.isoformat() if valid else None
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main(stdin=None):
|
|
143
|
+
parser = argparse.ArgumentParser()
|
|
144
|
+
parser.add_argument(MARKER, action="store_true")
|
|
145
|
+
parser.add_argument("--state", required=True)
|
|
146
|
+
parser.add_argument("--previous", default="")
|
|
147
|
+
args = parser.parse_args()
|
|
148
|
+
raw = (stdin or sys.stdin).read()
|
|
149
|
+
try:
|
|
150
|
+
capture(json.loads(raw), args.state)
|
|
151
|
+
except (ValueError, OSError, TypeError):
|
|
152
|
+
# A failed capture must not suppress the user's existing status line.
|
|
153
|
+
pass
|
|
154
|
+
if args.previous:
|
|
155
|
+
command = base64.urlsafe_b64decode(args.previous).decode()
|
|
156
|
+
return subprocess.run(command, shell=True, input=raw, text=True, check=False).returncode
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Dedicated controller-side Claude setup token, never a worker profile."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import stat
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .errors import CdxError
|
|
10
|
+
|
|
11
|
+
TOKEN_FILE = '.cdx-remote-claude-token'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def store_token(session, token):
|
|
15
|
+
token = token.strip()
|
|
16
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,16384}', token):
|
|
17
|
+
raise CdxError('Provide one Claude setup token on standard input.')
|
|
18
|
+
home = Path(session['authHome'])
|
|
19
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
descriptor, temporary = tempfile.mkstemp(prefix='.claude-token-', dir=home)
|
|
21
|
+
try:
|
|
22
|
+
with os.fdopen(descriptor, 'w') as handle:
|
|
23
|
+
handle.write(token)
|
|
24
|
+
os.replace(temporary, home / TOKEN_FILE)
|
|
25
|
+
finally:
|
|
26
|
+
if os.path.exists(temporary):
|
|
27
|
+
os.unlink(temporary)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_token(session):
|
|
31
|
+
try:
|
|
32
|
+
path = Path(session['authHome']) / TOKEN_FILE
|
|
33
|
+
with open(path, opener=lambda name, flags: os.open(name, flags | getattr(os, "O_NOFOLLOW", 0))) as handle:
|
|
34
|
+
info = os.fstat(handle.fileno())
|
|
35
|
+
if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600 or (hasattr(os, "getuid") and info.st_uid != os.getuid()):
|
|
36
|
+
raise ValueError('Unsafe token file')
|
|
37
|
+
token = handle.read(16385).strip()
|
|
38
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,16384}', token):
|
|
39
|
+
raise ValueError('Invalid token')
|
|
40
|
+
return token
|
|
41
|
+
except (OSError, ValueError) as error:
|
|
42
|
+
raise CdxError(
|
|
43
|
+
f"Remote Claude requires a dedicated setup token; run cdx auth claude-token {session['name']} "
|
|
44
|
+
"with the token from claude setup-token on standard input.", code='remote_claude_token_missing',
|
|
45
|
+
) from error
|