cdx-manager 0.24.1 → 0.24.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 +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_24_2.md +14 -0
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +4 -1
- package/docs/operations.md +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +4 -3
- package/src/cli_args.py +1 -1
- package/src/cli_helpers.py +8 -8
- package/src/commands/launch.py +12 -9
- package/src/commands/status.py +1 -1
- package/src/provider_runtime.py +21 -0
- package/src/run_command.py +12 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.24.
|
|
1
|
+
0.24.2
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CDX Manager 0.24.2
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-29
|
|
4
|
+
|
|
5
|
+
This patch makes busy-session handling explicit and surfaces headless permission denials without marking successful runs as failures.
|
|
6
|
+
|
|
7
|
+
## Sessions
|
|
8
|
+
|
|
9
|
+
- Add `--on-busy fail|ask` to session launches and resumes. Use `fail` to refuse a profile with a live session immediately with a non-zero exit and `session_busy` error. The default `ask` keeps interactive confirmation; without a terminal or with `--json`, it refuses the duplicate launch. The policy also works with task, directory, and prompt options. (#55)
|
|
10
|
+
|
|
11
|
+
## Runs
|
|
12
|
+
|
|
13
|
+
- Report nonempty Claude `permission_denials` result records as structured run warnings, including the provider and denied tools. A provider exit code of 0 still reports success (`ok: true`), since another tool may have completed the work. Empty or absent denials produce no warning. (#54)
|
|
14
|
+
- Clarify that Claude's `--scrub-env on` forces its default permission mode: unattended runs must explicitly allow tools that need approval, using `--allowedTools` through `--extra-args`. (#54)
|
|
@@ -584,6 +584,17 @@
|
|
|
584
584
|
"x86_64-pc-windows-msvc": "90772ff35a25db86ebfbe4187c39f0705001a9f10b6a7fe0c04a431b13ccbcb8",
|
|
585
585
|
"x86_64-unknown-linux-musl": "f7be523211fc4dba33724c2759f267f35669b8aff14136fc7ec97d398ebb1f87"
|
|
586
586
|
}
|
|
587
|
+
},
|
|
588
|
+
"v0.24.1": {
|
|
589
|
+
"github_tarball_sha256": "5ea0ba5fe38590f5bc8e4ae6d299ebce1dc2ef960501914d5520a2f2901888f5",
|
|
590
|
+
"github_zip_sha256": "d48ebf567fe85ff36a516cd84e9cc34a4af2eb1145185e3f3d3ea30ef5eaf71c",
|
|
591
|
+
"tray_assets": {
|
|
592
|
+
"aarch64-apple-darwin": "bf231ab3618ea189333b74a1a9cc71622dc79b00c7e2549967d1da5ddb22dc3e",
|
|
593
|
+
"aarch64-unknown-linux-musl": "009a4e2d4b54ac67f3991d879d5875c82dddf6161b0649986240ebc57285a9e8",
|
|
594
|
+
"x86_64-apple-darwin": "256cb6644394ab7140f6353480aedf000f47f930d2b2aa3f905b6a57bf86714f",
|
|
595
|
+
"x86_64-pc-windows-msvc": "89a7e43654ea5f0d4efc20a14c1398377a1d48485f1b9867b199e85aa9c25daf",
|
|
596
|
+
"x86_64-unknown-linux-musl": "a36a3589a3766d67e44a0466b11a18bda4297963416eec7b5b9d23a5c9f363a4"
|
|
597
|
+
}
|
|
587
598
|
}
|
|
588
599
|
}
|
|
589
600
|
}
|
package/docs/cli.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
| `cdx` | List all sessions with last-updated timestamps |
|
|
8
8
|
| `cdx --json` | List all sessions as a machine-readable JSON payload |
|
|
9
9
|
| `cdx <name>` | Launch a session (checks auth first) |
|
|
10
|
-
| `cdx <name> [--json]` | Launch a session; `--json` returns a structured success payload after the interactive run ends |
|
|
10
|
+
| `cdx <name> [--on-busy fail\|ask] [--json]` | Launch a session; `--json` returns a structured success payload after the interactive run ends |
|
|
11
|
+
| `cdx <name> --on-busy fail` | Refuse a profile with a live session immediately, even with a terminal: non-zero exit and `session_busy` (`error.code` with `--json`). Works with `--task`, `--dir`, prompts and resume; `--on-busy=fail` is also accepted. The default `ask` keeps the confirmation prompt for terminal users; without a terminal or with `--json`, it fails instead of launching a duplicate. Handoff launches and `cdx last` use `ask` too. |
|
|
11
12
|
| `cdx <name> --prompt TEXT` / `cdx <name> --prompt-file PATH` | Launch a session that starts on this first message (not with `--resume` or Ollama sessions; `--prompt-file -` is refused because the session needs standard input; a relative `--prompt-file` path is read from the current directory, not `--dir`) |
|
|
12
13
|
| `cdx <name> --dir PATH --task TEXT [--end-state TEXT]` | Start a session on a task. `--end-state` names what your go-ahead covers ("CI green on a PR, merged"): the session takes every step it needs, commits, pushes and PRs included, and stops only for a decision outside it. Without `--end-state` it stops and shows the result before any commit, push or PR. Replaces `--prompt`; the first message also tells it to read the repository's instructions and use its skills. |
|
|
13
14
|
| `cdx <name> -r` / `cdx <name> --resume` | Resume the provider-native conversation for a session when supported |
|
|
@@ -422,3 +423,5 @@ It returns accepted values for `permission` (with its aliases and canonical form
|
|
|
422
423
|
```
|
|
423
424
|
|
|
424
425
|
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.
|
|
426
|
+
|
|
427
|
+
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)).
|
package/docs/operations.md
CHANGED
|
@@ -230,7 +230,7 @@ credential sync.
|
|
|
230
230
|
|
|
231
231
|
- **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first. `cdx doctor` uses the same Codex app-server probe as `cdx auth refresh`, so a local auth file alone is never reported as live authentication; a rejected refresh token is reported as `login_required` with the matching login command.
|
|
232
232
|
- **A Claude session has no claude.ai connectors (Microsoft 365, ...) in `/mcp`** — `cdx login <name>` stores a setup-token for Claude profiles, and Claude Code can only make model requests with it: it never loads claude.ai connectors. `cdx status` shows each Claude profile's auth type (`setup-token` or `full-login`; `auth_type` in `--json`). To switch, start the session and run `/login` inside it. Once the profile holds its own claude.ai login (its `.claude/.credentials.json`, or its profile-specific `Claude Code-credentials-<hash>` keychain entry on macOS), cdx stops passing the setup-token on later launches. If that login later expires, run `/login` again, or go back to a setup-token with `cdx login <name>`, which removes the profile's `/login`.
|
|
233
|
-
- **Keep credentials away from what a Claude agent runs** — by default Claude Code passes its credentials, including the setup-token cdx injects, to the Bash commands, hooks and stdio MCP servers it starts. `cdx set <name> --scrub-env on` launches that session with `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1`, which strips credential-looking variables from those subprocesses (GitHub tokens and proxy settings stay). It is off by default: it also removes other secrets an agent may need (`NPM_TOKEN`, a `DATABASE_URL` with a password), and on Linux it isolates Bash from host processes, so `ps` and `kill` no longer see them.
|
|
233
|
+
- **Keep credentials away from what a Claude agent runs** — by default Claude Code passes its credentials, including the setup-token cdx injects, to the Bash commands, hooks and stdio MCP servers it starts. `cdx set <name> --scrub-env on` launches that session with `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1`, which strips credential-looking variables from those subprocesses (GitHub tokens and proxy settings stay). It is off by default: it also removes other secrets an agent may need (`NPM_TOKEN`, a `DATABASE_URL` with a password), and on Linux it isolates Bash from host processes, so `ps` and `kill` no longer see them. For unattended sessions that read untrusted content, enable it only with explicit tool permissions: Claude forces its default permission mode when scrubbing is enabled, even if cdx requests `--permission full`. In headless runs, tools that require approval are denied unless explicitly allowed. For example, `cdx set <name> --scrub-env on --extra-args '--allowedTools "Read" "Bash(pwd)"'` allows those tools; adapt the allowlist to the task. cdx keeps scrubbing enabled and reports structured `permission_denials` as a run warning, without turning a successful provider exit into a failure: a denied tool can be replaced successfully.
|
|
234
234
|
- **Share Claude's memory across accounts** — Claude Code keeps its auto-memory (`.claude/projects/<project>/memory`) in the profile home, so each account starts without what the others learned. `cdx set <name> --shared-memory on` links that folder, for the project being launched, to `~/.cdx/shared-memory/<project>`, shared by every profile with the setting on. Existing memory is copied in and the old folder kept as `memory.pre-cdx-<time>`; `MEMORY.md` indexes are merged line by line. A file with the same name and different content on two accounts is a conflict: cdx leaves that profile alone and names the files to merge by hand. macOS and Linux only (symlinks).
|
|
235
235
|
- **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.
|
|
236
236
|
- **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -295,7 +295,8 @@ def _print_help(use_color=False):
|
|
|
295
295
|
_style("Notifications:", "1", use_color),
|
|
296
296
|
f" {_style('cdx set <name> --notify on', '36', use_color)} # agent finishes or needs attention",
|
|
297
297
|
f" {_style('cdx ready', '36', use_color)} # next session to become ready",
|
|
298
|
-
f" {_style('cdx <name> [--json]', '36', use_color)}",
|
|
298
|
+
f" {_style('cdx <name> [--on-busy fail|ask] [--json]', '36', use_color)}",
|
|
299
|
+
" --on-busy: fail immediately if already running; ask (default) prompts only with a terminal and without --json",
|
|
299
300
|
f" {_style('cdx <name> --prompt TEXT|--prompt-file PATH', '36', use_color)} # start with a first message",
|
|
300
301
|
f" {_style('cdx <name> --dir PATH --task TEXT --end-state TEXT', '36', use_color)} # start on a task, with what the go-ahead covers",
|
|
301
302
|
f" {_style('cdx --help', '36', use_color)}",
|
|
@@ -637,7 +638,7 @@ def main(argv, options=None):
|
|
|
637
638
|
return handler(rest, ctx)
|
|
638
639
|
|
|
639
640
|
from .commands.launch import _parse_launch_args, _read_launch_prompt
|
|
640
|
-
directory, launch_args, prompt = _parse_launch_args(rest)
|
|
641
|
+
directory, launch_args, prompt, on_busy = _parse_launch_args(rest)
|
|
641
642
|
if all(arg in ("--json", "-r", "--resume") for arg in launch_args):
|
|
642
643
|
resume = "-r" in launch_args or "--resume" in launch_args
|
|
643
644
|
has_prompt = prompt["prompt"] is not None or prompt["prompt_file"] is not None
|
|
@@ -647,7 +648,7 @@ def main(argv, options=None):
|
|
|
647
648
|
if has_prompt and (ctx["service"]["get_session"](command) or {}).get("provider") == PROVIDER_OLLAMA:
|
|
648
649
|
raise CdxError("cdx <name>: --prompt and --prompt-file are not supported for Ollama sessions.")
|
|
649
650
|
return handle_launch(command, ctx, initial_prompt=_read_launch_prompt(prompt), resume=resume,
|
|
650
|
-
directory=directory)
|
|
651
|
+
directory=directory, on_busy=on_busy)
|
|
651
652
|
|
|
652
653
|
raise CdxError(f"Unknown command: {command}. Use cdx --help.")
|
|
653
654
|
|
package/src/cli_args.py
CHANGED
|
@@ -42,7 +42,7 @@ HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|t
|
|
|
42
42
|
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
|
|
43
43
|
STATS_GROUPS = ("session", "project", "model")
|
|
44
44
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
45
|
-
RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--json]"
|
|
45
|
+
RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--on-busy fail|ask] [--json]"
|
|
46
46
|
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
|
|
47
47
|
SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
48
48
|
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"
|
package/src/cli_helpers.py
CHANGED
|
@@ -386,22 +386,22 @@ def _resume_capability_for_session(session, ctx):
|
|
|
386
386
|
"command_preview": capability.get("command_preview") or [],
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
def _warn_if_session_already_running(name, ctx):
|
|
390
|
-
# ponytail: concurrent sessions on one profile share its single OAuth
|
|
391
|
-
# .credentials.json; both refresh near token expiry and the rotated refresh
|
|
392
|
-
# token invalidates the other session -> surprise logout. Warn before adding
|
|
393
|
-
# a second live session on the same profile.
|
|
389
|
+
def _warn_if_session_already_running(name, ctx, *, on_busy="ask", json_flag=False):
|
|
394
390
|
active = ctx["service"].get("active_session_runtime")
|
|
395
391
|
runtime = active(name) if active else None
|
|
396
392
|
if not runtime:
|
|
397
393
|
return
|
|
394
|
+
if on_busy == "fail" or not ctx["stdin_is_tty"] or json_flag:
|
|
395
|
+
raise CdxError(f"session_busy: Session {name} is already running.", code="session_busy")
|
|
398
396
|
pid = runtime.get("pid")
|
|
399
397
|
ctx["out"](f"{_warn(f'A session named {name} is already running (pid {pid}).', ctx['use_color'])}\n")
|
|
400
398
|
ctx["out"](
|
|
401
399
|
f"{_dim('Two sessions on one profile share its login and can log each other out. Use a different profile instead.', ctx['use_color'])}\n"
|
|
402
400
|
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
401
|
+
ask = ctx["options"].get("input") or input
|
|
402
|
+
try:
|
|
403
|
+
answer = ask(f"Launch a second {name} session anyway? [y/N] ")
|
|
404
|
+
except EOFError:
|
|
405
|
+
raise CdxError(f"session_busy: Session {name} is already running.", code="session_busy") from None
|
|
406
406
|
if answer.strip().lower() not in ("y", "yes"):
|
|
407
407
|
raise CdxError("Launch cancelled.")
|
package/src/commands/launch.py
CHANGED
|
@@ -36,7 +36,7 @@ from ..provider_runtime import (
|
|
|
36
36
|
from ..run_command import PROMPT_STDIN, read_run_prompt
|
|
37
37
|
from ..run_usage import estimate_cost
|
|
38
38
|
|
|
39
|
-
LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--json]"
|
|
39
|
+
LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--on-busy fail|ask] [--json]"
|
|
40
40
|
|
|
41
41
|
#: The first message of a session started with --task. The end state is what the
|
|
42
42
|
#: user's go-ahead covers: a bare "show me first" made sessions stop after a commit
|
|
@@ -52,8 +52,8 @@ NO_END_STATE = """No end state was given: stop and show the result before any co
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _parse_launch_args(args):
|
|
55
|
-
"""Return the
|
|
56
|
-
values = {"--dir": None, "--prompt": None, "--prompt-file": None, "--task": None, "--end-state": None}
|
|
55
|
+
"""Return the directory, remaining flags, prompt source and busy policy."""
|
|
56
|
+
values = {"--dir": None, "--prompt": None, "--prompt-file": None, "--task": None, "--end-state": None, "--on-busy": None}
|
|
57
57
|
cleaned = []
|
|
58
58
|
index = 0
|
|
59
59
|
while index < len(args):
|
|
@@ -72,6 +72,9 @@ def _parse_launch_args(args):
|
|
|
72
72
|
if values[flag] is not None or not value:
|
|
73
73
|
raise CdxError(LAUNCH_USAGE)
|
|
74
74
|
values[flag] = value
|
|
75
|
+
on_busy = values["--on-busy"] or "ask"
|
|
76
|
+
if on_busy not in ("fail", "ask"):
|
|
77
|
+
raise CdxError("cdx <name>: --on-busy must be fail or ask.")
|
|
75
78
|
if values["--prompt"] is not None and values["--prompt-file"] is not None:
|
|
76
79
|
raise CdxError("cdx <name>: --prompt and --prompt-file are mutually exclusive.")
|
|
77
80
|
if values["--end-state"] is not None and values["--task"] is None:
|
|
@@ -84,7 +87,7 @@ def _parse_launch_args(args):
|
|
|
84
87
|
if values["--prompt-file"] == PROMPT_STDIN:
|
|
85
88
|
raise CdxError("cdx <name>: --prompt-file - is not supported; the interactive session needs standard input.")
|
|
86
89
|
prompt = {"prompt": values["--prompt"], "prompt_file": values["--prompt-file"]}
|
|
87
|
-
return values["--dir"], cleaned, prompt
|
|
90
|
+
return values["--dir"], cleaned, prompt, on_busy
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
def _read_launch_prompt(prompt):
|
|
@@ -204,14 +207,14 @@ def handle_can_resume(rest, ctx):
|
|
|
204
207
|
|
|
205
208
|
def handle_resume(rest, ctx):
|
|
206
209
|
conversation_id, rest = _pop_value_flag(rest, "--conversation", RESUME_USAGE)
|
|
207
|
-
directory, rest, prompt = _parse_launch_args(rest)
|
|
210
|
+
directory, rest, prompt, on_busy = _parse_launch_args(rest)
|
|
208
211
|
if prompt["prompt"] is not None or prompt["prompt_file"] is not None:
|
|
209
212
|
raise CdxError("cdx resume: --prompt and --prompt-file are not supported when resuming.")
|
|
210
213
|
json_flag, args = _parse_json_flag(rest)
|
|
211
214
|
if len(args) != 1:
|
|
212
215
|
raise CdxError(RESUME_USAGE)
|
|
213
216
|
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory,
|
|
214
|
-
conversation_id=conversation_id)
|
|
217
|
+
conversation_id=conversation_id, on_busy=on_busy)
|
|
215
218
|
|
|
216
219
|
|
|
217
220
|
def _notification_provisioning_enabled(session, ctx, json_flag):
|
|
@@ -255,14 +258,14 @@ def _link_shared_memory(session, ctx, cwd, json_flag):
|
|
|
255
258
|
|
|
256
259
|
|
|
257
260
|
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None,
|
|
258
|
-
conversation_id=None):
|
|
261
|
+
conversation_id=None, on_busy="ask"):
|
|
259
262
|
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
260
263
|
if force_json is not None:
|
|
261
264
|
json_flag = force_json
|
|
262
265
|
warnings = _update_notice_warnings(ctx)
|
|
263
266
|
active_runtime = ctx["service"].get("active_session_runtime")
|
|
264
267
|
overlap_at_start = bool(active_runtime and active_runtime(command))
|
|
265
|
-
_warn_if_session_already_running(command, ctx)
|
|
268
|
+
_warn_if_session_already_running(command, ctx, on_busy=on_busy, json_flag=json_flag)
|
|
266
269
|
session = ctx["service"]["get_session"](command)
|
|
267
270
|
if not session:
|
|
268
271
|
raise CdxError(f"Unknown session: {command}")
|
|
@@ -595,4 +598,4 @@ def handle_handoff(rest, ctx):
|
|
|
595
598
|
))
|
|
596
599
|
return 0
|
|
597
600
|
ctx["out"](f"Handoff ready ({entry['mode']}): {path}\n")
|
|
598
|
-
return handle_launch(target['name'], ctx, initial_prompt=prompt, directory=workspace)
|
|
601
|
+
return handle_launch(target['name'], ctx, initial_prompt=prompt, directory=workspace, on_busy="ask")
|
package/src/commands/status.py
CHANGED
|
@@ -693,7 +693,7 @@ def handle_last(rest, ctx):
|
|
|
693
693
|
if not json_flag:
|
|
694
694
|
message = f"Launching last session: {session['name']}"
|
|
695
695
|
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
696
|
-
return handle_launch(session["name"], ctx)
|
|
696
|
+
return handle_launch(session["name"], ctx, on_busy="ask")
|
|
697
697
|
|
|
698
698
|
def handle_status(rest, ctx):
|
|
699
699
|
parsed = _parse_flag_args(rest, {
|
package/src/provider_runtime.py
CHANGED
|
@@ -26,6 +26,7 @@ from .config import (
|
|
|
26
26
|
split_extra_args,
|
|
27
27
|
)
|
|
28
28
|
from .errors import CdxError
|
|
29
|
+
from .run_usage import _parse_json_records
|
|
29
30
|
|
|
30
31
|
TERMINAL_TITLE_SEPARATOR = " — "
|
|
31
32
|
# Providers redraw their own title while their TUI runs, so a single write at
|
|
@@ -1184,6 +1185,22 @@ def _terminate_child_tree(child):
|
|
|
1184
1185
|
pass
|
|
1185
1186
|
|
|
1186
1187
|
|
|
1188
|
+
def _read_permission_denials(stdout_path):
|
|
1189
|
+
denials = []
|
|
1190
|
+
try:
|
|
1191
|
+
with open(stdout_path, encoding="utf-8", errors="replace") as handle:
|
|
1192
|
+
records = _parse_json_records(handle.read())
|
|
1193
|
+
except OSError:
|
|
1194
|
+
return denials
|
|
1195
|
+
for record in records:
|
|
1196
|
+
if not isinstance(record, dict) or record.get("type") != "result":
|
|
1197
|
+
continue
|
|
1198
|
+
reported = record.get("permission_denials")
|
|
1199
|
+
if isinstance(reported, list):
|
|
1200
|
+
denials.extend(item for item in reported if isinstance(item, dict))
|
|
1201
|
+
return denials
|
|
1202
|
+
|
|
1203
|
+
|
|
1187
1204
|
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
1188
1205
|
timeout_seconds=None, spawn=None, run_id=None):
|
|
1189
1206
|
spawn = spawn or subprocess.Popen
|
|
@@ -1277,6 +1294,10 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
1277
1294
|
"pid": getattr(child, "pid", None),
|
|
1278
1295
|
"returncode": returncode,
|
|
1279
1296
|
"timed_out": timed_out,
|
|
1297
|
+
"permission_denials": (
|
|
1298
|
+
_read_permission_denials(paths["stdout_path"])
|
|
1299
|
+
if session.get("provider") == PROVIDER_CLAUDE else []
|
|
1300
|
+
),
|
|
1280
1301
|
}
|
|
1281
1302
|
|
|
1282
1303
|
|
package/src/run_command.py
CHANGED
|
@@ -192,6 +192,17 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
|
192
192
|
if error_source == "provider"
|
|
193
193
|
else (str(error) if error else "Run failed.")
|
|
194
194
|
)
|
|
195
|
+
warnings = run_warnings(parsed, session, selection_row)
|
|
196
|
+
if run_info.get("permission_denials"):
|
|
197
|
+
warnings.append({
|
|
198
|
+
"code": "permission_denials",
|
|
199
|
+
"message": (
|
|
200
|
+
"The provider reported denied tool calls. A denied tool may have been "
|
|
201
|
+
"replaced successfully; check the denials and transcript for impact."
|
|
202
|
+
),
|
|
203
|
+
"provider": session.get("provider") if session else parsed.get("provider"),
|
|
204
|
+
"permission_denials": run_info["permission_denials"],
|
|
205
|
+
})
|
|
195
206
|
return {
|
|
196
207
|
"schema_version": api_schema_version,
|
|
197
208
|
"ok": bool(ok),
|
|
@@ -210,7 +221,7 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
|
210
221
|
"stdout_path": run_info.get("stdout_path"),
|
|
211
222
|
"stderr_path": run_info.get("stderr_path"),
|
|
212
223
|
"usage": usage,
|
|
213
|
-
"warnings":
|
|
224
|
+
"warnings": warnings,
|
|
214
225
|
"error": None if ok else {
|
|
215
226
|
"source": error_source or "cdx",
|
|
216
227
|
"code": error_code or "cdx_error",
|