cdx-manager 0.25.1 → 0.25.3

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.1-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.25.3-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
 
@@ -88,3 +88,101 @@ Recovery does not require `--failover`, which still controls account quota exhau
88
88
  - [Tray companion testing](docs/tray-local-testing.md) — local development loops.
89
89
 
90
90
  Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). CDX Manager is [MIT licensed](LICENSE).
91
+
92
+ ### Run Codex on an SSH worker
93
+
94
+ ```sh
95
+ cdx run work --remote agent@worker --cwd /srv/project --prompt 'Run the checks' --json
96
+ cdx run work --remote worker --cwd /srv/project --prompt-file task.txt --detach --label checks --budget 2 --json
97
+ cdx run work --remote Spire --remote-prefix 'wsl.exe -d Ubuntu --' --cwd /srv/project --prompt 'Run the checks' --json
98
+ ```
99
+
100
+ The controller remains the authority for the profile, quota, history, costs and
101
+ run registry. Use `run-status`, `run-report` and `run-tail` on the controller with
102
+ the returned run ID. Permission, model, reasoning effort, prompt, timeout, budget,
103
+ label and detached execution use the existing run options. `--cwd` names an
104
+ absolute directory on the worker; it need not exist on the controller. Files are
105
+ not synchronized. Use `--project` to explicitly attribute a remote checkout to a
106
+ project. Remote runs do not retry or fail over automatically, including when
107
+ `--failover` is supplied.
108
+
109
+ Requirements: a POSIX controller with `ssh` and Codex on PATH, and a Linux worker
110
+ with Python 3, memory-backed `/dev/shm`, and exactly the same `codex --version`.
111
+ Configure SSH keys, host trust and aliases in advance; the connection uses batch
112
+ 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
115
+ does not preserve conversation rollouts. Codex's native permission sandbox still
116
+ applies to ordinary remote runs.
117
+
118
+ For SSH servers landing in Windows `cmd.exe` (including Spire and Citadel), pass
119
+ `--remote-prefix 'wsl.exe -d Ubuntu --'` to enter Linux before starting Python.
120
+ The prefix accepts plain command words, options and paths without spaces or shell
121
+ metacharacters; it is forwarded to detached runs. POSIX targets need no prefix.
122
+ Only a short, double-quoted Python bootstrap is sent in the SSH command line;
123
+ the worker and shared sandbox source are framed on stdin before the payload.
124
+ Neither source size nor prompt size consumes cmd.exe's 8191-character command limit.
125
+
126
+ `--remote` supports `--isolate-workspace` using the same local isolation mechanism:
127
+ bubblewrap, pasta (passt), nftables, Python, the checkout/toolchain allowlist,
128
+ and an actual namespace/network setup using a dummy credential. Missing dependencies,
129
+ namespace denial or invalid checkouts fail before sending real credentials, without
130
+ an unisolated fallback.
131
+
132
+ For **every isolated Codex run**, local or remote, the sandbox mounts only a
133
+ run-private, mode-600 `auth.json` on verified memory-backed storage, with an empty
134
+ `refresh_token`. It never mounts the controller profile's full authentication file.
135
+ The controller refreshes under the authentication lock at the thresholds below
136
+ before preparing the snapshot, then releases the lock for the run. The temporary
137
+ home is removed after success, failure, launch errors and stop. Non-isolated local
138
+ runs and interactive login/refresh retain their normal profile authentication.
139
+
140
+ **Accepted residual risk (operator decision, 2026-11-01):** agent tools inside the
141
+ sandbox can read the copied access token, which can remain valid for up to ten
142
+ days. A read-only mount prevents modification, not disclosure. The rotating refresh
143
+ token never enters the sandbox or leaves the controller. Codex was measured to run
144
+ with an empty refresh token on 2026-11-01.
145
+
146
+ A long isolated Tower mission keeps its original credential snapshot even when
147
+ scheduled controller refresh updates the real profile. There is no authentication
148
+ writeback or token renewal inside the sandbox. A run that outlives its access token
149
+ fails with a clear temporary-credential expiry error and must start a new run.
150
+
151
+ The controller takes the profile's authentication lock and refreshes through its
152
+ local [Codex app-server](https://learn.chatgpt.com/docs/app-server) when `last_refresh` is older than seven days or the access
153
+ token has less than 24 hours left. It verifies freshness after refreshing and
154
+ fails closed if freshness or expiration cannot be established. Only
155
+ `access_token`, `id_token`, `account_id` and `last_refresh` travel, with an empty
156
+ `refresh_token`. The rotating refresh token never leaves the controller. The
157
+ credential travels exclusively over SSH stdin, and is absent from command
158
+ arguments, environment variables, run records and output. The agent receives
159
+ `/dev/null` as stdin, not the bootstrap script or credential transport.
160
+
161
+ Each worker run gets a private mode-700 home under
162
+ `/dev/shm/cdx-remote-<uid>/run-*`, with mode-600 `auth.json` and file-only credential storage. Only non-secret scalar
163
+ preferences (`model`, `model_reasoning_effort`, `service_tier`, `personality`,
164
+ `model_context_window`, `model_auto_compact_token_limit`) from the top level of
165
+ `config.toml` are copied. Hooks, custom providers, environment settings and other
166
+ controller-local configuration are not copied. The home is removed on completion,
167
+ failure, cancellation and SSH stdin closure. Stop a detached run by sending
168
+ SIGTERM to the controller PID returned at launch. Uncatchable worker termination
169
+ can leave a home behind; the next launch sweeps homes whose worker lock is no
170
+ longer held. No persistent worker authentication is installed or refreshed.
171
+
172
+ A profile occupied by a remote run is busy for local and remote runs using the
173
+ same controller through the run registry. The authentication lock is held only
174
+ while refreshing and validating the credential snapshot, then released before SSH
175
+ launch. Scheduled `cdx auth refresh` and quota probes can acquire it during a long
176
+ mission; they no longer see a mission-long `locked`/`auth_refresh_indeterminate`
177
+ result. A probe overlapping the short snapshot can still report contention.
178
+ Controller renewal does not extend the worker's copied access token; quota remains
179
+ one controller-side account snapshot, never a worker copy. Multiple independent controllers sharing copied OAuth profiles are not
180
+ supported. Costs and budget checks use measured worker usage and observed rollout
181
+ models; unknown prices and delayed provider reporting retain the usual budget
182
+ limitations. A run that outlives the access credential fails with
183
+ `remote_credential_expired`, requiring a new controller launch.
184
+
185
+ The empty-refresh-token behavior was measured with Codex CLI 0.166.0. Real-host
186
+ acceptance should verify that the installed matching CLI does not attempt a
187
+ refresh with the temporary home, that the controller token chain stays unchanged
188
+ for a fresh credential, and that cleanup works after disconnect and SIGTERM.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.25.1
1
+ 0.25.3
@@ -0,0 +1,15 @@
1
+ # CDX Manager 0.25.2
2
+
3
+ Release date: 2026-10-31
4
+
5
+ This patch release improves usage attribution and makes unverifiable run budgets fail explicitly.
6
+
7
+ ## Fixes
8
+
9
+ - Preserve observed Codex model evidence for usage pricing. Keep costs unknown when model evidence is missing or mixed without per-model counters, and fail unverifiable budgets with `budget_unverifiable` and exit code 125. (#76)
10
+ - Attribute CDX usage to canonical source repositories so project statistics group runs by their source repository rather than isolated execution directories. (#75)
11
+
12
+ ## Maintenance
13
+
14
+ - Record tray companion checksums for v0.25.1. (`e3530977b2b95cd3ec39bbe962725f275fef4370`)
15
+ - Record release archive checksums for v0.25.1. (`59b5d276abc5877554d9ffc44b468b53b12dd1d4`)
@@ -0,0 +1,20 @@
1
+ # CDX Manager 0.25.3
2
+
3
+ Release date: 2026-11-01
4
+
5
+ This patch release adds remote Codex execution over SSH and strengthens credential isolation for local and remote sandboxes.
6
+
7
+ ## Features
8
+
9
+ - Run Codex on a Linux SSH worker with temporary credentials while keeping profiles, quota, history, costs, and run tracking on the controller. Support detached execution and clean up the temporary worker home after the run. (#78)
10
+
11
+ ## Fixes and security
12
+
13
+ - Keep rotating refresh tokens on the controller and out of local and remote sandboxes. Use temporary access-token snapshots on verified memory-backed storage, refresh and validate credentials before launch, and fail explicitly when a snapshot expires. (#81)
14
+ - Upgrade cryptography and pytest to address security alerts while preserving Python 3.9 compatibility through a version-specific pytest requirement. (#80)
15
+ - Fix remote bootstrapping through Windows SSH shells and WSL without exceeding the command-line length limit. Check isolation prerequisites before sending credentials and refuse unsafe isolation without an unisolated fallback. (#79)
16
+
17
+ ## Maintenance
18
+
19
+ - Record tray companion checksums for v0.25.2. (`4f810c3801418bc0ad8a480333cc0bdfda39383e`)
20
+ - Record release archive checksums for v0.25.2. (`b33eba1894d9cc68d395133848beccae38589a68`)
@@ -1,7 +1,35 @@
1
1
  # Unreleased
2
2
 
3
+ ## Dependency security
4
+
5
+ - Require cryptography >=50.0.0 for encrypted auth bundles and remove the vulnerable 47.0.0 lock entry. Python 3.9 remains supported from 3.9.2, as cryptography 50 excludes 3.9.0 and 3.9.1.
6
+ - Require pytest >=9.0.3 in the development extra on Python >=3.10; retain the existing 9.2.0 lock entry. pytest is not a runtime dependency. Python 3.9 still uses 8.4.2, the last compatible release, and its medium-severity alert remains unresolved pending an upstream backport or a decision to drop Python 3.9 development support.
7
+ - Check lock consistency and run the CI test matrix against locked development dependencies without upgrading unrelated packages.
8
+
3
9
  ## Run costs and budgets
4
10
 
5
11
  - Add nullable `cost_usd` to JSON history entries using the same observed usage, model pricing, and price overrides as stats.
6
12
  - Add `cdx run --budget USD` to stop on measured estimated spend with exit code 125, `budget_exceeded`, and `error.spent_usd` in the final JSON. Share the budget across recovery attempts and failover; support detached runs and fresh budgets for continued runs without charging earlier turns.
7
13
  - Document unknown usage/prices and delayed provider reporting, including possible budget overshoot. Cover pricing, unknown costs, validation, live process termination, Claude transcript accumulation, continuation boundaries, and detached supervision in tests.
14
+
15
+ ## Remote Codex runs
16
+
17
+ - Add `cdx run --remote SSH_TARGET` with controller-owned supervision, registry, history, output, costs, budgets and detached execution.
18
+ - Refresh under the controller authentication lock at the seven-day/24-hour thresholds, then send a temporary OAuth credential over SSH stdin with an empty refresh token. Refuse rotating credentials, other providers and CLI version mismatches.
19
+ - Keep worker credentials in a private memory-backed home, clean up after success, failure and stop, and sweep dead homes on the next launch. Keep agent stdin separate from bootstrap input and report credential expiration without retrying.
20
+ - Cover transport and credential isolation, thresholds, cleanup, sweep, unreachable workers, provider refusal and registry reports with a fake SSH worker.
21
+
22
+ ## Remote worker transport and isolation preflight
23
+
24
+ - Add `--remote-prefix` for Windows SSH landings such as `wsl.exe -d Ubuntu --`, including detached runs. Keep the POSIX default and use a short double-quoted bootstrap on argv; send framed worker/sandbox source and then credentials/payload over stdin.
25
+ - Probe the local bubblewrap/pasta/nftables isolation mechanism on the worker with dummy credentials, including namespace/network startup. Refuse missing dependencies or setup failures without falling back to an unisolated run.
26
+ - Enable remote isolated Codex execution after a successful isolation probe, following the operator decision of 2026-11-01. Agent tools can read the copied access token (up to ten days of validity); missing dependencies still refuse without fallback.
27
+ - Release the controller authentication lock after preparing the credential snapshot so scheduled authentication refresh can run during long remote missions. Keep profile mission exclusion in the run registry and the worker credential's original expiry.
28
+ - Test cmd.exe length/quoting enforcement, Windows prefixes, large stdin payloads, POSIX transport, sanitized credential snapshots, sandbox construction/dependencies and concurrent scheduled refresh with synthetic credentials.
29
+
30
+ ## Isolated Codex credentials
31
+
32
+ - Replace the local full-profile auth mount with a mode-600 run-private snapshot on verified memory-backed storage, containing an empty refresh token. Refresh on the controller under the auth lock at the existing seven-day/24-hour thresholds before launch.
33
+ - 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.
34
+ - 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.
35
+ - 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.
@@ -694,6 +694,28 @@
694
694
  "x86_64-pc-windows-msvc": "2493b0c8212ccdbf5e69756af07afe47c73474407f79604d671142d5192aa642",
695
695
  "x86_64-unknown-linux-musl": "53024f4908847452ce6ac399ca03725b95b628df721856da6e2ed9399691455f"
696
696
  }
697
+ },
698
+ "v0.25.1": {
699
+ "github_tarball_sha256": "7899a8cb3f3eb2d83d10b921a4c22c16f1d7eba4681c564b7b09812699892ad6",
700
+ "github_zip_sha256": "19f9646e1e74761f588b9ef195a2a90914a48ad2621a67f9d85057417db8cd19",
701
+ "tray_assets": {
702
+ "aarch64-apple-darwin": "d5d5fd19ad3a2e8bd3bcd72a959a217bbfa2fea351bcd739b94265ceb1fdd2c4",
703
+ "aarch64-unknown-linux-musl": "e8824cd7ec91e81bf7be1f2af53689de0aa8bbb9f7037d9e2e51fed35837a8a3",
704
+ "x86_64-apple-darwin": "d507a6f35894a7125cef7dc67f98e485e08774221d9dc92698c11b0f9c5354c3",
705
+ "x86_64-pc-windows-msvc": "183ef3664489888a356ad9a6096b68c913d433c513d6d10e9890e65667e1e417",
706
+ "x86_64-unknown-linux-musl": "5660cfd08c0580db43dee5664fe95fc4cd528a6495c40167e34c122d3c4fcd4a"
707
+ }
708
+ },
709
+ "v0.25.2": {
710
+ "github_tarball_sha256": "8ed02d4ce31ca6aa9669396675ee1d5b6669e80f33418c00c2a210306ea19e0d",
711
+ "github_zip_sha256": "0e8770a0e18c6c99bd5d845c8c54d90eb33fbf71720be89d87396eaee1632cfe",
712
+ "tray_assets": {
713
+ "aarch64-apple-darwin": "3c0cc113b70670e0f1bd9eff4589cddd8a5e3f76d7b53d7a1d66a2529f5e6e84",
714
+ "aarch64-unknown-linux-musl": "ff978672276fdfa8f22af8d3dadb63e0d26fa40c6f10d48f834073b08c620efd",
715
+ "x86_64-apple-darwin": "42323cbce340238ada8d986192d198a82e37d5958fb25b56cbaf8dba347da05c",
716
+ "x86_64-pc-windows-msvc": "ffcd4deb8b3843771659d2c6b92b003822b288329ddcf3b9a13f0f9820fa3df1",
717
+ "x86_64-unknown-linux-musl": "2cefeea0aad5bb0acfcbc99146ffacc5c5da9386f8338e4a01541e97ccf71e07"
718
+ }
697
719
  }
698
720
  }
699
721
  }
package/docs/cli.md CHANGED
@@ -28,6 +28,7 @@
28
28
  | `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power minimal\|low\|medium\|high\|xhigh] [--permission review\|default\|auto\|full] [--fast on\|off] [--rtk on\|off] [--logics on\|off] [--notify on\|off] [--agent-state on\|off] [--notify-preview on\|off] [--scrub-env on\|off] [--shared-memory on\|off] [--retro on\|off] [--policy use\|ask-first\|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
29
29
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--reasoning-effort\|--permission\|--fast\|--rtk\|--logics\|--notify\|--agent-state\|--notify-preview\|--scrub-env\|--shared-memory\|--retro\|--policy\|--model\|--fallback-model\|--budget\|--extra-args\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
30
30
  | `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
31
+ | `cdx history backfill-projects [--mapping FILE] [--dry-run \| --apply] [--json]` | Recover source repository attribution from exact recorded run evidence or an operator mapping; defaults to a read-only preview |
31
32
  | `cdx history backfill-models [--dry-run \| --apply] [--json]` | Recover missing historical usage models from evidence tied to each run; defaults to a read-only preview |
32
33
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
33
34
  | `cdx resume <name> [--conversation <id>] [--json]` | Resume the provider-native conversation for a session using the named command form; `--conversation` reopens that exact conversation and refuses with `conversation_not_found` if the profile has no such transcript |
@@ -62,7 +63,7 @@
62
63
  | `cdx run-tail <run_id> [--lines N] --json` | Last lines of a run's own output, while it is still running or after it finished |
63
64
  | `cdx runs [--limit N] [--since 7d\|today\|DATE] [--running] --json` | List recent runs; `--since` returns every run completed after the cursor and ignores `--limit`; `--running` returns every run still running, uncapped. A run's `kind` is one of the values `cdx schema --json` lists under `enums.kind` |
64
65
  | `cdx schema --json` | Publish the enums, mutually-exclusive argument groups, and error codes programmatic callers should validate against |
65
- | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--by session\|project\|model] [--json]` | Aggregate launch counts, duration, and known token usage by session, for headless and interactive runs alike. `--by project` groups runs by launch folder (labelled by the shortest trailing part that tells folders apart; `project` in JSON holds the full path), `--by model` by the model that served them (same totals, same pricing; default `session`). PROV. lists every provider a row saw, such as `claude+codex`. Without a session filter it also prints what rtk saved over the period (`rtk` in JSON), read with `rtk gain` from `RTK_DB_PATH` when set, else the shared history, and the older per-profile ones. |
66
+ | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--by session\|project\|model] [--json]` | Aggregate launch counts, duration, and known token usage by session, for headless and interactive runs alike. `--by project` groups runs by the recorded canonical source repository (labelled by the shortest distinguishing path suffix; `project` in JSON holds the canonical identity, `session_name` the display label, and `-` explicitly means unknown), `--by model` by the model that served them (same totals, same pricing; default `session`). PROV. lists every provider a row saw, such as `claude+codex`. Without a session filter it also prints what rtk saved over the period (`rtk` in JSON), read with `rtk gain` from `RTK_DB_PATH` when set, else the shared history, and the older per-profile ones. |
66
67
  | `cdx status [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show token usage table for all sessions; empty credits, block, and reset columns are omitted; `--cached` skips live provider probes and returns only stored status; Claude rows also report the auth type (`setup-token` or `full-login`) in a TYPE column and as `auth_type` in JSON. |
67
68
  | `cdx status --small [--refresh\|--cached] [--timeout SECONDS]` / `cdx status -s [--refresh\|--cached] [--timeout SECONDS]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
68
69
  | `cdx status <name> [--json] [--refresh\|--cached] [--timeout SECONDS]` | Show detailed usage breakdown for one session; `--cached` avoids live provider refreshes |
@@ -253,7 +254,7 @@ At or above the limit, cdx terminates the provider process tree, skips further r
253
254
 
254
255
  With `--detach`, the launch still exits successfully and includes `budget_usd`; cdx always uses its own child supervisor for budgeted runs, including when native background delegation is enabled. Inspect `cdx run-status RUN_ID --json` or `cdx run-report RUN_ID --json` for the child's final payload and budget error. `--continue-run` starts a fresh budget for the new run, including in detached mode.
255
256
 
256
- Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Missing usage, missing continuation boundaries, mixed/unobserved models, and unknown prices are not treated as free tokens and cannot trigger the budget; their estimated cost stays `null`. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
257
+ Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Before the first measurement, `budget_status` is `usage_not_measured`; absence is not zero spend. Once usage is measured, missing continuation boundaries, mixed/unobserved models, and unknown prices stop further execution/recovery with exit **125**, `error.source: "cdx"`, `error.code: "budget_unverifiable"`, `budget_status: "unverifiable"`, and `cost_usd: null`. This is a measurement failure, not a claim that the dollar limit was exceeded or enforced. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
257
258
 
258
259
  Every usage record, whichever path produced it, uses one set of definitions:
259
260
 
@@ -289,6 +290,10 @@ The figure uses standard public list prices, excluding Fast/priority, Batch/Flex
289
290
 
290
291
  `cdx stats` counts completed history attempts. A headless failover has one logical run in `cdx runs` but one history row per attempted session, including failed attempts that reported usage. Period filtering includes the entire run in every period it overlaps, so adjacent daily results can both include a run crossing midnight; adding daily totals may exceed a combined period. JSON `accounting` names this policy and the current price-table source. `usage_runs` and `priced_runs` show how much of the token total has a dollar estimate. Live runs are absent until their history row is written, and unknown usage is different from measured zero.
291
292
 
293
+ Completed results, registry records, and history JSON include `cost_status` (`estimated` or `unknown`) and nullable `cost_reason`: `usage_not_measured`, `model_not_observed`, `model_price_unknown`, `attempt_usage_unavailable`, or `mixed_models_without_counters`. Stats include `unpriced_reasons` counts alongside `priced_runs` and `unpriced_models`. These are list-price estimates, not billing receipts. `budget_status` is `not_requested`, `usage_not_measured`, `estimated`, `exceeded`, or `unverifiable`.
294
+
295
+ New headless attempts have distinct `attempt_id` values and artifact paths, including retries on the same profile. Final payloads and registry reports expose `attempts` with their individual model evidence, counters, and estimates; top-level usage describes the final attempt, while a budget-exceeded spend includes all attempts. New isolated Codex attempts mount only a fresh sessions directory from volatile `/dev/shm` storage. The supervisor reads the exact `thread.started` rollout while it exists and at closeout, requiring matching session identity, turn timestamps within this attempt, and token counters consistent with stdout. It exports `usage_evidence` containing only the stdout attempt identity, thread id, timestamp bounds, turn ids, observed models, and normalized counters; live rollout counters can establish the first measurement before `turn.completed`. No requested model, agent-authored cost report, current profile setting, prompt, message, or tool output is used as model evidence. The raw temporary sessions are removed at closeout, including cancellation and startup failure; they are never copied into persistent run artifacts. Host profiles are not mounted. Mixed serving models stay unpriced without attributable counters. Isolated continuation without comparable launch boundaries stays unmeasured rather than charging an older conversation to the new attempt.
296
+
292
297
  Completed run JSON and registry reports expose `usage_model` separately from the requested `model`. Pricing uses the observed model from provider output; Codex can also supply it through the exact rollout identified by `thread.started.thread_id`. Configured defaults alone never establish serving-model identity, and missing or mixed model evidence stays unpriced. `--by model` groups Claude's `[1m]` context variant with its base model while preserving the original model string and per-run pricing. New runs record this evidence automatically. To recover missing models in existing history, run:
293
298
 
294
299
  ```bash
@@ -297,9 +302,9 @@ cdx history backfill-models --dry-run --json # Machine-readable preview
297
302
  cdx history backfill-models --apply # Save proven models with a backup
298
303
  ```
299
304
 
300
- The command considers only history rows with usage and an empty or placeholder `usage_model`. Evidence is checked in order: the row's preserved JSON/JSONL stdout, a preserved run-registry report with the same run id, profile, provider and stdout path, then the exact Codex thread rollout or Claude session transcript. Thread/session ids must come from the row or its stdout, or from a preserved transcript path recorded as an exact conversation-id match. The current profile is used only to locate an exact transcript under its auth home; its configured model and latest conversation/transcript never establish a historical model. The requested `model` in a report is also ignored.
305
+ The command considers only history rows with usage and an empty or placeholder `usage_model`. Evidence is checked in order: a supervisor-preserved isolated observation tied to the exact stdout attempt and counters, the row's preserved JSON/JSONL stdout, a preserved run-registry report with the same run id, profile, provider and stdout path, then the exact Codex thread rollout or Claude session transcript. Thread/session ids must come from the row or its stdout, or from a preserved transcript path recorded as an exact conversation-id match. The current profile is used only to locate an exact transcript under its auth home; its configured model and latest conversation/transcript never establish a historical model. The requested `model` in a report is also ignored.
301
306
 
302
- Only one serving model is accepted. Claude `[1m]` variants count as the same model for this check and for stats grouping; the observed spelling is preserved. Mixed models, incomplete turn models, malformed or oversized evidence, conflicting identities and missing exact transcripts stay unknown. For resumed conversations, the conservative rule is that all recorded turns in the exact transcript must agree: this command does not infer historical turn boundaries. Ambiguous higher-priority evidence stops recovery rather than being overridden by a lower-priority source. Missing files may fall through to the next source.
307
+ Run recovery in dry-run mode first and review every evidence kind and unknown reason before applying. A missing isolated rollout with no preserved observation cannot be reconstructed from a current profile: its historical model and cost remain unknown. No historical recovery is applied automatically. Only one serving model is accepted. Claude `[1m]` variants count as the same model for this check and for stats grouping; the observed spelling is preserved. Mixed models, incomplete turn models, malformed or oversized evidence, conflicting identities and missing exact transcripts stay unknown. For resumed conversations, the conservative rule is that all recorded turns in the exact transcript must agree: this command does not infer historical turn boundaries. Ambiguous higher-priority evidence stops recovery rather than being overridden by a lower-priority source. Missing files may fall through to the next source.
303
308
 
304
309
  Text and JSON output include counts by evidence kind, counts of unknown reasons and up to 20 samples. Dry-run leaves history and run reports unchanged. `--apply` changes only missing models and their evidence annotation, preserving token counts and unrelated history bytes. It holds the history lock, saves the original bytes to a unique `state/launch_history.jsonl.models-<id>.bak` file, then atomically replaces history. The backup path is printed; a repeated apply leaves already recovered rows alone. `--apply` and `--dry-run` cannot be combined. Subsequent `cdx stats` calls price recovered models when the local price table knows them; unknown models remain unpriced.
305
310
 
@@ -459,3 +464,84 @@ It returns accepted values for `permission` (with its aliases and canonical form
459
464
  Codex ties network access to its sandbox, so any permission below `full` leaves the run unable to resolve DNS — `gh`, `curl`, and package installs fail inside an otherwise successful run. Show these warnings to whoever reads your output rather than discarding them; a run that succeeds while quietly doing less is the case they exist for.
460
465
 
461
466
  Claude result records with nonempty `permission_denials` produce a warning with code `permission_denials`, the `provider`, and the structured `permission_denials` list. Exit code 0 still means `ok: true`: a denied tool may have been replaced successfully. Empty or absent denials add no warning. With `--scrub-env on`, Claude forces its default permission mode; allow tools needed by unattended runs explicitly using `--allowedTools` via `--extra-args` (see [operations](operations.md)).
467
+
468
+ ### Source repository attribution and external launchers
469
+
470
+ CDX captures `project` (an absolute canonical source repository path) and
471
+ `project_source` before starting a provider or applying workspace isolation.
472
+ Normal Git launches use the repository root, including launches from subfolders.
473
+ Linked Git worktrees use their common Git directory's source repository, so
474
+ normal and worktree launches share an identity even after worktree deletion.
475
+ Unrelated repositories with the same basename retain separate full-path keys.
476
+ Without Git evidence or an explicit source, attribution is unknown (`project:
477
+ null`, `project_source: "unknown"` in history; `project: "-"` in project stats).
478
+ Old cwd-only history also stays unknown: stats never inspect today's filesystem
479
+ or profile settings to guess yesterday's project.
480
+
481
+ Trusted producers such as Jarvis/remote-ops must resolve the source repository
482
+ **before** creating a disposable mission directory, worktree, or temporary clone,
483
+ and pass it to the installed CDX launcher:
484
+
485
+ ```bash
486
+ cdx run work --cwd /temporary/mission --project /repos/remote-ops --prompt 'Fix the issue' --isolate-workspace --json
487
+ cdx work --dir /temporary/mission --project /repos/remote-ops
488
+ ```
489
+
490
+ `--project` is a trusted operator assertion, not provider output, a profile
491
+ setting, or an environment variable. It must be an absolute normalized path,
492
+ with symlinks already resolved by the producer; it may refer to a deleted source.
493
+ A plain temporary clone has no reliable link to a local source repository: the
494
+ producer must supply `--project` rather than expecting inference from a remote
495
+ URL or directory naming convention. Existing launch-directory validation and
496
+ execution `cwd` are unchanged. CDX records the separate identity in history,
497
+ runtime/registry records, and launch/result JSON. Failover attempts and detached
498
+ children retain the launch identity. `--continue-run` inherits the parent's
499
+ recorded identity (including unknown) and rejects a different `--project`.
500
+ Neither continuation nor backfill reattributes an older run from current profile
501
+ configuration.
502
+
503
+ Consumers should use stats `project` as the stable grouping key and
504
+ `session_name` as the disambiguated display label. In particular, dependent
505
+ remote-ops `scripts/cdx-stats.py` should not reduce `project` to `Path(...).name`:
506
+ that loses disambiguation between unrelated repositories named alike. Token,
507
+ duration, cost, and attempt counts keep the existing accounting rules; the
508
+ existing `totals.sessions` field counts groups for the chosen grouping.
509
+
510
+ ### Recover historical project attribution
511
+
512
+ ```bash
513
+ cdx history backfill-projects --mapping projects.json # Preview (default)
514
+ cdx history backfill-projects --mapping projects.json --dry-run --json
515
+ cdx history backfill-projects --mapping projects.json --apply
516
+ ```
517
+
518
+ The mapping format is deliberately generic, with exact CDX run IDs, not mission
519
+ IDs, profile names, prefixes, or cwd values:
520
+
521
+ ```json
522
+ {"version":1,"projects":[{"run_id":"exact CDX run id","project":"/repos/remote-ops"}]}
523
+ ```
524
+
525
+ Operators must build this file from attributable external evidence, such as an
526
+ exact launch receipt. CDX never imports Jarvis, scans its private state, guesses
527
+ from disposable path prefixes, or resolves deleted worktrees. The supplied
528
+ Tower observation (84 disposable groups attributed by receipts to remote-ops
529
+ 66, cdx-manager 10, logics-manager 6, city-jump 2) is diagnostic evidence, not a
530
+ mapping: actual exact CDX run IDs must be obtained on Tower before repair.
531
+
532
+ Without a mapping, the command can recover only from trusted project fields
533
+ already preserved for the exact run in CDX history or its run registry. A cwd-only
534
+ report is insufficient evidence. All attempts sharing an exact run ID must agree.
535
+ Invalid schema, contradictory duplicate mapping entries, and unknown exact run
536
+ IDs fail validation. An attribution conflict with known history or recorded run
537
+ evidence blocks the entire batch, preserving the original history and returning
538
+ exit status 1 with the conflict counts.
539
+
540
+ Results report `recovered` (recoverable history rows), `unchanged`, `unresolved`,
541
+ and `conflicting` counts, plus `applied` and `backup_path`. Counts are history
542
+ attempts, matching existing history accounting. Dry runs write nothing. Apply
543
+ holds the existing history lock, writes an original-byte `.projects-*.bak`
544
+ backup, and atomically replaces history only when the complete batch has no
545
+ conflict. Unrelated fields, usage, and malformed/unrelated history lines survive.
546
+ Repeating the same mapping is a no-op and creates no additional backup. The
547
+ registry and provider transcripts are never rewritten by backfill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
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,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.25.1"
7
+ version = "0.25.3"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.9.2"
11
11
  license = "MIT"
12
12
  authors = [
13
13
  { name = "Alexandre Agostini" }
@@ -26,11 +26,13 @@ classifiers = [
26
26
  "Topic :: Software Development",
27
27
  "Topic :: Terminals",
28
28
  ]
29
- dependencies = ["cryptography>=42"]
29
+ dependencies = ["cryptography>=50.0.0"]
30
30
 
31
31
  [project.optional-dependencies]
32
32
  dev = [
33
- "pytest>=8",
33
+ # pytest 9 requires Python 3.10; 8.4.2 is the last Python 3.9 release.
34
+ "pytest>=8.4.2; python_version < '3.10'",
35
+ "pytest>=9.0.3; python_version >= '3.10'",
34
36
  "pytest-cov>=5",
35
37
  "ruff>=0.6",
36
38
  ]
@@ -70,6 +72,7 @@ show_missing = true
70
72
  skip_empty = true
71
73
 
72
74
  [tool.ruff]
75
+ extend-exclude = [".jarvis-input"]
73
76
  target-version = "py39"
74
77
  line-length = 140
75
78
 
package/src/cli.py CHANGED
@@ -245,7 +245,7 @@ def _print_help(use_color=False):
245
245
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
246
246
  f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
247
247
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
248
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
248
+ f" {_style('cdx run [session] [--remote SSH_TARGET] --cwd PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
249
249
  f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json', '36', use_color)}",
250
250
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
251
251
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -260,10 +260,11 @@ def _print_help(use_color=False):
260
260
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
261
261
  f" {_style(SET_USAGE[len('Usage: '):], '36', use_color)}",
262
262
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
263
+ f" {_style('cdx history backfill-projects [--mapping FILE] [--dry-run|--apply] [--json]', '36', use_color)}",
263
264
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
264
265
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
265
266
  f" {_style('cdx last [--json]', '36', use_color)}",
266
- f" {_style('cdx resume <name> [--conversation <id>] [--json]', '36', use_color)}",
267
+ f" {_style('cdx resume <name> [--project PATH] [--conversation <id>] [--json]', '36', use_color)}",
267
268
  f" {_style('cdx can-resume <name> [--conversation <id>] [--json]', '36', use_color)}",
268
269
  f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
269
270
  f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
@@ -295,7 +296,7 @@ def _print_help(use_color=False):
295
296
  _style("Notifications:", "1", use_color),
296
297
  f" {_style('cdx set <name> --notify on', '36', use_color)} # agent finishes or needs attention",
297
298
  f" {_style('cdx ready', '36', use_color)} # next session to become ready",
298
- f" {_style('cdx <name> [--on-busy fail|ask] [--json]', '36', use_color)}",
299
+ f" {_style('cdx <name> [--project PATH] [--on-busy fail|ask] [--json]', '36', use_color)}",
299
300
  " --on-busy: fail immediately if already running; ask (default) prompts only with a terminal and without --json",
300
301
  f" {_style('cdx <name> --prompt TEXT|--prompt-file PATH', '36', use_color)} # start with a first message",
301
302
  f" {_style('cdx <name> --dir PATH --task TEXT --end-state TEXT', '36', use_color)} # start on a task, with what the go-ahead covers",
@@ -637,7 +638,8 @@ def main(argv, options=None):
637
638
  if handler:
638
639
  return handler(rest, ctx)
639
640
 
640
- from .commands.launch import _parse_launch_args, _read_launch_prompt
641
+ from .commands.launch import _parse_launch_args, _pop_value_flag, _read_launch_prompt
642
+ project_path, rest = _pop_value_flag(rest, "--project", "Usage: cdx <name> [--project PATH]")
641
643
  directory, launch_args, prompt, on_busy = _parse_launch_args(rest)
642
644
  if all(arg in ("--json", "-r", "--resume") for arg in launch_args):
643
645
  resume = "-r" in launch_args or "--resume" in launch_args
@@ -648,7 +650,7 @@ def main(argv, options=None):
648
650
  if has_prompt and (ctx["service"]["get_session"](command) or {}).get("provider") == PROVIDER_OLLAMA:
649
651
  raise CdxError("cdx <name>: --prompt and --prompt-file are not supported for Ollama sessions.")
650
652
  return handle_launch(command, ctx, initial_prompt=_read_launch_prompt(prompt), resume=resume,
651
- directory=directory, on_busy=on_busy)
653
+ directory=directory, on_busy=on_busy, project_path=project_path)
652
654
 
653
655
  raise CdxError(f"Unknown command: {command}. Use cdx --help.")
654
656
 
package/src/cli_args.py CHANGED
@@ -43,12 +43,12 @@ HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|t
43
43
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
44
44
  STATS_GROUPS = ("session", "project", "model")
45
45
  LAST_USAGE = "Usage: cdx last [--json]"
46
- RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--on-busy fail|ask] [--json]"
46
+ RESUME_USAGE = "Usage: cdx resume <name> [--project PATH] [--conversation <id>] [--on-busy fail|ask] [--json]"
47
47
  CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
48
48
  SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
49
49
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
50
50
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
51
- RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
51
+ RUN_USAGE = "Usage: cdx run [session] [--remote SSH_TARGET] [--remote-prefix COMMAND] (--cwd|--dir) PATH [--project PATH] (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--network-access] [--isolate-workspace] [--timeout-seconds N] [--budget USD] [--label TEXT] [--max-parallel N] [--continue-run RUN_ID] [--detach] [--failover] [--refresh] --json"
52
52
  RUN_JSON_REQUIRED = "cdx run: --json is required."
53
53
  RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
54
54
  RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
@@ -546,12 +546,20 @@ def cdx_schema():
546
546
  "run": [
547
547
  "invalid_cwd",
548
548
  "session_disabled",
549
+ "session_busy",
550
+ "auth_lock_failed",
551
+ "remote_unavailable",
552
+ "remote_version_mismatch",
553
+ "remote_provider_unsupported",
554
+ "remote_credential_expired",
555
+ "remote_isolation_unavailable",
549
556
  "no_suitable_session",
550
557
  "provider_cli_not_found",
551
558
  "provider_start_failed",
552
559
  "provider_failed",
553
560
  "provider_timeout",
554
561
  "budget_exceeded",
562
+ "budget_unverifiable",
555
563
  "run_cancelled",
556
564
  "too_many_runs",
557
565
  "failover_exhausted",
@@ -672,6 +680,9 @@ def _parse_max_parallel(value):
672
680
 
673
681
  def _parse_run_args(args):
674
682
  parsed = _parse_run_flag_args(args, {
683
+ "--remote": {"key": "remote", "type": "str", "default": None},
684
+ "--remote-prefix": {"key": "remote_prefix", "type": "str", "default": None},
685
+ "--project": {"key": "project", "type": "str", "default": None},
675
686
  "--cwd": {"key": "cwd", "type": "str", "default": None},
676
687
  "--dir": {"key": "cwd", "type": "str", "default": None},
677
688
  "--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
@@ -730,11 +741,20 @@ def _parse_run_args(args):
730
741
  if parsed["continue_run"] is not None and not parsed["continue_run"].strip():
731
742
  raise CdxArgumentError("cdx run: --continue-run requires a nonempty run id.",
732
743
  code=ARG_CODE_INVALID_VALUE, arguments=["--continue-run"])
744
+ if parsed["remote"] is not None and not parsed["remote"].strip():
745
+ raise CdxArgumentError("cdx run: --remote requires a nonempty SSH target.",
746
+ code=ARG_CODE_INVALID_VALUE, arguments=["--remote"])
747
+ if parsed["remote_prefix"] is not None and (not parsed["remote"] or not parsed["remote_prefix"].strip()):
748
+ raise CdxArgumentError("cdx run: --remote-prefix requires --remote and a nonempty command.",
749
+ code=ARG_CODE_INVALID_VALUE, arguments=["--remote-prefix"])
733
750
  effort = _parse_run_effort(parsed["reasoning_effort"], parsed["power"])
734
751
  return {
735
752
  "name": name,
753
+ "remote": parsed["remote"],
754
+ "remote_prefix": parsed["remote_prefix"],
736
755
  "provider": parsed["provider"],
737
756
  "cwd": parsed["cwd"],
757
+ "project": parsed["project"],
738
758
  "prompt_file": parsed["prompt_file"],
739
759
  "prompt": parsed["prompt"],
740
760
  "continue_run": parsed["continue_run"],
@@ -18,7 +18,7 @@ CODEX_AUTH_LOCK_NAME = ".cdx-auth.lock"
18
18
 
19
19
 
20
20
  @contextlib.contextmanager
21
- def codex_auth_lock(auth_home, blocking=False, timeout_seconds=None, retry_interval_seconds=0.05):
21
+ def codex_auth_lock(auth_home, blocking=False, timeout_seconds=None, retry_interval_seconds=0.05, strict=False):
22
22
  """Serialize codex token refreshes per CODEX_HOME.
23
23
 
24
24
  Codex rotates its OAuth refresh_token on refresh and invalidates the old
@@ -28,6 +28,8 @@ def codex_auth_lock(auth_home, blocking=False, timeout_seconds=None, retry_inter
28
28
  the lock is held, False when a non-blocking acquire found it already taken.
29
29
  ponytail: POSIX flock only; on Windows (no fcntl) this is a no-op.
30
30
  """
31
+ if strict and (not auth_home or fcntl is None):
32
+ raise OSError("A POSIX authentication lock is required")
31
33
  if not auth_home or fcntl is None:
32
34
  yield True
33
35
  return
@@ -35,6 +37,8 @@ def codex_auth_lock(auth_home, blocking=False, timeout_seconds=None, retry_inter
35
37
  os.makedirs(auth_home, exist_ok=True)
36
38
  handle = open(os.path.join(auth_home, CODEX_AUTH_LOCK_NAME), "w")
37
39
  except OSError:
40
+ if strict:
41
+ raise
38
42
  yield True
39
43
  return
40
44
  deadline = None if timeout_seconds is None else time.monotonic() + timeout_seconds