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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <br clear="left"/>
6
6
 
7
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.25.8-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
7
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.26.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
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
- Requirements: a POSIX controller with `ssh` and Codex on PATH, and a Linux worker
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. Only Codex OAuth
114
- profiles are supported. Remote continuation is refused because the temporary home
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
- `--remote` supports `--isolate-workspace` using the same local isolation mechanism:
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.25.8
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, fail unverified updates unless an unchanged version is explicitly confirmed current by the updater, and expose before/after versions without hiding major upgrades.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.25.8",
3
+ "version": "0.26.0",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.25.8"
7
+ version = "0.26.0"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9.2"
@@ -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