xdog-coding 2.1.2__tar.gz → 2.2.0__tar.gz
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.
- xdog_coding-2.2.0/PKG-INFO +95 -0
- xdog_coding-2.2.0/README.md +67 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/pyproject.toml +4 -4
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/args.py +10 -1
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/config_selector.py +10 -1
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/config.py +9 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/agent_session.py +36 -3
- xdog_coding-2.2.0/src/xdog/coding/core/permissions.py +275 -0
- xdog_coding-2.2.0/src/xdog/coding/core/remote_session.py +39 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/sdk.py +41 -11
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/slash_commands.py +6 -3
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/main.py +8 -2
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/components/assistant_message.py +63 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/components/chat_log.py +64 -27
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/components/custom_editor.py +19 -0
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/components/editor_layout.py +5 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/components/footer.py +29 -2
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/components/permission_prompt.py +91 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/components/tool_execution.py +119 -64
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/components/user_message.py +2 -1
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/interactive_mode.py +967 -0
- xdog_coding-2.2.0/src/xdog/coding/modes/interactive/tool_renderers.py +24 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/print_mode.py +49 -0
- xdog_coding-2.2.0/src/xdog/coding/modes/rpc/rpc_mode.py +231 -0
- xdog_coding-2.2.0/tests/fixtures/terminal_client.py +83 -0
- xdog_coding-2.2.0/tests/test_cli_args.py +44 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_cli_config_selector.py +3 -2
- xdog_coding-2.2.0/tests/test_compact_layout.py +471 -0
- xdog_coding-2.2.0/tests/test_footer_context.py +40 -0
- xdog_coding-2.2.0/tests/test_interactive_mode.py +875 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_messages.py +27 -5
- xdog_coding-2.2.0/tests/test_permissions.py +121 -0
- xdog_coding-2.2.0/tests/test_rpc_mode.py +93 -0
- xdog_coding-2.2.0/tests/test_sdk_session_state.py +63 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_session_smoke.py +23 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_slash_commands.py +23 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_slash_skills.py +1 -1
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_system_prompt.py +2 -0
- xdog_coding-2.2.0/tests/test_terminal_acceptance.py +261 -0
- xdog_coding-2.1.2/PKG-INFO +0 -55
- xdog_coding-2.1.2/README.md +0 -27
- xdog_coding-2.1.2/src/xdog/coding/modes/interactive/components/assistant_message.py +0 -22
- xdog_coding-2.1.2/src/xdog/coding/modes/interactive/components/custom_editor.py +0 -260
- xdog_coding-2.1.2/src/xdog/coding/modes/interactive/interactive_mode.py +0 -543
- xdog_coding-2.1.2/src/xdog/coding/modes/rpc/rpc_mode.py +0 -187
- xdog_coding-2.1.2/tests/test_cli_args.py +0 -23
- xdog_coding-2.1.2/tests/test_footer_context.py +0 -18
- xdog_coding-2.1.2/tests/test_interactive_mode.py +0 -145
- xdog_coding-2.1.2/tests/test_rpc_mode.py +0 -10
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/.gitignore +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/LICENSE +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/LICENSE-EXCEPTION.md +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/file_processor.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/initial_message.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/list_models.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/cli/session_picker.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/bash_executor.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/compaction/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/compaction/compaction.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/compaction/utils.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/defaults.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/event_bus.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/exec_utils.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/extensions/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/extensions/loader.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/extensions/runner.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/extensions/types.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/keybindings.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/messages.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/prompt_templates.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/resource_loader.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/session_manager.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/settings_manager.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/system_prompt.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/timings.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/core/tools/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/components/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/interactive/theme.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/modes/rpc/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/py.typed +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/__init__.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/frontmatter.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/git.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/mime.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/shell.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/src/xdog/coding/utils/sleep.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_agent_session.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_cli_file_processor.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_coding_config.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_event_bus.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_prompt_templates.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_resource_loader.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_session_manager.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_settings_manager.py +0 -0
- {xdog_coding-2.1.2 → xdog_coding-2.2.0}/tests/test_timings.py +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xdog-coding
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Interactive coding agent CLI
|
|
5
|
+
Project-URL: Homepage, https://xdog.942295.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/litterzhang/xdog
|
|
7
|
+
Project-URL: Documentation, https://xdog.942295.xyz
|
|
8
|
+
Project-URL: Issues, https://github.com/litterzhang/xdog/issues
|
|
9
|
+
Author: HugeMan
|
|
10
|
+
License-Expression: AGPL-3.0-or-later
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: LICENSE-EXCEPTION.md
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: click>=8.1.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: xdog-agent>=2.2.0
|
|
25
|
+
Requires-Dist: xdog-ai[openai]>=2.2.0
|
|
26
|
+
Requires-Dist: xdog-tui>=2.2.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# xdog-coding
|
|
30
|
+
|
|
31
|
+
**Interactive coding-agent CLI.**
|
|
32
|
+
|
|
33
|
+
A terminal coding agent with session management, built on
|
|
34
|
+
[`xdog-agent`](https://pypi.org/project/xdog-agent/) and the
|
|
35
|
+
[`xdog-tui`](https://pypi.org/project/xdog-tui/) rendering layer.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run xdog-coding
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Interactive terminal
|
|
42
|
+
|
|
43
|
+
The interface uses native scrollback, one status row, and a shared multiline
|
|
44
|
+
editor. Enter submits or queues; Shift/Alt+Enter inserts a newline. Ctrl+O opens
|
|
45
|
+
recent tool/reasoning details: Left/Right chooses an entry and Page Up/Page Down
|
|
46
|
+
scrolls. Escape closes autocomplete/details first, then cancels active work and
|
|
47
|
+
restores queued text ahead of the current draft. Ctrl+Z suspends on POSIX.
|
|
48
|
+
|
|
49
|
+
Permission prompts take priority. Up/Down chooses an action, Enter confirms,
|
|
50
|
+
Escape denies that call, and Page Up/Page Down scrolls the command summary.
|
|
51
|
+
The editor draft stays intact when panels open or the terminal resizes.
|
|
52
|
+
See [terminal verification](../../docs/tui-pi-parity.md); remote coding and
|
|
53
|
+
fullscreen application modes are excluded.
|
|
54
|
+
|
|
55
|
+
## Tool permissions
|
|
56
|
+
|
|
57
|
+
Potentially mutating tool calls are gated immediately before execution. By
|
|
58
|
+
default, read-only filesystem operations and `current_time` run automatically;
|
|
59
|
+
`bash`, filesystem writes/edits/deletes, and unknown extension tools ask first.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
xdog-coding --permission-mode ask # default: ask for dangerous calls
|
|
63
|
+
xdog-coding --permission-mode ask-all # ask before every tool
|
|
64
|
+
xdog-coding --permission-mode allow-all # trusted unattended automation
|
|
65
|
+
xdog-coding --permission-mode deny # allow read-only calls only
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Interactive approvals offer **Allow once**, **Allow for this session** (the exact
|
|
69
|
+
same call only), and **Deny**. Non-interactive runs fail closed when no approver
|
|
70
|
+
is available unless `allow-all` was explicitly selected.
|
|
71
|
+
|
|
72
|
+
The same setting can be stored in `~/.config/xdog/coding/settings.json`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{"permission_mode": "ask"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Permission prompts are an execution gate, not an OS sandbox: an approved shell
|
|
79
|
+
command still has the permissions of the `xdog-coding` process.
|
|
80
|
+
|
|
81
|
+
## Part of xdog
|
|
82
|
+
|
|
83
|
+
This package is one piece of [xdog](https://github.com/litterzhang/xdog), a
|
|
84
|
+
local-first toolkit for building, running and scheduling LLM workflows. The
|
|
85
|
+
centrepiece is [`xdog-flow`](https://pypi.org/project/xdog-flow/) — a typed
|
|
86
|
+
workflow format and compiler.
|
|
87
|
+
|
|
88
|
+
Documentation: **https://xdog.942295.xyz**
|
|
89
|
+
|
|
90
|
+
## Licence
|
|
91
|
+
|
|
92
|
+
Copyright (c) 2026 HugeMan <942295.xyz>
|
|
93
|
+
|
|
94
|
+
GNU Affero General Public License v3.0 or later — see
|
|
95
|
+
[LICENSE](https://github.com/litterzhang/xdog/blob/main/LICENSE).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# xdog-coding
|
|
2
|
+
|
|
3
|
+
**Interactive coding-agent CLI.**
|
|
4
|
+
|
|
5
|
+
A terminal coding agent with session management, built on
|
|
6
|
+
[`xdog-agent`](https://pypi.org/project/xdog-agent/) and the
|
|
7
|
+
[`xdog-tui`](https://pypi.org/project/xdog-tui/) rendering layer.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv run xdog-coding
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Interactive terminal
|
|
14
|
+
|
|
15
|
+
The interface uses native scrollback, one status row, and a shared multiline
|
|
16
|
+
editor. Enter submits or queues; Shift/Alt+Enter inserts a newline. Ctrl+O opens
|
|
17
|
+
recent tool/reasoning details: Left/Right chooses an entry and Page Up/Page Down
|
|
18
|
+
scrolls. Escape closes autocomplete/details first, then cancels active work and
|
|
19
|
+
restores queued text ahead of the current draft. Ctrl+Z suspends on POSIX.
|
|
20
|
+
|
|
21
|
+
Permission prompts take priority. Up/Down chooses an action, Enter confirms,
|
|
22
|
+
Escape denies that call, and Page Up/Page Down scrolls the command summary.
|
|
23
|
+
The editor draft stays intact when panels open or the terminal resizes.
|
|
24
|
+
See [terminal verification](../../docs/tui-pi-parity.md); remote coding and
|
|
25
|
+
fullscreen application modes are excluded.
|
|
26
|
+
|
|
27
|
+
## Tool permissions
|
|
28
|
+
|
|
29
|
+
Potentially mutating tool calls are gated immediately before execution. By
|
|
30
|
+
default, read-only filesystem operations and `current_time` run automatically;
|
|
31
|
+
`bash`, filesystem writes/edits/deletes, and unknown extension tools ask first.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
xdog-coding --permission-mode ask # default: ask for dangerous calls
|
|
35
|
+
xdog-coding --permission-mode ask-all # ask before every tool
|
|
36
|
+
xdog-coding --permission-mode allow-all # trusted unattended automation
|
|
37
|
+
xdog-coding --permission-mode deny # allow read-only calls only
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Interactive approvals offer **Allow once**, **Allow for this session** (the exact
|
|
41
|
+
same call only), and **Deny**. Non-interactive runs fail closed when no approver
|
|
42
|
+
is available unless `allow-all` was explicitly selected.
|
|
43
|
+
|
|
44
|
+
The same setting can be stored in `~/.config/xdog/coding/settings.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{"permission_mode": "ask"}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Permission prompts are an execution gate, not an OS sandbox: an approved shell
|
|
51
|
+
command still has the permissions of the `xdog-coding` process.
|
|
52
|
+
|
|
53
|
+
## Part of xdog
|
|
54
|
+
|
|
55
|
+
This package is one piece of [xdog](https://github.com/litterzhang/xdog), a
|
|
56
|
+
local-first toolkit for building, running and scheduling LLM workflows. The
|
|
57
|
+
centrepiece is [`xdog-flow`](https://pypi.org/project/xdog-flow/) — a typed
|
|
58
|
+
workflow format and compiler.
|
|
59
|
+
|
|
60
|
+
Documentation: **https://xdog.942295.xyz**
|
|
61
|
+
|
|
62
|
+
## Licence
|
|
63
|
+
|
|
64
|
+
Copyright (c) 2026 HugeMan <942295.xyz>
|
|
65
|
+
|
|
66
|
+
GNU Affero General Public License v3.0 or later — see
|
|
67
|
+
[LICENSE](https://github.com/litterzhang/xdog/blob/main/LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xdog-coding"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Interactive coding agent CLI"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
classifiers = [
|
|
@@ -21,9 +21,9 @@ readme = "README.md"
|
|
|
21
21
|
license = "AGPL-3.0-or-later"
|
|
22
22
|
license-files = ["LICENSE", "LICENSE-EXCEPTION.md"]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"xdog-ai>=2.
|
|
25
|
-
"xdog-agent>=2.
|
|
26
|
-
"xdog-tui>=2.
|
|
24
|
+
"xdog-ai[openai]>=2.2.0",
|
|
25
|
+
"xdog-agent>=2.2.0",
|
|
26
|
+
"xdog-tui>=2.2.0",
|
|
27
27
|
"pyyaml>=6.0",
|
|
28
28
|
"click>=8.1.0",
|
|
29
29
|
"pydantic>=2.0",
|
|
@@ -60,10 +60,16 @@ from xdog.coding.cli.session_picker import pick_session_command
|
|
|
60
60
|
)
|
|
61
61
|
@click.option(
|
|
62
62
|
"--thinking-level",
|
|
63
|
-
type=click.Choice(["
|
|
63
|
+
type=click.Choice(["off", "minimal", "low", "medium", "high", "xhigh"]),
|
|
64
64
|
default=None,
|
|
65
65
|
help="Thinking / reasoning level.",
|
|
66
66
|
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--permission-mode",
|
|
69
|
+
type=click.Choice(["ask", "ask-all", "allow-all", "deny"]),
|
|
70
|
+
default=None,
|
|
71
|
+
help="Tool permission policy.",
|
|
72
|
+
)
|
|
67
73
|
@click.option(
|
|
68
74
|
"--list-models",
|
|
69
75
|
is_flag=True,
|
|
@@ -99,6 +105,7 @@ def cli(
|
|
|
99
105
|
working_dir: Path | None,
|
|
100
106
|
config_path: Path | None,
|
|
101
107
|
thinking_level: str | None,
|
|
108
|
+
permission_mode: str | None,
|
|
102
109
|
list_models: bool,
|
|
103
110
|
pick_session: bool,
|
|
104
111
|
rpc: bool,
|
|
@@ -124,6 +131,8 @@ def cli(
|
|
|
124
131
|
overrides["model"] = model
|
|
125
132
|
if thinking_level is not None:
|
|
126
133
|
overrides["thinking_level"] = thinking_level
|
|
134
|
+
if permission_mode is not None:
|
|
135
|
+
overrides["permission_mode"] = permission_mode
|
|
127
136
|
|
|
128
137
|
# Defer to the main entry-point for actual execution
|
|
129
138
|
from xdog.coding.main import run_agent
|
|
@@ -57,10 +57,19 @@ def config_selector(config_path: Path | None = None) -> GlobalConfig:
|
|
|
57
57
|
cfg = cfg.model_copy(update={"default_model": new_model})
|
|
58
58
|
|
|
59
59
|
# Thinking level
|
|
60
|
-
new_thinking = input(
|
|
60
|
+
new_thinking = input(
|
|
61
|
+
f"Thinking level [{cfg.thinking_level}] (off/minimal/low/medium/high/xhigh): "
|
|
62
|
+
).strip()
|
|
61
63
|
if new_thinking:
|
|
62
64
|
cfg = cfg.model_copy(update={"thinking_level": new_thinking})
|
|
63
65
|
|
|
66
|
+
# Tool permissions
|
|
67
|
+
new_permissions = input(
|
|
68
|
+
f"Permission mode [{cfg.permission_mode}] (ask/ask-all/allow-all/deny): "
|
|
69
|
+
).strip()
|
|
70
|
+
if new_permissions:
|
|
71
|
+
cfg = cfg.model_copy(update={"permission_mode": new_permissions})
|
|
72
|
+
|
|
64
73
|
# Custom instructions
|
|
65
74
|
if cfg.custom_instructions:
|
|
66
75
|
print(f"\nCurrent custom instructions: {cfg.custom_instructions[:80]}...")
|
|
@@ -172,6 +172,7 @@ class GlobalConfig(BaseModel):
|
|
|
172
172
|
|
|
173
173
|
default_model: str = ""
|
|
174
174
|
thinking_level: str = "normal"
|
|
175
|
+
permission_mode: str = "ask"
|
|
175
176
|
extensions: list[str] = Field(default_factory=list)
|
|
176
177
|
allowed_tools: list[str] = Field(default_factory=list)
|
|
177
178
|
custom_instructions: str = ""
|
|
@@ -207,6 +208,7 @@ class ProjectConfig(BaseModel):
|
|
|
207
208
|
|
|
208
209
|
model: str | None = None
|
|
209
210
|
thinking_level: str | None = None
|
|
211
|
+
permission_mode: str | None = None
|
|
210
212
|
allowed_tools: list[str] = Field(default_factory=list)
|
|
211
213
|
custom_instructions: str = ""
|
|
212
214
|
extensions: list[str] = Field(default_factory=list)
|
|
@@ -233,6 +235,7 @@ class RuntimeConfig:
|
|
|
233
235
|
|
|
234
236
|
model: str
|
|
235
237
|
thinking_level: str
|
|
238
|
+
permission_mode: str
|
|
236
239
|
allowed_tools: tuple[str, ...]
|
|
237
240
|
custom_instructions: str
|
|
238
241
|
extensions: tuple[str, ...]
|
|
@@ -253,6 +256,11 @@ class RuntimeConfig:
|
|
|
253
256
|
pcfg = project_cfg or ProjectConfig()
|
|
254
257
|
model = ov.get("model") or pcfg.model or global_cfg.default_model
|
|
255
258
|
thinking = ov.get("thinking_level") or pcfg.thinking_level or global_cfg.thinking_level
|
|
259
|
+
permission_mode = (
|
|
260
|
+
ov.get("permission_mode")
|
|
261
|
+
or pcfg.permission_mode
|
|
262
|
+
or global_cfg.permission_mode
|
|
263
|
+
)
|
|
256
264
|
|
|
257
265
|
merged_tools = list(global_cfg.allowed_tools) + list(pcfg.allowed_tools)
|
|
258
266
|
merged_ext = list(global_cfg.extensions) + list(pcfg.extensions)
|
|
@@ -266,6 +274,7 @@ class RuntimeConfig:
|
|
|
266
274
|
return cls(
|
|
267
275
|
model=model,
|
|
268
276
|
thinking_level=thinking,
|
|
277
|
+
permission_mode=permission_mode,
|
|
269
278
|
allowed_tools=tuple(merged_tools),
|
|
270
279
|
custom_instructions="\n\n".join(instructions_parts),
|
|
271
280
|
extensions=tuple(merged_ext),
|
|
@@ -24,6 +24,7 @@ from xdog.ai.types import (
|
|
|
24
24
|
from xdog.coding.core.defaults import COMPACTION_THRESHOLD_RATIO, MAX_CONTEXT_TOKENS
|
|
25
25
|
from xdog.coding.core.event_bus import EventBus, get_event_bus
|
|
26
26
|
from xdog.coding.core.messages import dicts_to_messages, messages_to_dicts
|
|
27
|
+
from xdog.coding.core.permissions import PermissionManager
|
|
27
28
|
from xdog.coding.core.session_manager import SessionData, SessionManager
|
|
28
29
|
from xdog.coding.core.settings_manager import SettingsManager
|
|
29
30
|
from xdog.coding.core.system_prompt import build_system_prompt
|
|
@@ -47,6 +48,9 @@ class AgentSession:
|
|
|
47
48
|
tool_registry: Any # kept for backward compat, may be None
|
|
48
49
|
bash: Any # kept for backward compat, may be None
|
|
49
50
|
working_dir: Path
|
|
51
|
+
context_window: int = MAX_CONTEXT_TOKENS
|
|
52
|
+
max_prompt_tokens: int = 0
|
|
53
|
+
permissions: PermissionManager = field(default_factory=PermissionManager)
|
|
50
54
|
event_bus: EventBus = field(default_factory=get_event_bus)
|
|
51
55
|
_unsubscribe: Any = field(default=None, init=False, repr=False)
|
|
52
56
|
#: Slugs of skills whose instructions are currently in the system prompt.
|
|
@@ -59,6 +63,9 @@ class AgentSession:
|
|
|
59
63
|
if self.session_data.settings:
|
|
60
64
|
self.settings.load_session_settings(self.session_data.settings)
|
|
61
65
|
|
|
66
|
+
# Sessions constructed outside the SDK still receive enforcement.
|
|
67
|
+
self.agent.set_before_tool_call(self.permissions.before_tool_call)
|
|
68
|
+
|
|
62
69
|
# Subscribe to agent events for persistence and event bus forwarding
|
|
63
70
|
self._unsubscribe = self.agent.subscribe(self._on_agent_event)
|
|
64
71
|
|
|
@@ -80,6 +87,11 @@ class AgentSession:
|
|
|
80
87
|
def is_streaming(self) -> bool:
|
|
81
88
|
return self.agent.state.is_streaming
|
|
82
89
|
|
|
90
|
+
@property
|
|
91
|
+
def context_limit(self) -> int:
|
|
92
|
+
"""Prompt limit used for compaction; falls back to the model window."""
|
|
93
|
+
return self.max_prompt_tokens or self.context_window or MAX_CONTEXT_TOKENS
|
|
94
|
+
|
|
83
95
|
# -- Public API --
|
|
84
96
|
|
|
85
97
|
@property
|
|
@@ -176,17 +188,35 @@ class AgentSession:
|
|
|
176
188
|
"""Cancel the current agent turn."""
|
|
177
189
|
self.agent.abort()
|
|
178
190
|
|
|
191
|
+
def cancel(self) -> None:
|
|
192
|
+
"""Cancel active work and discard agent-level queued messages."""
|
|
193
|
+
self.agent.clear_all_queues()
|
|
194
|
+
self.permissions.deny_all()
|
|
195
|
+
self.agent.abort()
|
|
196
|
+
|
|
179
197
|
# -- Model / thinking --
|
|
180
198
|
|
|
181
199
|
def set_model(self, model: str) -> None:
|
|
182
|
-
"""Switch the active model."""
|
|
200
|
+
"""Switch the active model and refresh its provider-reported limits."""
|
|
183
201
|
self.agent.set_model(model)
|
|
202
|
+
try:
|
|
203
|
+
import xdog.ai as ai
|
|
204
|
+
|
|
205
|
+
model_info = ai.provider("copilot").model(model)
|
|
206
|
+
if model_info is not None:
|
|
207
|
+
self.context_window = model_info.context_window or MAX_CONTEXT_TOKENS
|
|
208
|
+
self.max_prompt_tokens = model_info.max_prompt_tokens
|
|
209
|
+
except Exception:
|
|
210
|
+
logger.debug("could not refresh model limits for %s", model, exc_info=True)
|
|
211
|
+
self.settings.set_session_model(model)
|
|
184
212
|
self._persist()
|
|
185
213
|
|
|
186
214
|
def set_thinking_level(self, level: ThinkingLevel | None) -> None:
|
|
187
|
-
"""Switch the thinking/reasoning level."""
|
|
215
|
+
"""Switch and persist the thinking/reasoning level."""
|
|
188
216
|
from dataclasses import replace
|
|
217
|
+
|
|
189
218
|
self.agent.set_options(replace(self.agent.options, thinking=level))
|
|
219
|
+
self.settings.set_session_thinking(level or "off")
|
|
190
220
|
self._persist()
|
|
191
221
|
|
|
192
222
|
# -- Compaction --
|
|
@@ -222,7 +252,7 @@ class AgentSession:
|
|
|
222
252
|
char_count += 200
|
|
223
253
|
|
|
224
254
|
estimated_tokens = char_count / 4
|
|
225
|
-
threshold =
|
|
255
|
+
threshold = self.context_limit * COMPACTION_THRESHOLD_RATIO
|
|
226
256
|
|
|
227
257
|
if estimated_tokens > threshold:
|
|
228
258
|
await self._run_compaction()
|
|
@@ -257,6 +287,7 @@ class AgentSession:
|
|
|
257
287
|
return False
|
|
258
288
|
|
|
259
289
|
self.session_data = loaded
|
|
290
|
+
self.permissions.clear_session_rules()
|
|
260
291
|
restored_messages = list(self.session_data.messages)
|
|
261
292
|
self.agent.replace_messages(restored_messages)
|
|
262
293
|
|
|
@@ -335,6 +366,7 @@ class AgentSession:
|
|
|
335
366
|
RuntimeConfig(
|
|
336
367
|
model=self.agent.state.model or "unknown",
|
|
337
368
|
thinking_level=str(self.agent.options.thinking or "normal"),
|
|
369
|
+
permission_mode=self.permissions.mode,
|
|
338
370
|
allowed_tools=tuple(t.name for t in self.agent.state.tools),
|
|
339
371
|
custom_instructions=self.settings.custom_instructions,
|
|
340
372
|
extensions=(),
|
|
@@ -372,6 +404,7 @@ class AgentSession:
|
|
|
372
404
|
|
|
373
405
|
def dispose(self) -> None:
|
|
374
406
|
"""Clean up resources."""
|
|
407
|
+
self.permissions.close()
|
|
375
408
|
if self._unsubscribe:
|
|
376
409
|
self._unsubscribe()
|
|
377
410
|
self._unsubscribe = None
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Runtime permission policy and approval broker for coding-agent tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Literal, cast
|
|
13
|
+
|
|
14
|
+
from xdog.agent import BeforeToolCallContext, BeforeToolCallResult
|
|
15
|
+
|
|
16
|
+
PermissionMode = Literal["ask", "ask-all", "allow-all", "deny"]
|
|
17
|
+
PermissionDecision = Literal["allow_once", "allow_session", "deny"]
|
|
18
|
+
PermissionRequestHandler = Callable[["PermissionRequest"], None]
|
|
19
|
+
|
|
20
|
+
_PERMISSION_MODES: tuple[PermissionMode, ...] = ("ask", "ask-all", "allow-all", "deny")
|
|
21
|
+
_ALLOW_DECISIONS = ("allow_once", "allow_session")
|
|
22
|
+
_READ_ONLY_FS_ACTIONS = frozenset({"read", "ls", "grep", "find"})
|
|
23
|
+
_SECRET_KEY = re.compile(r"(?:password|passwd|secret|token|authorization|api[_-]?key|credential)", re.I)
|
|
24
|
+
_MAX_DISPLAY_STRING = 2_000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class PermissionRequest:
|
|
29
|
+
"""A tool call waiting for a decision from a user-facing approver."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
tool_name: str
|
|
33
|
+
arguments: dict[str, Any]
|
|
34
|
+
summary: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(slots=True)
|
|
38
|
+
class _PendingApproval:
|
|
39
|
+
loop: asyncio.AbstractEventLoop
|
|
40
|
+
future: asyncio.Future[PermissionDecision]
|
|
41
|
+
rule_key: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_permission_mode(raw: object) -> PermissionMode:
|
|
45
|
+
"""Return a valid mode, failing closed for malformed configuration."""
|
|
46
|
+
if isinstance(raw, str) and raw in _PERMISSION_MODES:
|
|
47
|
+
return raw
|
|
48
|
+
return "deny"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tool_call_requires_approval(
|
|
52
|
+
mode: PermissionMode,
|
|
53
|
+
tool_name: str,
|
|
54
|
+
arguments: Mapping[str, Any],
|
|
55
|
+
) -> bool:
|
|
56
|
+
"""Classify whether a call needs an external decision under *mode*."""
|
|
57
|
+
if mode == "allow-all":
|
|
58
|
+
return False
|
|
59
|
+
if mode == "ask-all":
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
# `deny` shares the safe read-only allowlist with `ask`; everything else is
|
|
63
|
+
# blocked without prompting.
|
|
64
|
+
if tool_name == "current_time":
|
|
65
|
+
return False
|
|
66
|
+
if tool_name == "filesystem":
|
|
67
|
+
action = arguments.get("action")
|
|
68
|
+
return not isinstance(action, str) or action not in _READ_ONLY_FS_ACTIONS
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _redact(value: Any, *, key: str = "") -> Any:
|
|
73
|
+
if key and _SECRET_KEY.search(key):
|
|
74
|
+
return "<redacted>"
|
|
75
|
+
if isinstance(value, Mapping):
|
|
76
|
+
return {str(k): _redact(v, key=str(k)) for k, v in value.items()}
|
|
77
|
+
if isinstance(value, (list, tuple)):
|
|
78
|
+
return [_redact(item) for item in value]
|
|
79
|
+
if isinstance(value, str) and len(value) > _MAX_DISPLAY_STRING:
|
|
80
|
+
omitted = len(value) - _MAX_DISPLAY_STRING
|
|
81
|
+
return f"{value[:_MAX_DISPLAY_STRING]}… <{omitted} chars omitted>"
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _request_arguments(tool_name: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
|
86
|
+
"""Return redacted, bounded arguments suitable for terminals and RPC."""
|
|
87
|
+
displayed = cast(dict[str, Any], _redact(dict(arguments)))
|
|
88
|
+
if tool_name == "filesystem":
|
|
89
|
+
content = arguments.get("content")
|
|
90
|
+
if isinstance(content, str):
|
|
91
|
+
displayed["content"] = f"<{len(content)} characters>"
|
|
92
|
+
return displayed
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _summary(tool_name: str, arguments: Mapping[str, Any]) -> str:
|
|
96
|
+
if tool_name == "bash":
|
|
97
|
+
command = str(arguments.get("command", "")).strip()
|
|
98
|
+
if len(command) > 500:
|
|
99
|
+
command = command[:500] + "…"
|
|
100
|
+
return f"Run shell command:\n{command}" if command else "Run a shell command"
|
|
101
|
+
if tool_name == "filesystem":
|
|
102
|
+
action = str(arguments.get("action", "unknown"))
|
|
103
|
+
path = str(arguments.get("path", ""))
|
|
104
|
+
if action == "write":
|
|
105
|
+
size = len(str(arguments.get("content", "")))
|
|
106
|
+
return f"Write {size} characters to {path or '(unknown path)'}"
|
|
107
|
+
if action == "edit":
|
|
108
|
+
return f"Edit {path or '(unknown path)'}"
|
|
109
|
+
if action == "delete":
|
|
110
|
+
return f"Delete {path or '(unknown path)'}"
|
|
111
|
+
return f"Filesystem {action}: {path}".rstrip()
|
|
112
|
+
try:
|
|
113
|
+
rendered = json.dumps(_redact(dict(arguments)), ensure_ascii=False, sort_keys=True)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
rendered = repr(arguments)
|
|
116
|
+
return f"Run {tool_name}: {rendered}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _rule_key(tool_name: str, arguments: Mapping[str, Any]) -> str:
|
|
120
|
+
"""Build a conservative exact-call session rule key."""
|
|
121
|
+
try:
|
|
122
|
+
encoded = json.dumps(dict(arguments), ensure_ascii=False, sort_keys=True, default=repr)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
encoded = repr(sorted((str(k), repr(v)) for k, v in arguments.items()))
|
|
125
|
+
return f"{tool_name}\0{encoded}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PermissionManager:
|
|
129
|
+
"""Enforce tool policy and bridge async tool calls to a UI approver.
|
|
130
|
+
|
|
131
|
+
The hook runs in the agent's asyncio loop, which is a background thread in
|
|
132
|
+
interactive mode. Request handlers may run on that thread, but decisions may
|
|
133
|
+
be submitted from any thread; :meth:`resolve` schedules completion on the
|
|
134
|
+
future's owning event loop.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, mode: object = "ask") -> None:
|
|
138
|
+
self._mode = normalize_permission_mode(mode)
|
|
139
|
+
self._handler: PermissionRequestHandler | None = None
|
|
140
|
+
self._pending: dict[str, _PendingApproval] = {}
|
|
141
|
+
self._session_rules: set[str] = set()
|
|
142
|
+
self._lock = threading.RLock()
|
|
143
|
+
self._closed = False
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def mode(self) -> PermissionMode:
|
|
147
|
+
return self._mode
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def has_approver(self) -> bool:
|
|
151
|
+
with self._lock:
|
|
152
|
+
return self._handler is not None and not self._closed
|
|
153
|
+
|
|
154
|
+
def set_mode(self, mode: object) -> None:
|
|
155
|
+
self._mode = normalize_permission_mode(mode)
|
|
156
|
+
|
|
157
|
+
def set_request_handler(self, handler: PermissionRequestHandler | None) -> None:
|
|
158
|
+
with self._lock:
|
|
159
|
+
self._handler = handler
|
|
160
|
+
|
|
161
|
+
def clear_session_rules(self) -> None:
|
|
162
|
+
with self._lock:
|
|
163
|
+
self._session_rules.clear()
|
|
164
|
+
|
|
165
|
+
async def before_tool_call(
|
|
166
|
+
self,
|
|
167
|
+
ctx: BeforeToolCallContext,
|
|
168
|
+
cancel: asyncio.Event | None = None,
|
|
169
|
+
) -> BeforeToolCallResult | None:
|
|
170
|
+
"""Agent hook that permits, waits for approval, or blocks a tool call."""
|
|
171
|
+
args = dict(ctx.args) if isinstance(ctx.args, Mapping) else {}
|
|
172
|
+
tool_name = ctx.tool_call.name
|
|
173
|
+
|
|
174
|
+
if not tool_call_requires_approval(self._mode, tool_name, args):
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
rule_key = _rule_key(tool_name, args)
|
|
178
|
+
with self._lock:
|
|
179
|
+
if rule_key in self._session_rules:
|
|
180
|
+
return None
|
|
181
|
+
handler = self._handler
|
|
182
|
+
closed = self._closed
|
|
183
|
+
|
|
184
|
+
if self._mode == "deny":
|
|
185
|
+
return BeforeToolCallResult(
|
|
186
|
+
block=True,
|
|
187
|
+
reason="Tool call denied by permission policy",
|
|
188
|
+
)
|
|
189
|
+
if closed or handler is None:
|
|
190
|
+
return BeforeToolCallResult(
|
|
191
|
+
block=True,
|
|
192
|
+
reason="Tool call requires approval, but no permission approver is available",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
request = PermissionRequest(
|
|
196
|
+
id=uuid.uuid4().hex,
|
|
197
|
+
tool_name=tool_name,
|
|
198
|
+
arguments=_request_arguments(tool_name, args),
|
|
199
|
+
summary=_summary(tool_name, args),
|
|
200
|
+
)
|
|
201
|
+
loop = asyncio.get_running_loop()
|
|
202
|
+
future: asyncio.Future[PermissionDecision] = loop.create_future()
|
|
203
|
+
pending = _PendingApproval(loop=loop, future=future, rule_key=rule_key)
|
|
204
|
+
with self._lock:
|
|
205
|
+
if self._closed:
|
|
206
|
+
return BeforeToolCallResult(block=True, reason="Permission manager is closed")
|
|
207
|
+
self._pending[request.id] = pending
|
|
208
|
+
|
|
209
|
+
cancel_task: asyncio.Task[PermissionDecision] | None = None
|
|
210
|
+
try:
|
|
211
|
+
handler(request)
|
|
212
|
+
if cancel is not None:
|
|
213
|
+
async def _wait_for_cancel() -> PermissionDecision:
|
|
214
|
+
await cancel.wait()
|
|
215
|
+
return "deny"
|
|
216
|
+
|
|
217
|
+
cancel_task = asyncio.create_task(_wait_for_cancel())
|
|
218
|
+
waiters: set[asyncio.Future[PermissionDecision]] = {future, cancel_task}
|
|
219
|
+
done, _ = await asyncio.wait(
|
|
220
|
+
waiters,
|
|
221
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
222
|
+
)
|
|
223
|
+
if cancel_task in done and future not in done:
|
|
224
|
+
return BeforeToolCallResult(
|
|
225
|
+
block=True,
|
|
226
|
+
reason="Tool approval was cancelled",
|
|
227
|
+
)
|
|
228
|
+
decision = await future
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
return BeforeToolCallResult(
|
|
231
|
+
block=True,
|
|
232
|
+
reason=f"Could not obtain tool approval: {exc}",
|
|
233
|
+
)
|
|
234
|
+
finally:
|
|
235
|
+
if cancel_task is not None and not cancel_task.done():
|
|
236
|
+
cancel_task.cancel()
|
|
237
|
+
with self._lock:
|
|
238
|
+
self._pending.pop(request.id, None)
|
|
239
|
+
|
|
240
|
+
if decision == "allow_session":
|
|
241
|
+
with self._lock:
|
|
242
|
+
self._session_rules.add(rule_key)
|
|
243
|
+
if decision in _ALLOW_DECISIONS:
|
|
244
|
+
return None
|
|
245
|
+
return BeforeToolCallResult(block=True, reason="User denied this tool call")
|
|
246
|
+
|
|
247
|
+
def resolve(self, request_id: str, decision: PermissionDecision) -> bool:
|
|
248
|
+
"""Resolve a pending request from any thread. Return False if unknown."""
|
|
249
|
+
if decision not in ("allow_once", "allow_session", "deny"):
|
|
250
|
+
return False
|
|
251
|
+
with self._lock:
|
|
252
|
+
pending = self._pending.get(request_id)
|
|
253
|
+
if pending is None:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _set_result() -> None:
|
|
257
|
+
if not pending.future.done():
|
|
258
|
+
pending.future.set_result(decision)
|
|
259
|
+
|
|
260
|
+
pending.loop.call_soon_threadsafe(_set_result)
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
def deny_all(self) -> None:
|
|
264
|
+
"""Deny every pending request without closing the manager."""
|
|
265
|
+
with self._lock:
|
|
266
|
+
request_ids = list(self._pending)
|
|
267
|
+
for request_id in request_ids:
|
|
268
|
+
self.resolve(request_id, "deny")
|
|
269
|
+
|
|
270
|
+
def close(self) -> None:
|
|
271
|
+
"""Fail all pending requests and reject future interactive approvals."""
|
|
272
|
+
with self._lock:
|
|
273
|
+
self._closed = True
|
|
274
|
+
self._handler = None
|
|
275
|
+
self.deny_all()
|