cdx-manager 0.11.1 → 0.11.2
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 +2 -2
- package/changelogs/CHANGELOGS_0_11_2.md +27 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +1 -1
- package/src/health.py +129 -4
- package/src/provider_runtime.py +14 -0
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
|
|
|
@@ -683,7 +683,7 @@ Without `--yes`, an interactive confirmation is required. `cdx` sends an idempot
|
|
|
683
683
|
## Troubleshooting
|
|
684
684
|
|
|
685
685
|
- **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
|
|
686
|
-
- **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file` and `
|
|
686
|
+
- **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file`, `codex_live_auth`, and `codex_stale_auth_logs` checks. If recent logs mention expired auth, repair only that isolated profile with `cdx login <name>`. Codex Business profiles can share the same `tokens.account_id`; `cdx doctor` reports `codex_shared_account_id` as a workspace-level clue, not proof that two profiles use the same user. Newly created sessions seed from the current global `~/.codex/auth.json` when one exists. For two separate Codex accounts, create or repair each session by running `cdx login <name>` for that session; `cdx login` does not log out first, so use `cdx logout <name>` explicitly only when you want to clear that isolated profile.
|
|
687
687
|
- **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
|
|
688
688
|
- **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
|
|
689
689
|
- **Status shows `n/a` for all fields** — the Codex app-server rate-limit probe may be unavailable, the session may not be authenticated, and no legacy transcript/history status has been captured yet.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CDX Manager 0.11.2
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Improved Codex Business auth diagnostics without changing login or launch behavior.
|
|
6
|
+
- Added stale-auth log warnings to help repair the affected profile only.
|
|
7
|
+
- Documented the Business `account_id` ambiguity and the `cdx login <name>` repair path.
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
### Codex Business auth diagnostics
|
|
12
|
+
|
|
13
|
+
`cdx doctor` now reports shared Codex `tokens.account_id` values as workspace-level evidence on Business plans, not proof that two profiles are using the same user account. When recent `/status` transcript output exposes different observed users for profiles that share the same account id, doctor marks that as expected instead of a duplicate-user warning.
|
|
14
|
+
|
|
15
|
+
Codex auth diagnostics now include a masked `account_id` and an observed account from recent logs when available. Raw tokens remain excluded from JSON output.
|
|
16
|
+
|
|
17
|
+
### Stale auth hints
|
|
18
|
+
|
|
19
|
+
`cdx doctor` scans recent Codex profile logs for common expired-auth markers such as `401`, `token_expired`, and `authentication token is expired`. When found, it emits `codex_stale_auth_logs` with a direct `cdx login <name>` recommendation for the affected isolated profile.
|
|
20
|
+
|
|
21
|
+
## Validation
|
|
22
|
+
|
|
23
|
+
- `npm run lint`
|
|
24
|
+
- `npm test`
|
|
25
|
+
- `logics-manager health`
|
|
26
|
+
- `logics-manager lint --require-status`
|
|
27
|
+
- `logics-manager audit --group-by-doc`
|
|
@@ -140,6 +140,10 @@
|
|
|
140
140
|
"v0.11.0": {
|
|
141
141
|
"github_tarball_sha256": "bc42f83f20ba1c6ff81f824c382801457f93cf8cb166acbe952c12d009560a70",
|
|
142
142
|
"github_zip_sha256": "1c03b4f03f53c46d0a5ba580fc8ade7b26fa3cb1bb5f029f703681f136b9ac1f"
|
|
143
|
+
},
|
|
144
|
+
"v0.11.1": {
|
|
145
|
+
"github_tarball_sha256": "33ee5a76cd217007468ea620fa725644dfd62179585551b5703129c00a5094d5",
|
|
146
|
+
"github_zip_sha256": "8b53832ad2af109c22a898782bd36e1408356fc2bd0c3f99ca2ffb1262e2325b"
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -77,7 +77,7 @@ from .status_view import (
|
|
|
77
77
|
)
|
|
78
78
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
79
79
|
|
|
80
|
-
VERSION = "0.11.
|
|
80
|
+
VERSION = "0.11.2"
|
|
81
81
|
|
|
82
82
|
# Public surface: this module is a facade. Names below are imported above
|
|
83
83
|
# purely to be re-exported (consumed by tests and external callers); listing
|
package/src/health.py
CHANGED
|
@@ -3,11 +3,13 @@ import os
|
|
|
3
3
|
import shutil
|
|
4
4
|
import sys
|
|
5
5
|
import tempfile
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from urllib.parse import quote, unquote
|
|
7
8
|
|
|
8
9
|
from .cli_render import _pad_table, _style
|
|
9
10
|
from .config import PROVIDER_CODEX
|
|
10
11
|
from .provider_runtime import codex_auth_diagnostic
|
|
12
|
+
from .status_source import _extract_account_identity
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def _encode(name):
|
|
@@ -73,6 +75,7 @@ def collect_health_report(service, base_dir, env=None, spawn_sync=None):
|
|
|
73
75
|
issues.append(_check_cdx_home(base_dir))
|
|
74
76
|
sessions = service["list_sessions"]()
|
|
75
77
|
session_names = {session["name"] for session in sessions}
|
|
78
|
+
codex_diagnostics = []
|
|
76
79
|
for session in sessions:
|
|
77
80
|
name = session["name"]
|
|
78
81
|
root = session.get("sessionRoot") or service["get_session_root"](name)
|
|
@@ -82,16 +85,19 @@ def collect_health_report(service, base_dir, env=None, spawn_sync=None):
|
|
|
82
85
|
if not os.path.isfile(state_path):
|
|
83
86
|
issues.append(_issue("FAIL", "missing_state", f"session {name} state file is missing", state_path, True))
|
|
84
87
|
if session.get("provider") == PROVIDER_CODEX:
|
|
85
|
-
|
|
88
|
+
diag = codex_auth_diagnostic(session, spawn_sync=spawn_sync, env_override=env)
|
|
89
|
+
diag["observed_account"] = _recent_codex_observed_account(diag.get("auth_home"))
|
|
90
|
+
codex_diagnostics.append((session, diag))
|
|
91
|
+
issues.extend(_codex_auth_issues(session, diag))
|
|
86
92
|
|
|
93
|
+
issues.extend(_codex_shared_account_id_issues(codex_diagnostics))
|
|
87
94
|
issues.extend(_collect_profile_issues(base_dir, session_names))
|
|
88
95
|
return {"base_dir": base_dir, "issues": issues, "summary": summarize_health(issues)}
|
|
89
96
|
|
|
90
97
|
|
|
91
|
-
def _codex_auth_issues(session,
|
|
98
|
+
def _codex_auth_issues(session, diag):
|
|
92
99
|
name = session["name"]
|
|
93
|
-
|
|
94
|
-
identity = diag.get("account_email") or "unknown account"
|
|
100
|
+
identity = diag.get("account_email") or diag.get("observed_account") or "unknown account"
|
|
95
101
|
issues = [
|
|
96
102
|
_issue(
|
|
97
103
|
"OK" if diag.get("auth_json_exists") else "WARN",
|
|
@@ -103,6 +109,8 @@ def _codex_auth_issues(session, spawn_sync=None, env=None):
|
|
|
103
109
|
"auth_json_exists": diag.get("auth_json_exists"),
|
|
104
110
|
"local_tokens_present": diag.get("local_tokens_present"),
|
|
105
111
|
"account_email": diag.get("account_email"),
|
|
112
|
+
"observed_account": diag.get("observed_account"),
|
|
113
|
+
"account_id": _mask_identifier(diag.get("account_id")),
|
|
106
114
|
},
|
|
107
115
|
)
|
|
108
116
|
]
|
|
@@ -120,9 +128,126 @@ def _codex_auth_issues(session, spawn_sync=None, env=None):
|
|
|
120
128
|
"live_error": diag.get("live_error"),
|
|
121
129
|
},
|
|
122
130
|
))
|
|
131
|
+
stale_auth = _recent_codex_auth_error(diag.get("auth_home"))
|
|
132
|
+
if stale_auth:
|
|
133
|
+
issues.append(_issue(
|
|
134
|
+
"WARN",
|
|
135
|
+
"codex_stale_auth_logs",
|
|
136
|
+
f"session {name} recent Codex logs mention expired auth; run cdx login {name} if disconnects continue",
|
|
137
|
+
{
|
|
138
|
+
"session": name,
|
|
139
|
+
"auth_home": diag.get("auth_home"),
|
|
140
|
+
"source": stale_auth["source"],
|
|
141
|
+
"mtime": stale_auth["mtime"],
|
|
142
|
+
"markers": stale_auth["markers"],
|
|
143
|
+
},
|
|
144
|
+
))
|
|
123
145
|
return issues
|
|
124
146
|
|
|
125
147
|
|
|
148
|
+
def _codex_shared_account_id_issues(codex_diagnostics):
|
|
149
|
+
by_account_id = {}
|
|
150
|
+
for session, diag in codex_diagnostics:
|
|
151
|
+
account_id = diag.get("account_id")
|
|
152
|
+
if account_id:
|
|
153
|
+
by_account_id.setdefault(account_id, []).append((session, diag))
|
|
154
|
+
|
|
155
|
+
issues = []
|
|
156
|
+
for account_id, rows in sorted(by_account_id.items()):
|
|
157
|
+
if len(rows) < 2:
|
|
158
|
+
continue
|
|
159
|
+
identities = sorted({
|
|
160
|
+
_identity_user_part(diag.get("account_email") or diag.get("observed_account"))
|
|
161
|
+
for _session, diag in rows
|
|
162
|
+
if _identity_user_part(diag.get("account_email") or diag.get("observed_account"))
|
|
163
|
+
})
|
|
164
|
+
sessions = [session["name"] for session, _diag in rows]
|
|
165
|
+
status = "OK" if len(identities) > 1 else "WARN"
|
|
166
|
+
message = (
|
|
167
|
+
"Codex profiles share a Business account_id but observed different users; account_id is not a user identity"
|
|
168
|
+
if status == "OK"
|
|
169
|
+
else "Codex profiles share account_id; on Business plans this may be a workspace id, not proof of duplicate user auth"
|
|
170
|
+
)
|
|
171
|
+
issues.append(_issue(
|
|
172
|
+
status,
|
|
173
|
+
"codex_shared_account_id",
|
|
174
|
+
message,
|
|
175
|
+
{
|
|
176
|
+
"sessions": sessions,
|
|
177
|
+
"account_id": _mask_identifier(account_id),
|
|
178
|
+
"observed_identities": identities,
|
|
179
|
+
},
|
|
180
|
+
))
|
|
181
|
+
return issues
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _identity_user_part(value):
|
|
185
|
+
if not value:
|
|
186
|
+
return None
|
|
187
|
+
return str(value).strip().lower().split()[0].split("(", 1)[0]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _mask_identifier(value):
|
|
191
|
+
if not value:
|
|
192
|
+
return None
|
|
193
|
+
text = str(value)
|
|
194
|
+
return text if len(text) <= 8 else f"{text[:6]}...{text[-4:]}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _recent_codex_observed_account(auth_home):
|
|
198
|
+
for path in _recent_codex_log_paths(auth_home):
|
|
199
|
+
text = _read_tail(path)
|
|
200
|
+
account = _extract_account_identity(text)
|
|
201
|
+
if account:
|
|
202
|
+
return account
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _recent_codex_auth_error(auth_home):
|
|
207
|
+
markers = ("token_expired", "authentication token is expired", "http 401")
|
|
208
|
+
for path in _recent_codex_log_paths(auth_home):
|
|
209
|
+
text = _read_tail(path).lower()
|
|
210
|
+
found = [marker for marker in markers if marker in text]
|
|
211
|
+
if found:
|
|
212
|
+
try:
|
|
213
|
+
mtime = datetime.fromtimestamp(os.path.getmtime(path), timezone.utc).isoformat().replace("+00:00", "Z")
|
|
214
|
+
except OSError:
|
|
215
|
+
mtime = None
|
|
216
|
+
return {"source": path, "mtime": mtime, "markers": found}
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _recent_codex_log_paths(auth_home):
|
|
221
|
+
if not auth_home:
|
|
222
|
+
return []
|
|
223
|
+
candidates = []
|
|
224
|
+
for root in (os.path.join(auth_home, "log"), os.path.join(auth_home, "sessions")):
|
|
225
|
+
if not os.path.isdir(root):
|
|
226
|
+
continue
|
|
227
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
228
|
+
dirnames[:] = [name for name in dirnames if name not in {"cache", "plugins", "skills", "sqlite"}]
|
|
229
|
+
for filename in filenames:
|
|
230
|
+
if filename.endswith((".log", ".jsonl")):
|
|
231
|
+
path = os.path.join(dirpath, filename)
|
|
232
|
+
try:
|
|
233
|
+
stat = os.stat(path)
|
|
234
|
+
except OSError:
|
|
235
|
+
continue
|
|
236
|
+
candidates.append((stat.st_mtime, path))
|
|
237
|
+
return [path for _mtime, path in sorted(candidates, reverse=True)[:16]]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _read_tail(path, max_bytes=128 * 1024):
|
|
241
|
+
try:
|
|
242
|
+
with open(path, "rb") as handle:
|
|
243
|
+
handle.seek(0, os.SEEK_END)
|
|
244
|
+
size = handle.tell()
|
|
245
|
+
handle.seek(max(0, size - max_bytes))
|
|
246
|
+
return handle.read().decode("utf-8", errors="replace")
|
|
247
|
+
except OSError:
|
|
248
|
+
return ""
|
|
249
|
+
|
|
250
|
+
|
|
126
251
|
def _check_cdx_home(base_dir):
|
|
127
252
|
try:
|
|
128
253
|
os.makedirs(base_dir, exist_ok=True)
|
package/src/provider_runtime.py
CHANGED
|
@@ -166,6 +166,19 @@ def _read_codex_account_email(auth_home):
|
|
|
166
166
|
return None
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def _read_codex_account_id(auth_home):
|
|
170
|
+
try:
|
|
171
|
+
with open(os.path.join(auth_home, "auth.json"), encoding="utf-8") as handle:
|
|
172
|
+
auth = json.load(handle)
|
|
173
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
174
|
+
return None
|
|
175
|
+
tokens = auth.get("tokens") if isinstance(auth, dict) else {}
|
|
176
|
+
account_id = tokens.get("account_id") if isinstance(tokens, dict) else None
|
|
177
|
+
if not account_id:
|
|
178
|
+
return None
|
|
179
|
+
return str(account_id).strip() or None
|
|
180
|
+
|
|
181
|
+
|
|
169
182
|
def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
|
|
170
183
|
auth_home = _get_auth_home(session)
|
|
171
184
|
auth_path = os.path.join(auth_home, "auth.json")
|
|
@@ -174,6 +187,7 @@ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
|
|
|
174
187
|
"auth_json_exists": os.path.isfile(auth_path),
|
|
175
188
|
"local_tokens_present": _has_local_codex_auth(auth_home),
|
|
176
189
|
"account_email": _read_codex_account_email(auth_home),
|
|
190
|
+
"account_id": _read_codex_account_id(auth_home),
|
|
177
191
|
"live_status": "unknown",
|
|
178
192
|
"live_error": None,
|
|
179
193
|
}
|