cdx-manager 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/changelogs/CHANGELOGS_0_9_8.md +43 -0
- package/changelogs/CHANGELOGS_0_9_9.md +36 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +4 -3
- package/pyproject.toml +27 -1
- package/src/backup_bundle.py +1 -1
- package/src/cli.py +75 -111
- package/src/codex_usage.py +53 -0
- package/src/fs_utils.py +12 -2
- package/src/provider_runtime.py +16 -0
- package/src/session_service.py +8 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -76,7 +76,8 @@ One command to launch any session. Zero auth juggling.
|
|
|
76
76
|
- On Windows, transcript capture is optional. If no compatible `script` wrapper is installed, Codex still launches normally without transcript capture.
|
|
77
77
|
- Auth probe: synchronous subprocess call to `codex login status` or `claude auth status` before any interactive launch.
|
|
78
78
|
- Signal forwarding: `SIGINT`, `SIGTERM`, and `SIGHUP` are forwarded to the child process and produce clean exit codes.
|
|
79
|
-
- Test stack:
|
|
79
|
+
- Test stack: `pytest`, `pytest-cov`, and `ruff` via the documented `dev` dependency extra.
|
|
80
|
+
- Python floor decision: `cdx-manager` keeps Python 3.9 support for the 0.9.x line to avoid dropping existing users before a planned compatibility break; classifiers remain aligned with `requires-python = ">=3.9"`.
|
|
80
81
|
|
|
81
82
|
---
|
|
82
83
|
|
|
@@ -149,6 +150,12 @@ cd cdx-manager
|
|
|
149
150
|
make install
|
|
150
151
|
```
|
|
151
152
|
|
|
153
|
+
For local development, install the Python toolchain as well:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python3 -m pip install -e ".[dev]"
|
|
157
|
+
```
|
|
158
|
+
|
|
152
159
|
From source on Windows:
|
|
153
160
|
|
|
154
161
|
```powershell
|
|
@@ -512,9 +519,10 @@ Notes:
|
|
|
512
519
|
|
|
513
520
|
## Available Scripts
|
|
514
521
|
|
|
515
|
-
- `npm test`: run the Python test suite
|
|
516
|
-
- `npm run test:py`: run
|
|
517
|
-
- `npm run
|
|
522
|
+
- `npm test`: run the Python test suite with `pytest`
|
|
523
|
+
- `npm run test:py`: run `pytest` through the portable launcher
|
|
524
|
+
- `npm run test:coverage`: run `pytest` with terminal coverage reporting for `src/`
|
|
525
|
+
- `npm run lint`: check project guidance, the Node launcher, `ruff`, and byte-compile the Python sources, scripts, and tests
|
|
518
526
|
- `npm run release:validate`: verify version alignment and required GitHub release archive checksum metadata before publication
|
|
519
527
|
- `npm run link`: link `cdx` globally for local development (`npm link`)
|
|
520
528
|
- `npm run unlink`: remove the global link
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# CDX Manager 0.9.8
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Hardened encrypted auth bundles so session names are no longer exposed in cleartext bundle metadata.
|
|
6
|
+
- Replaced deprecated tree removal handling with Python 3.12+ `onexc` support and a fallback for older supported Python versions.
|
|
7
|
+
- Added `pytest` coverage reporting and a `ruff` lint gate to local and CI validation.
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
### Auth bundle privacy
|
|
12
|
+
|
|
13
|
+
Encrypted bundles now keep session names inside the encrypted payload instead of duplicating them in the cleartext wrapper.
|
|
14
|
+
|
|
15
|
+
Existing encrypted bundles that still contain cleartext `session_names` remain import-compatible.
|
|
16
|
+
|
|
17
|
+
### Runtime hardening
|
|
18
|
+
|
|
19
|
+
Profile tree deletion now uses `shutil.rmtree(..., onexc=...)` where available, avoiding the deprecated `onerror` path on Python 3.12+ while retaining compatibility with Python 3.9-3.11.
|
|
20
|
+
|
|
21
|
+
Auth bundle tests now skip cleanly when `cryptography` is unavailable in the active interpreter.
|
|
22
|
+
|
|
23
|
+
### CLI maintainability
|
|
24
|
+
|
|
25
|
+
Top-level command routing now uses an explicit dispatch table, with update notice suppression controlled by command metadata instead of an inline negative list.
|
|
26
|
+
|
|
27
|
+
### Tooling and CI
|
|
28
|
+
|
|
29
|
+
CI now installs the `dev` extra, runs `ruff check`, and executes the same `pytest` suite used locally with coverage reporting for `src/`.
|
|
30
|
+
|
|
31
|
+
The Python floor remains `>=3.9` for the 0.9.x line to avoid a compatibility break before a planned major support change.
|
|
32
|
+
|
|
33
|
+
## Validation
|
|
34
|
+
|
|
35
|
+
- `npm run release:validate`
|
|
36
|
+
- `npm run lint`
|
|
37
|
+
- `npm test`
|
|
38
|
+
- `logics-manager lint --require-status`
|
|
39
|
+
- `logics-manager audit`
|
|
40
|
+
- `git diff --check`
|
|
41
|
+
- `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
|
|
42
|
+
- `python -m build`
|
|
43
|
+
- `python -m twine check dist/*`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# CDX Manager 0.9.9
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Fixed Codex session logouts caused by concurrent OAuth token refresh races.
|
|
6
|
+
- Refreshed the local Logics assistant bridge and canonical workflow instructions.
|
|
7
|
+
|
|
8
|
+
## Changes
|
|
9
|
+
|
|
10
|
+
### Codex token refresh stability
|
|
11
|
+
|
|
12
|
+
Codex session startup now serializes OAuth token refresh work per source session and reuses a freshly refreshed source token when multiple sessions start at the same time.
|
|
13
|
+
|
|
14
|
+
This prevents parallel launches from copying stale credentials into isolated Codex profiles while another launch is already rotating the shared token.
|
|
15
|
+
|
|
16
|
+
### Runtime observability
|
|
17
|
+
|
|
18
|
+
The provider runtime now recognizes Codex refresh-rate errors as refresh failures, making status and failure reporting more precise when token refresh is blocked or rate-limited.
|
|
19
|
+
|
|
20
|
+
Regression coverage exercises concurrent Codex launches and verifies that all isolated profiles receive the refreshed token.
|
|
21
|
+
|
|
22
|
+
### Logics workflow instructions
|
|
23
|
+
|
|
24
|
+
The local `LOGICS.md` bridge and `logics/instructions.md` were refreshed to keep release and workflow operations aligned with the current `logics-manager` commands.
|
|
25
|
+
|
|
26
|
+
## Validation
|
|
27
|
+
|
|
28
|
+
- `npm run release:validate`
|
|
29
|
+
- `npm run lint`
|
|
30
|
+
- `npm test`
|
|
31
|
+
- `logics-manager lint --require-status`
|
|
32
|
+
- `logics-manager audit`
|
|
33
|
+
- `git diff --check`
|
|
34
|
+
- `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
|
|
35
|
+
- `python -m build`
|
|
36
|
+
- `python -m twine check dist/*`
|
|
@@ -92,6 +92,14 @@
|
|
|
92
92
|
"v0.9.6": {
|
|
93
93
|
"github_tarball_sha256": "7706ba127f247dfab50404ca175921c7874624e835c920799b3b122a45f7dc4b",
|
|
94
94
|
"github_zip_sha256": "be11246f65eae006512247067bff22863a6ebe8d5738b056fe17f654fd5d9b5c"
|
|
95
|
+
},
|
|
96
|
+
"v0.9.7": {
|
|
97
|
+
"github_tarball_sha256": "8ca47e94843c9bf2592eb6a771aa33f209cf0f5bf04ed3e93cb000cea8a73bf6",
|
|
98
|
+
"github_zip_sha256": "1fe3ac56efe3de42a794614edaffab2f8ae8b5da8e51cdbda0f625665993f1f9"
|
|
99
|
+
},
|
|
100
|
+
"v0.9.8": {
|
|
101
|
+
"github_tarball_sha256": "8f05d052b7890d74de7ac510c20376430515ad2616e139941cb9bb8edc4fb680",
|
|
102
|
+
"github_zip_sha256": "3bc3834ecf84784c77cf603fcd4f6f439e42005612f82fabab588886519179eb"
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdx-manager",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "Terminal session manager for Codex and Claude accounts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexandre Agostini",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"scripts": {
|
|
45
45
|
"status": "node bin/cdx.js status",
|
|
46
46
|
"test": "npm run test:py",
|
|
47
|
-
"test:py": "node bin/python-runner.js -m
|
|
48
|
-
"
|
|
47
|
+
"test:py": "node bin/python-runner.js -m pytest",
|
|
48
|
+
"test:coverage": "node bin/python-runner.js -m pytest --cov=src --cov-report=term-missing",
|
|
49
|
+
"lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js scripts/verify_project_docs.py && node bin/python-runner.js -m ruff check . && node bin/python-runner.js -m py_compile bin/cdx src/*.py scripts/*.py test/test_*_py.py",
|
|
49
50
|
"release:validate": "node bin/python-runner.js scripts/verify_release_checksums.py",
|
|
50
51
|
"prepublishOnly": "npm run release:validate && npm run lint && npm test",
|
|
51
52
|
"link": "npm link",
|
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.9.
|
|
7
|
+
version = "0.9.9"
|
|
8
8
|
description = "Terminal session manager for Codex and Claude accounts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -28,6 +28,13 @@ classifiers = [
|
|
|
28
28
|
]
|
|
29
29
|
dependencies = ["cryptography>=42"]
|
|
30
30
|
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8",
|
|
34
|
+
"pytest-cov>=5",
|
|
35
|
+
"ruff>=0.6",
|
|
36
|
+
]
|
|
37
|
+
|
|
31
38
|
[project.urls]
|
|
32
39
|
Homepage = "https://github.com/AlexAgo83/cdx-manager"
|
|
33
40
|
Repository = "https://github.com/AlexAgo83/cdx-manager.git"
|
|
@@ -42,3 +49,22 @@ packages = ["cdx_manager"]
|
|
|
42
49
|
|
|
43
50
|
[tool.setuptools.package-dir]
|
|
44
51
|
cdx_manager = "src"
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["test"]
|
|
55
|
+
python_files = ["test_*_py.py"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.run]
|
|
58
|
+
branch = true
|
|
59
|
+
source = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.report]
|
|
62
|
+
show_missing = true
|
|
63
|
+
skip_empty = true
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
target-version = "py39"
|
|
67
|
+
line-length = 140
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E9", "F63", "F7", "F82"]
|
package/src/backup_bundle.py
CHANGED
|
@@ -105,7 +105,6 @@ def encode_bundle(payload, include_auth=False, passphrase=None):
|
|
|
105
105
|
"created_at": _now_iso(),
|
|
106
106
|
"include_auth": bool(include_auth),
|
|
107
107
|
"encrypted": bool(include_auth),
|
|
108
|
-
"session_names": [item["name"] for item in payload.get("sessions", [])],
|
|
109
108
|
}
|
|
110
109
|
if include_auth:
|
|
111
110
|
salt = os.urandom(_SALT_BYTES)
|
|
@@ -120,6 +119,7 @@ def encode_bundle(payload, include_auth=False, passphrase=None):
|
|
|
120
119
|
"payload": _b64_encode(ciphertext),
|
|
121
120
|
})
|
|
122
121
|
else:
|
|
122
|
+
wrapper["session_names"] = [item["name"] for item in payload.get("sessions", [])]
|
|
123
123
|
wrapper["payload"] = _b64_encode(payload_bytes)
|
|
124
124
|
return json.dumps(wrapper, indent=2).encode("utf-8")
|
|
125
125
|
|
package/src/cli.py
CHANGED
|
@@ -66,7 +66,70 @@ from .status_view import (
|
|
|
66
66
|
)
|
|
67
67
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
68
68
|
|
|
69
|
-
VERSION = "0.9.
|
|
69
|
+
VERSION = "0.9.9"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_COMMAND_HANDLERS = {
|
|
73
|
+
"add": handle_add,
|
|
74
|
+
"cp": handle_copy,
|
|
75
|
+
"clean": handle_clean,
|
|
76
|
+
"config": handle_config,
|
|
77
|
+
"configs": handle_configs,
|
|
78
|
+
"context": handle_context,
|
|
79
|
+
"doctor": handle_doctor,
|
|
80
|
+
"disable": handle_disable,
|
|
81
|
+
"enable": handle_enable,
|
|
82
|
+
"export": handle_export,
|
|
83
|
+
"handoff": handle_handoff,
|
|
84
|
+
"history": handle_history,
|
|
85
|
+
"import": handle_import,
|
|
86
|
+
"last": handle_last,
|
|
87
|
+
"login": handle_login,
|
|
88
|
+
"logout": handle_logout,
|
|
89
|
+
"next": handle_next,
|
|
90
|
+
"notify": handle_notify,
|
|
91
|
+
"repair": handle_repair,
|
|
92
|
+
"rename": handle_rename,
|
|
93
|
+
"resume": handle_resume,
|
|
94
|
+
"can-resume": handle_can_resume,
|
|
95
|
+
"rmv": handle_remove,
|
|
96
|
+
"run": handle_run,
|
|
97
|
+
"run-report": handle_run_report,
|
|
98
|
+
"run-status": handle_run_status,
|
|
99
|
+
"runs": handle_runs,
|
|
100
|
+
"select": handle_select,
|
|
101
|
+
"set": handle_set,
|
|
102
|
+
"stats": handle_stats,
|
|
103
|
+
"status": handle_status,
|
|
104
|
+
"unset": handle_unset,
|
|
105
|
+
"update": handle_update,
|
|
106
|
+
"view": handle_view,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_COMMAND_ALIASES = {
|
|
110
|
+
"ren": "rename",
|
|
111
|
+
"mv": "rename",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_SUPPRESS_UPDATE_NOTICE_COMMANDS = {
|
|
115
|
+
*set(_COMMAND_HANDLERS),
|
|
116
|
+
"fast",
|
|
117
|
+
"help",
|
|
118
|
+
"model",
|
|
119
|
+
"perm",
|
|
120
|
+
"power",
|
|
121
|
+
"ready",
|
|
122
|
+
"rename",
|
|
123
|
+
"version",
|
|
124
|
+
} - {"status"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_command(command):
|
|
128
|
+
return _COMMAND_ALIASES.get(command, command)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _should_check_update_notices(command):
|
|
132
|
+
return _resolve_command(command) not in _SUPPRESS_UPDATE_NOTICE_COMMANDS
|
|
70
133
|
|
|
71
134
|
|
|
72
135
|
# ---------------------------------------------------------------------------
|
|
@@ -271,6 +334,7 @@ def main(argv, options=None):
|
|
|
271
334
|
return 0
|
|
272
335
|
|
|
273
336
|
command, *rest = argv
|
|
337
|
+
resolved_command = _resolve_command(command)
|
|
274
338
|
ctx = {
|
|
275
339
|
"env": env,
|
|
276
340
|
"options": options,
|
|
@@ -286,130 +350,30 @@ def main(argv, options=None):
|
|
|
286
350
|
"stdin_is_tty": stdin_is_tty,
|
|
287
351
|
"version": VERSION,
|
|
288
352
|
"cwd": options.get("cwd") or os.getcwd(),
|
|
289
|
-
"update_notices": _get_update_notices(service, env, options) if command
|
|
290
|
-
"add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "resume", "can-resume", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
|
|
291
|
-
) else None,
|
|
353
|
+
"update_notices": _get_update_notices(service, env, options) if _should_check_update_notices(command) else None,
|
|
292
354
|
"use_color": use_color,
|
|
293
355
|
}
|
|
294
356
|
|
|
295
|
-
if
|
|
296
|
-
return handle_add(rest, ctx)
|
|
297
|
-
|
|
298
|
-
if command == "cp":
|
|
299
|
-
return handle_copy(rest, ctx)
|
|
300
|
-
|
|
301
|
-
if command in ("ren", "rename", "mv"):
|
|
302
|
-
return handle_rename(rest, ctx)
|
|
303
|
-
|
|
304
|
-
if command == "rmv":
|
|
305
|
-
return handle_remove(rest, ctx)
|
|
306
|
-
|
|
307
|
-
if command == "disable":
|
|
308
|
-
return handle_disable(rest, ctx)
|
|
309
|
-
|
|
310
|
-
if command == "enable":
|
|
311
|
-
return handle_enable(rest, ctx)
|
|
312
|
-
|
|
313
|
-
if command == "clean":
|
|
314
|
-
return handle_clean(rest, ctx)
|
|
315
|
-
|
|
316
|
-
if command == "export":
|
|
317
|
-
return handle_export(rest, ctx)
|
|
318
|
-
|
|
319
|
-
if command == "import":
|
|
320
|
-
return handle_import(rest, ctx)
|
|
321
|
-
|
|
322
|
-
if command == "doctor":
|
|
323
|
-
return handle_doctor(rest, ctx)
|
|
324
|
-
|
|
325
|
-
if command == "repair":
|
|
326
|
-
return handle_repair(rest, ctx)
|
|
327
|
-
|
|
328
|
-
if command == "view":
|
|
329
|
-
return handle_view(rest, ctx)
|
|
330
|
-
|
|
331
|
-
if command == "update":
|
|
332
|
-
return handle_update(rest, ctx)
|
|
333
|
-
|
|
334
|
-
if command == "ready":
|
|
357
|
+
if resolved_command == "ready":
|
|
335
358
|
if any(arg not in ("--refresh", "--json") for arg in rest):
|
|
336
359
|
raise CdxError("Usage: cdx ready [--refresh] [--json]")
|
|
337
360
|
return handle_notify(["--next-ready", "--schedule", *rest], ctx)
|
|
338
361
|
|
|
339
|
-
if
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
if command == "next":
|
|
343
|
-
return handle_next(rest, ctx)
|
|
344
|
-
|
|
345
|
-
if command == "context":
|
|
346
|
-
return handle_context(rest, ctx)
|
|
347
|
-
|
|
348
|
-
if command == "config":
|
|
349
|
-
return handle_config(rest, ctx)
|
|
350
|
-
|
|
351
|
-
if command == "configs":
|
|
352
|
-
return handle_configs(rest, ctx)
|
|
353
|
-
|
|
354
|
-
if command == "set":
|
|
355
|
-
return handle_set(rest, ctx)
|
|
362
|
+
if resolved_command in ("power", "perm", "fast", "model"):
|
|
363
|
+
return handle_launch_setting_alias(resolved_command, rest, ctx)
|
|
356
364
|
|
|
357
|
-
if
|
|
358
|
-
return handle_unset(rest, ctx)
|
|
359
|
-
|
|
360
|
-
if command in ("power", "perm", "fast", "model"):
|
|
361
|
-
return handle_launch_setting_alias(command, rest, ctx)
|
|
362
|
-
|
|
363
|
-
if command == "history":
|
|
364
|
-
return handle_history(rest, ctx)
|
|
365
|
-
|
|
366
|
-
if command == "stats":
|
|
367
|
-
return handle_stats(rest, ctx)
|
|
368
|
-
|
|
369
|
-
if command == "last":
|
|
370
|
-
return handle_last(rest, ctx)
|
|
371
|
-
|
|
372
|
-
if command == "resume":
|
|
373
|
-
return handle_resume(rest, ctx)
|
|
374
|
-
|
|
375
|
-
if command == "can-resume":
|
|
376
|
-
return handle_can_resume(rest, ctx)
|
|
377
|
-
|
|
378
|
-
if command == "handoff":
|
|
379
|
-
return handle_handoff(rest, ctx)
|
|
380
|
-
|
|
381
|
-
if command == "status":
|
|
382
|
-
return handle_status(rest, ctx)
|
|
383
|
-
|
|
384
|
-
if command == "select":
|
|
385
|
-
return handle_select(rest, ctx)
|
|
386
|
-
|
|
387
|
-
if command == "run":
|
|
388
|
-
return handle_run(rest, ctx)
|
|
389
|
-
|
|
390
|
-
if command == "runs":
|
|
391
|
-
return handle_runs(rest, ctx)
|
|
392
|
-
|
|
393
|
-
if command == "run-status":
|
|
394
|
-
return handle_run_status(rest, ctx)
|
|
395
|
-
|
|
396
|
-
if command == "run-report":
|
|
397
|
-
return handle_run_report(rest, ctx)
|
|
398
|
-
|
|
399
|
-
if command == "login":
|
|
400
|
-
return handle_login(rest, ctx)
|
|
401
|
-
|
|
402
|
-
if command == "logout":
|
|
403
|
-
return handle_logout(rest, ctx)
|
|
404
|
-
|
|
405
|
-
if command in ("help",):
|
|
365
|
+
if resolved_command == "help":
|
|
406
366
|
out(f"{_print_help(use_color=use_color)}\n")
|
|
407
367
|
return 0
|
|
408
368
|
|
|
409
|
-
if
|
|
369
|
+
if resolved_command == "version":
|
|
410
370
|
out(f"{_print_version()}\n")
|
|
411
371
|
return 0
|
|
412
372
|
|
|
373
|
+
handler = _COMMAND_HANDLERS.get(resolved_command)
|
|
374
|
+
if handler:
|
|
375
|
+
return handler(rest, ctx)
|
|
376
|
+
|
|
413
377
|
if all(arg in ("--json", "-r", "--resume") for arg in rest):
|
|
414
378
|
return handle_launch(command, ctx, resume=("-r" in rest or "--resume" in rest))
|
|
415
379
|
|
package/src/codex_usage.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import queue
|
|
@@ -5,6 +6,49 @@ import subprocess
|
|
|
5
6
|
import threading
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
|
|
9
|
+
try:
|
|
10
|
+
import fcntl
|
|
11
|
+
except ImportError: # pragma: no cover - non-POSIX platforms
|
|
12
|
+
fcntl = None
|
|
13
|
+
|
|
14
|
+
CODEX_AUTH_LOCK_NAME = ".cdx-auth.lock"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextlib.contextmanager
|
|
18
|
+
def codex_auth_lock(auth_home, blocking=False):
|
|
19
|
+
"""Serialize codex token refreshes per CODEX_HOME.
|
|
20
|
+
|
|
21
|
+
Codex rotates its OAuth refresh_token on refresh and invalidates the old
|
|
22
|
+
one. If a status probe and an interactive session refresh the same
|
|
23
|
+
auth.json concurrently, one rotates the token from under the other and that
|
|
24
|
+
session gets logged out. This flock makes them take turns. Yields True when
|
|
25
|
+
the lock is held, False when a non-blocking acquire found it already taken.
|
|
26
|
+
ponytail: POSIX flock only; on Windows (no fcntl) this is a no-op.
|
|
27
|
+
"""
|
|
28
|
+
if not auth_home or fcntl is None:
|
|
29
|
+
yield True
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
os.makedirs(auth_home, exist_ok=True)
|
|
33
|
+
handle = open(os.path.join(auth_home, CODEX_AUTH_LOCK_NAME), "w")
|
|
34
|
+
except OSError:
|
|
35
|
+
yield True
|
|
36
|
+
return
|
|
37
|
+
flags = fcntl.LOCK_EX if blocking else fcntl.LOCK_EX | fcntl.LOCK_NB
|
|
38
|
+
try:
|
|
39
|
+
fcntl.flock(handle, flags)
|
|
40
|
+
except OSError:
|
|
41
|
+
handle.close()
|
|
42
|
+
yield False
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
yield True
|
|
46
|
+
finally:
|
|
47
|
+
try:
|
|
48
|
+
fcntl.flock(handle, fcntl.LOCK_UN)
|
|
49
|
+
finally:
|
|
50
|
+
handle.close()
|
|
51
|
+
|
|
8
52
|
MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
9
53
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
10
54
|
|
|
@@ -110,7 +154,16 @@ def fetch_codex_rate_limit_diagnostic(session, timeout=5, popen_factory=None):
|
|
|
110
154
|
auth_home = session.get("authHome")
|
|
111
155
|
if not auth_home:
|
|
112
156
|
return {"ok": False, "reason": "missing_auth_home", "status": None}
|
|
157
|
+
# Back off to cached status rather than racing an interactive session's
|
|
158
|
+
# token refresh (which would log it out). The launcher holds this lock for
|
|
159
|
+
# the whole session, so a busy account simply skips the live probe.
|
|
160
|
+
with codex_auth_lock(auth_home) as acquired:
|
|
161
|
+
if not acquired:
|
|
162
|
+
return {"ok": False, "reason": "auth_locked", "status": None}
|
|
163
|
+
return _probe_codex_rate_limit_diagnostic(session, auth_home, timeout, popen_factory)
|
|
164
|
+
|
|
113
165
|
|
|
166
|
+
def _probe_codex_rate_limit_diagnostic(session, auth_home, timeout=5, popen_factory=None):
|
|
114
167
|
env = os.environ.copy()
|
|
115
168
|
env["CODEX_HOME"] = auth_home
|
|
116
169
|
popen_factory = popen_factory or subprocess.Popen
|
package/src/fs_utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import stat
|
|
@@ -35,7 +36,7 @@ def _make_tree_user_writable(path):
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def remove_tree(path, ignore_errors=False):
|
|
38
|
-
def
|
|
39
|
+
def retry_after_chmod(func, failing_path):
|
|
39
40
|
parent = os.path.dirname(failing_path)
|
|
40
41
|
if parent:
|
|
41
42
|
_make_user_writable(parent)
|
|
@@ -46,5 +47,14 @@ def remove_tree(path, ignore_errors=False):
|
|
|
46
47
|
if not ignore_errors:
|
|
47
48
|
raise
|
|
48
49
|
|
|
50
|
+
def onerror(func, failing_path, _exc_info):
|
|
51
|
+
retry_after_chmod(func, failing_path)
|
|
52
|
+
|
|
53
|
+
def onexc(func, failing_path, _exc):
|
|
54
|
+
retry_after_chmod(func, failing_path)
|
|
55
|
+
|
|
49
56
|
_make_tree_user_writable(path)
|
|
50
|
-
shutil.rmtree
|
|
57
|
+
if "onexc" in inspect.signature(shutil.rmtree).parameters:
|
|
58
|
+
shutil.rmtree(path, ignore_errors=ignore_errors, onexc=onexc)
|
|
59
|
+
else:
|
|
60
|
+
shutil.rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
|
package/src/provider_runtime.py
CHANGED
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
import base64
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
|
|
13
|
+
from .codex_usage import codex_auth_lock
|
|
13
14
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
14
15
|
from .errors import CdxError
|
|
15
16
|
|
|
@@ -929,6 +930,21 @@ def _signal_name(sig):
|
|
|
929
930
|
def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
930
931
|
env_override=None, signal_emitter=None,
|
|
931
932
|
initial_prompt=None, lifecycle_callback=None):
|
|
933
|
+
kwargs = dict(spawn=spawn, cwd=cwd, env_override=env_override,
|
|
934
|
+
signal_emitter=signal_emitter, initial_prompt=initial_prompt,
|
|
935
|
+
lifecycle_callback=lifecycle_callback)
|
|
936
|
+
if session.get("provider") != PROVIDER_CODEX:
|
|
937
|
+
return _run_interactive_provider_command_impl(session, action, **kwargs)
|
|
938
|
+
# Hold the per-CODEX_HOME lock for the whole session so a concurrent status
|
|
939
|
+
# probe backs off instead of rotating the refresh_token mid-run (which logs
|
|
940
|
+
# the session out). ponytail: non-blocking, so launch never waits on a probe.
|
|
941
|
+
with codex_auth_lock(_get_auth_home(session)):
|
|
942
|
+
return _run_interactive_provider_command_impl(session, action, **kwargs)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _run_interactive_provider_command_impl(session, action, spawn=None, cwd=None,
|
|
946
|
+
env_override=None, signal_emitter=None,
|
|
947
|
+
initial_prompt=None, lifecycle_callback=None):
|
|
932
948
|
spawn = spawn or subprocess.Popen
|
|
933
949
|
if action == "launch":
|
|
934
950
|
spec = _build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
|
package/src/session_service.py
CHANGED
|
@@ -68,6 +68,9 @@ RESERVED_SESSION_NAMES = {
|
|
|
68
68
|
}
|
|
69
69
|
STATUS_CACHE_TTL_SECONDS = 60
|
|
70
70
|
CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
|
|
71
|
+
# Each live Codex probe spawns app-server, which can refresh+rotate the OAuth
|
|
72
|
+
# token. Cache longer so routine status calls don't keep triggering refreshes.
|
|
73
|
+
CODEX_STATUS_CACHE_TTL_SECONDS = 5 * 60
|
|
71
74
|
STATUS_PROBE_TIMEOUT_SECONDS = 5
|
|
72
75
|
MAX_STATUS_WORKERS = 8
|
|
73
76
|
LAUNCH_POWER_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
|
|
@@ -275,8 +278,11 @@ def _parse_status_timestamp(value):
|
|
|
275
278
|
|
|
276
279
|
|
|
277
280
|
def _status_cache_ttl_seconds(session, ttl_seconds=STATUS_CACHE_TTL_SECONDS):
|
|
278
|
-
if
|
|
279
|
-
|
|
281
|
+
if ttl_seconds == STATUS_CACHE_TTL_SECONDS:
|
|
282
|
+
if session.get("provider") == PROVIDER_CLAUDE:
|
|
283
|
+
return CLAUDE_STATUS_CACHE_TTL_SECONDS
|
|
284
|
+
if session.get("provider") == PROVIDER_CODEX:
|
|
285
|
+
return CODEX_STATUS_CACHE_TTL_SECONDS
|
|
280
286
|
return ttl_seconds
|
|
281
287
|
|
|
282
288
|
|