@qilin-agent/user-approval 0.0.0-stage → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +88 -0
- package/README.md +165 -2
- package/README.zh.md +166 -0
- package/lib/index.js +192 -0
- package/lib/types/index.d.ts +159 -0
- package/lib/types/index.js +209 -0
- package/lib/types/types.d.ts +84 -0
- package/lib/types/types.js +15 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/interaction/user-approval/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 515e991a5671711d
|
|
7
|
+
zh: 194d48a8da9b51b0
|
|
8
|
+
/qilin-user-approval:
|
|
9
|
+
en: 5159b08d092efa80
|
|
10
|
+
zh: 2caf27c411229846
|
|
11
|
+
/qilin-user-approval/summary:
|
|
12
|
+
en: 9b438f002e664c9e
|
|
13
|
+
zh: 1f1adf3db6a56ddc
|
|
14
|
+
/qilin-user-approval/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/qilin-user-approval/use-this-package:
|
|
18
|
+
en: dd034141bc1a7634
|
|
19
|
+
zh: a44f1f90dac2a444
|
|
20
|
+
/qilin-user-approval/use-this-package/composing-answerers:
|
|
21
|
+
en: 3618ddc3386e65f0
|
|
22
|
+
zh: d6c0ac91aae122bb
|
|
23
|
+
/qilin-user-approval/use-this-package/setting-the-policy:
|
|
24
|
+
en: b39f26cb8715c56e
|
|
25
|
+
zh: a26951daa09fc57f
|
|
26
|
+
/qilin-user-approval/use-this-package/requesting-a-decision:
|
|
27
|
+
en: add08f0d0dace4c8
|
|
28
|
+
zh: e45268e0f0ed2e95
|
|
29
|
+
/qilin-user-approval/use-this-package/what-the-model-and-user-see:
|
|
30
|
+
en: 016bf3cd1891d3d7
|
|
31
|
+
zh: 33e7e5ffaf95f5e7
|
|
32
|
+
/qilin-user-approval/understand-the-implementation:
|
|
33
|
+
en: d07be7ba08baf010
|
|
34
|
+
zh: 665e68e9188e5d60
|
|
35
|
+
/qilin-user-approval/understand-the-implementation/source-map:
|
|
36
|
+
en: ac27642b3a91082f
|
|
37
|
+
zh: 769a56eaad4218f7
|
|
38
|
+
/qilin-user-approval/understand-the-implementation/dispatch:
|
|
39
|
+
en: 38c3646e45d3dddd
|
|
40
|
+
zh: 3363dde8c786d6b2
|
|
41
|
+
/qilin-user-approval/understand-the-implementation/policy-and-the-runtime-context-snapshot:
|
|
42
|
+
en: a8df8823aa0b015d
|
|
43
|
+
zh: ed0906f11645e7fd
|
|
44
|
+
/qilin-user-approval/understand-the-implementation/audit:
|
|
45
|
+
en: 145530678bac5912
|
|
46
|
+
zh: a464effa3a29fce0
|
|
47
|
+
/qilin-user-approval/further-exploration:
|
|
48
|
+
en: f605b73345732588
|
|
49
|
+
zh: ac9a76e77dbed4a7
|
|
50
|
+
/qilin-user-approval/model-experience:
|
|
51
|
+
en: 215e7ba838619b7b
|
|
52
|
+
zh: a311da3843709f90
|
|
53
|
+
/qilin-user-approval/model-experience/current-approval-policy-context:
|
|
54
|
+
en: d23980f003b42fbb
|
|
55
|
+
zh: 532f0174de5d87b5
|
|
56
|
+
/qilin-user-approval/model-experience/current-approval-policy-context/what-the-model-sees:
|
|
57
|
+
en: a38775858ccd61f3
|
|
58
|
+
zh: c19c76827be7aabf
|
|
59
|
+
/qilin-user-approval/model-experience/current-approval-policy-context/what-the-model-sees/ask-policy-contribution:
|
|
60
|
+
en: b9f1262c97ec7568
|
|
61
|
+
zh: 8a6613695f896f70
|
|
62
|
+
/qilin-user-approval/model-experience/current-approval-policy-context/what-the-model-sees/never-policy-contribution:
|
|
63
|
+
en: f13f09fc2466513b
|
|
64
|
+
zh: 4a755a682551c481
|
|
65
|
+
/qilin-user-approval/model-experience/current-approval-policy-context/token-effect:
|
|
66
|
+
en: e96807422bdce44d
|
|
67
|
+
zh: 9fdcfafcc96bd004
|
|
68
|
+
/qilin-user-approval/model-experience/current-approval-policy-context/kv-cache-effect:
|
|
69
|
+
en: b4bac06588fdd1c3
|
|
70
|
+
zh: 60271562fdd52f72
|
|
71
|
+
/qilin-user-approval/model-experience/tool-outcome:
|
|
72
|
+
en: 54c140b4a1b4a5bc
|
|
73
|
+
zh: 8573891441963be4
|
|
74
|
+
/qilin-user-approval/model-experience/tool-outcome/what-the-model-sees:
|
|
75
|
+
en: fd0c40c0a6b4c967
|
|
76
|
+
zh: c7e18063e11080e9
|
|
77
|
+
/qilin-user-approval/model-experience/tool-outcome/token-effect:
|
|
78
|
+
en: 972a63ca590fecde
|
|
79
|
+
zh: 217b63d98365dbdb
|
|
80
|
+
/qilin-user-approval/model-experience/tool-outcome/kv-cache-effect:
|
|
81
|
+
en: f5d5f97a153f7c70
|
|
82
|
+
zh: 4db81881ebdcfcc4
|
|
83
|
+
/qilin-user-approval/known-limitations-and-deferred-work:
|
|
84
|
+
en: c6928515ff91ee7e
|
|
85
|
+
zh: c23bd844b4907f12
|
|
86
|
+
/qilin-user-approval/known-limitations-and-deferred-work/dev-note:
|
|
87
|
+
en: e67f633dc6fe7773
|
|
88
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Channel-neutral one-shot approval seam for users and maintainers composing answerers, setting policy, or debugging fail-closed permission decisions."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/user-approval
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Use this package to require a one-shot decision before a sensitive tool action proceeds. The `ask` policy sends each request to the deployment's human or machine answerers; `never` rejects it without prompting. Missing or failed answerers return `unavailable`, so the action fails closed, and an approval applies only to that request. Every request and outcome is recorded in the requesting session's audit log. The model sees the resulting tool outcome and current policy, but not the human permission UI or audit events.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Compose this service when sensitive tool actions should pause for a human or machine decision instead of running unconditionally. The tools pipeline and the sandboxed bash tool route their `ask` decisions through this seam and fail closed when it is absent, so interactive deployments mount it with at least one answerer.
|
|
29
|
+
|
|
30
|
+
### Composing answerers
|
|
31
|
+
|
|
32
|
+
Answerers are `approval/request` waterfall listeners: return an outcome to answer for an owned agent, or call `next()` to delegate. Agent-scoped listeners receive only that agent's requests, and a deployment composes one terminal answerer — sibling listener order is not a policy-priority mechanism. Without a terminal answerer, requests resolve `unavailable` and fail closed; the service itself never prompts a human.
|
|
33
|
+
|
|
34
|
+
### Setting the policy
|
|
35
|
+
|
|
36
|
+
The effective policy is the one set for the session, falling back to the configured default. `ask` (the default) delegates to the composed answerers; `never` rejects every request deterministically before interactive dispatch — the strict headless stance for CI and unattended runs.
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@qilin-agent/user-approval'
|
|
40
|
+
config:
|
|
41
|
+
policy: ask
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Field | Default | Meaning |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `policy` | `ask` | Default for sessions without an `approval/policy` override |
|
|
47
|
+
|
|
48
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#qilin-agentuser-approval) is the exhaustive source for every accepted field and its JSDoc. `setPolicy(agent, policy)` switches a live agent and queues a "changed by the user" message for its next model step; `setApprovalPolicy(session, policy)` is the direct durable write path used by session initialization.
|
|
49
|
+
|
|
50
|
+
### Requesting a decision
|
|
51
|
+
|
|
52
|
+
`request(req)` names the agent, tool, optional call id and reason, and an abort signal. It requires an open turn: an idle or between-turn caller throws before auditing anything. Aborting withdraws the question — the request settles `cancelled` and a late answer is discarded. A failure that prevents either audit append from committing rejects instead of returning an unlogged decision.
|
|
53
|
+
|
|
54
|
+
### What the model and user see
|
|
55
|
+
|
|
56
|
+
The model sees only the asking consumer's eventual tool outcome — allowed, rejected, cancelled, or unavailable — plus the current policy in the runtime-context snapshot; the audit events and the human permission UI are not model context. A `never` switch is announced to the model by a sourced user message, and both policies contribute their complete current meaning to the snapshot.
|
|
57
|
+
|
|
58
|
+
-----
|
|
59
|
+
|
|
60
|
+
<a id="understand-the-implementation"></a>
|
|
61
|
+
## Understand the implementation
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary>Implementation internals — click to expand</summary>
|
|
65
|
+
|
|
66
|
+
The observable behavior is covered in [Use this package](#use-this-package); this section explains dispatch, policy enforcement, and the audit path.
|
|
67
|
+
|
|
68
|
+
### Source map
|
|
69
|
+
|
|
70
|
+
| File | Role |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [`src/index.ts`](src/index.ts) | `ApprovalService`: request dispatch, policy fold and write path, runtime-context contribution |
|
|
73
|
+
| [`src/types.ts`](src/types.ts) | `ApprovalRequestId` brand and outcome types |
|
|
74
|
+
|
|
75
|
+
### Dispatch
|
|
76
|
+
|
|
77
|
+
`decide()` races the answerer waterfall against the request signal and contains every answerer failure: a throwing listener fails the question closed to `unavailable`, and a rogue non-vocabulary return is normalized to `unavailable`. The `never` policy is enforced inside the service before waterfall dispatch, so a listener registered later with `prepend` cannot bypass the deterministic rejection. The request must be turn-enclosed because the turn is the durable log's commit/replay boundary — a bare event between turns is indistinguishable from a crash tail.
|
|
78
|
+
|
|
79
|
+
### Policy and the runtime-context snapshot
|
|
80
|
+
|
|
81
|
+
The system-prompt contribution `approval:policy` states the complete current meaning of the effective policy — `ask` with its fail-closed consequence, or `never` with its non-escalation consequence — after retained history, so switching policy appends a new full snapshot instead of rewriting the stable request header. `setPolicy()` also injects a sourced user message announcing the change for the next step.
|
|
82
|
+
|
|
83
|
+
### Audit
|
|
84
|
+
|
|
85
|
+
`request()` appends `approval/asked` with the request identity and tool, then `approval/decided` with the closed outcome; the exact appended fields live in [`src/index.ts`](src/index.ts). Both are log-only.
|
|
86
|
+
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
-----
|
|
90
|
+
|
|
91
|
+
<a id="further-exploration"></a>
|
|
92
|
+
## Further Exploration
|
|
93
|
+
|
|
94
|
+
Read these pages when the package-level contract is not enough. They move from the approval vocabulary to the consumers and the design rationale.
|
|
95
|
+
|
|
96
|
+
- [Approval subsystem reference](../../../docs/subsystems/approval.md) — the shared request/outcome vocabulary and the `ctx.approval` Cordis surface.
|
|
97
|
+
- [Approval seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.md) — design rationale for the seam.
|
|
98
|
+
- [Sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) — how the sandboxed bash tool consumes approvals for escalated retries.
|
|
99
|
+
- [Interaction group map](../README.md) — adjacent permission preset and question packages.
|
|
100
|
+
|
|
101
|
+
-----
|
|
102
|
+
|
|
103
|
+
<a id="model-experience"></a>
|
|
104
|
+
## Model Experience
|
|
105
|
+
|
|
106
|
+
### Current approval policy context
|
|
107
|
+
|
|
108
|
+
#### What the model sees
|
|
109
|
+
|
|
110
|
+
The first request and each effective policy change append a full runtime-context snapshot after retained history. Under `ask`, the approval contribution states that configured answerers may be consulted and absence fails closed. Under `never`, it states the deterministic rejection and non-escalation consequence. Unchanged requests retain the earlier snapshot without adding another message.
|
|
111
|
+
|
|
112
|
+
##### Ask-policy contribution
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
##### Never-policy contribution
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Token effect
|
|
125
|
+
|
|
126
|
+
One concise context message on the first request and on an effective change; unchanged requests add no duplicate policy tokens.
|
|
127
|
+
|
|
128
|
+
#### KV Cache effect
|
|
129
|
+
|
|
130
|
+
Append-only after retained history. An `ask`/`never` switch preserves the stable system and conversation prefix instead of rewriting the first wire message.
|
|
131
|
+
|
|
132
|
+
### Tool outcome
|
|
133
|
+
|
|
134
|
+
#### What the model sees
|
|
135
|
+
|
|
136
|
+
`approval/asked` and `approval/decided` are log-only. The model sees only the asking consumer's eventual allowed, rejected, cancelled, or unavailable tool outcome; the human permission UI is not context.
|
|
137
|
+
|
|
138
|
+
#### Token effect
|
|
139
|
+
|
|
140
|
+
Zero duplicate audit tokens. A rejection may replace a normal tool result with a small retained error, while an allowance leaves the consumer's ordinary result.
|
|
141
|
+
|
|
142
|
+
#### KV Cache effect
|
|
143
|
+
|
|
144
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
145
|
+
|
|
146
|
+
## Known Limitations and Deferred Work
|
|
147
|
+
|
|
148
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
These limits define when the seam is a poor fit or needs special composition care. They are current package constraints, not a general permission comparison.
|
|
152
|
+
|
|
153
|
+
- **Requests are valid only inside an open turn** — an idle or between-turn caller throws before auditing; a durable out-of-turn approval workflow is deferred.
|
|
154
|
+
- **Only one-shot grants exist** — the outcome vocabulary has `allowed-once` but no `allow-always`, remembered rule, revocation, or grant store; session policy is only `ask` / `never`.
|
|
155
|
+
- **The request carries no tool arguments** — an answerer sees the tool name, reason, and optional call id; the ACP machine channel requires a call id and delegates requests without one.
|
|
156
|
+
- **No built-in answerer** — headless or incompletely composed deployments resolve `unavailable` and fail closed; the service itself never prompts a human.
|
|
157
|
+
|
|
158
|
+
<a id="dev-note"></a>
|
|
159
|
+
### Dev Note
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
163
|
+
|
|
164
|
+
None.
|
|
165
|
+
|
|
166
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "与通道无关的一次性审批 seam;供组合应答者、设置策略或排查以拒绝方式关闭的权限决定的用户与维护者阅读。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/user-approval
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
使用本包可要求敏感工具操作在继续前取得一次性决定。`ask` 策略将每个请求发送给部署中的人类或机器应答者;`never` 则直接拒绝,不发出提示。应答者缺失或失败时返回 `unavailable`,使操作以拒绝方式关闭;每项批准也只适用于对应请求。每个请求与结果都会记录在发起请求的会话审计日志中。模型会看到最终工具结果与当前策略,但不会看到人类权限 UI 或审计事件。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
当敏感工具操作应当暂停等待人或机器的决定、而非无条件执行时,组合此服务。工具流水线与沙箱 bash 工具会通过此 seam 路由 `ask` 决定,并在该 seam 缺失时以拒绝方式关闭,因此交互式部署至少应组合一个应答者。
|
|
29
|
+
|
|
30
|
+
### 组合应答者
|
|
31
|
+
|
|
32
|
+
应答者是 `approval/request` waterfall(瀑布式事件)监听器:返回一个结果即为所负责的 agent(智能体)作答,否则调用 `next()` 委托。限定到 agent 的监听器只接收该 agent 的请求,且每项部署应组合一个最终应答者——同级监听器的顺序不是策略优先级机制。没有最终应答者时,请求解析为 `unavailable` 并以拒绝方式关闭;服务自身绝不会提示人类。
|
|
33
|
+
|
|
34
|
+
### 设置策略
|
|
35
|
+
|
|
36
|
+
有效策略取会话中已设置的策略,并回退到配置的默认值。`ask`(默认)委托给已组合的应答者;`never` 在交互式分发之前确定性地拒绝每个请求——这是 CI 与无人值守运行采用的严格无头模式。
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@qilin-agent/user-approval'
|
|
40
|
+
config:
|
|
41
|
+
policy: ask
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| 字段 | 默认值 | 含义 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `policy` | `ask` | 没有 `approval/policy` 覆盖的会话的默认策略 |
|
|
47
|
+
|
|
48
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#qilin-agentuser-approval)是每个受支持字段及其 JSDoc 的穷尽式真源。`setPolicy(agent, policy)` 切换运行中的 agent 的策略,并为它的下一个模型步骤排队一条「由用户更改」消息;`setApprovalPolicy(session, policy)` 是会话初始化使用的直接持久写入路径。
|
|
49
|
+
|
|
50
|
+
### 请求决定
|
|
51
|
+
|
|
52
|
+
`request(req)` 指名 agent、工具、原因、可选的调用 id,以及一个中止信号。它要求当前处于尚未结束的轮次中:空闲或在轮次之间调用会在审计前抛出异常。中止会撤回问题——请求以 `cancelled` 结算,迟到的回答被丢弃。若任一审计事件在提交前失败,请求会被拒绝,而不会返回一项未记录的决定。
|
|
53
|
+
|
|
54
|
+
### 模型与用户看到什么
|
|
55
|
+
|
|
56
|
+
模型只会看到发起请求的消费方最终给出的工具结果——允许、拒绝、取消或不可用——以及运行时上下文快照中的当前策略;审计事件与面向人类的权限 UI 不属于模型上下文。`never` 切换会以一条带来源的用户消息告知模型,两种策略都会把各自的完整当前含义贡献给快照。
|
|
57
|
+
|
|
58
|
+
-----
|
|
59
|
+
|
|
60
|
+
<a id="understand-the-implementation"></a>
|
|
61
|
+
## 理解实现
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary>实现细节——点击展开</summary>
|
|
65
|
+
|
|
66
|
+
可观察行为已在[使用本包](#use-this-package)中说明;本节解释分发、策略执行与审计路径。
|
|
67
|
+
|
|
68
|
+
### 源码地图
|
|
69
|
+
|
|
70
|
+
| 文件 | 职责 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [`src/index.ts`](src/index.ts) | `ApprovalService`:请求分发、策略折叠与写入路径、运行时上下文贡献 |
|
|
73
|
+
| [`src/types.ts`](src/types.ts) | `ApprovalRequestId` brand 与结果类型 |
|
|
74
|
+
|
|
75
|
+
### 分发
|
|
76
|
+
|
|
77
|
+
`decide()` 让应答者 waterfall 与请求信号赛跑,并隔离所有应答者故障:抛出异常的监听器会使问题以 `unavailable` 关闭,不属于结果词汇的异常返回值也会规范化为 `unavailable`。`never` 策略在服务内部、waterfall 分发之前执行,因此之后以 `prepend` 注册的监听器也无法绕过确定性的拒绝。请求必须处于未结束的轮次内,因为轮次是持久日志的提交/回放边界——轮次之间的裸事件与崩溃尾部无法区分。
|
|
78
|
+
|
|
79
|
+
### 策略与运行时上下文快照
|
|
80
|
+
|
|
81
|
+
系统提示词贡献 `approval:policy` 在保留历史之后陈述有效策略的完整当前含义——`ask` 及其以拒绝方式关闭的后果,或 `never` 及其非升权后果——因此切换策略会追加一份新的完整快照,而不会改写稳定的请求头。`setPolicy()` 还会注入一条带来源的用户消息,为下一步宣布变更。
|
|
82
|
+
|
|
83
|
+
### 审计
|
|
84
|
+
|
|
85
|
+
`request()` 先追加携带请求身份与工具的 `approval/asked`,再追加携带最终结果的 `approval/decided`;确切追加字段见 [`src/index.ts`](src/index.ts)。两者都只写入日志。
|
|
86
|
+
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
-----
|
|
90
|
+
|
|
91
|
+
<a id="further-exploration"></a>
|
|
92
|
+
## 进一步探索
|
|
93
|
+
|
|
94
|
+
当包级约定不够用时阅读以下页面。它们从审批词汇逐步进入消费方与设计依据。
|
|
95
|
+
|
|
96
|
+
- [审批子系统参考](../../../docs/subsystems/approval.zh.md)——共享的请求/结果词汇与 `ctx.approval` 的 Cordis 接口面。
|
|
97
|
+
- [审批 seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md)——该 seam 的设计依据。
|
|
98
|
+
- [沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)——沙箱 bash 工具如何为升权重试消费审批。
|
|
99
|
+
- [交互组映射](../README.zh.md)——相邻的权限预设与问答包。
|
|
100
|
+
|
|
101
|
+
-----
|
|
102
|
+
|
|
103
|
+
<a id="model-experience"></a>
|
|
104
|
+
## 模型体验
|
|
105
|
+
|
|
106
|
+
### 当前审批策略上下文
|
|
107
|
+
|
|
108
|
+
#### 模型看到的内容
|
|
109
|
+
|
|
110
|
+
首次请求与有效策略每次变化时,都会在保留的历史后追加一份完整运行时上下文快照。在 `ask` 下,审批上下文内容会说明系统可以咨询已配置的应答者,缺少可用应答者时则以拒绝方式关闭。在 `never` 下,它会说明确定性的拒绝与非升权后果。未变化的请求会保留先前快照,不增加另一条消息。
|
|
111
|
+
|
|
112
|
+
##### Ask 策略贡献
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
##### Never 策略贡献
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Token 影响
|
|
125
|
+
|
|
126
|
+
首次请求和策略实际变化时增加一条简洁的上下文消息;未变化的请求不增加重复的策略 token。
|
|
127
|
+
|
|
128
|
+
#### KV Cache 影响
|
|
129
|
+
|
|
130
|
+
在保留的历史之后仅追加。`ask`/`never` 切换会保留稳定的系统与对话前缀,而不会改写第一条 wire 消息。
|
|
131
|
+
|
|
132
|
+
### 工具结果
|
|
133
|
+
|
|
134
|
+
#### 模型看到的内容
|
|
135
|
+
|
|
136
|
+
`approval/asked` 和 `approval/decided` 只写入日志。模型只会看到发起请求的消费方最终给出的允许、拒绝、取消或不可用工具结果;面向人类的权限 UI 不属于上下文。
|
|
137
|
+
|
|
138
|
+
#### Token 影响
|
|
139
|
+
|
|
140
|
+
不会产生重复的审计 token。拒绝可能以一条简短且会保留的错误信息替换正常工具结果,而允许会保留消费方的普通结果。
|
|
141
|
+
|
|
142
|
+
#### KV Cache 影响
|
|
143
|
+
|
|
144
|
+
仅追加;新出现的可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
145
|
+
|
|
146
|
+
## 已知限制与延期工作
|
|
147
|
+
|
|
148
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
这些限制说明该 seam 不适用的场景,以及组合时需要特别注意的场景。它们是当前包约束,不是通用权限对比。
|
|
152
|
+
|
|
153
|
+
- **请求只在尚未结束的轮次内有效**:在空闲时或轮次之间发起调用,会在审计前抛出异常;持久化的轮次外审批工作流仍属延期工作。
|
|
154
|
+
- **仅存在一次性授权**:结果词汇包含 `allowed-once`,但不含 `allow-always`、已记住的规则、撤销或授权存储;会话策略只有 `ask`/`never`。
|
|
155
|
+
- **请求不携带工具参数**:应答者会看到工具名称、原因和可选调用 id;ACP(Agent Client Protocol)机器通道要求调用 id,并会委托不含 id 的请求。
|
|
156
|
+
- **没有内置应答者**:无头或组合不完整的部署会返回 `unavailable` 并以拒绝方式关闭;服务自身绝不会提示人类。
|
|
157
|
+
|
|
158
|
+
<a id="dev-note"></a>
|
|
159
|
+
### 开发备注
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
163
|
+
|
|
164
|
+
无。
|
|
165
|
+
|
|
166
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Service } from "@qilin-agent/kylin";
|
|
3
|
+
import z from "@qilin-agent/schemastery";
|
|
4
|
+
import { createUserMessage } from "@qilin-agent/llm";
|
|
5
|
+
import { scopeTarget } from "@qilin-agent/scope";
|
|
6
|
+
import { SessionSeq } from "@qilin-agent/session";
|
|
7
|
+
//#region lib/types/types.js
|
|
8
|
+
/**
|
|
9
|
+
* Wire-safe approval identifiers and outcome vocabulary, free of
|
|
10
|
+
* cordis/service imports so browser type chains can
|
|
11
|
+
* consume them without loading this package's Context augmentation.
|
|
12
|
+
* @module @qilin-agent/user-approval/types
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Brand a string as an {@link ApprovalRequestId}.
|
|
16
|
+
* @param id - the raw id string to brand.
|
|
17
|
+
* @returns the same string carrying the brand.
|
|
18
|
+
*/
|
|
19
|
+
function ApprovalRequestId(id) {
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region lib/types/index.js
|
|
24
|
+
/**
|
|
25
|
+
* Service Definition for the approval capability seam, covering requests, cancellation, audit, and per-session policy. Missing
|
|
26
|
+
* answerers fail closed; grants apply only to the requested action.
|
|
27
|
+
* @module @qilin-agent/user-approval
|
|
28
|
+
*/
|
|
29
|
+
/** Every {@link ApprovalOutcome}, for runtime normalization of answerer returns. */
|
|
30
|
+
const OUTCOMES = [
|
|
31
|
+
"allowed-once",
|
|
32
|
+
"rejected",
|
|
33
|
+
"cancelled",
|
|
34
|
+
"unavailable"
|
|
35
|
+
];
|
|
36
|
+
/** Every {@link ApprovalPolicy}, for option advertisement and runtime validation of untrusted policy strings. */
|
|
37
|
+
const APPROVAL_POLICIES = ["ask", "never"];
|
|
38
|
+
/** Model-facing statement for the deterministic `'never'` policy. */
|
|
39
|
+
const NEVER_SENTENCE = "Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).";
|
|
40
|
+
/** Model-facing statement for an interactive policy that may still fail closed. */
|
|
41
|
+
const ASK_SENTENCE = "Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.";
|
|
42
|
+
/**
|
|
43
|
+
* Whether the log currently sits inside an open turn (a `turn/start` not yet
|
|
44
|
+
* closed by a `turn/end`) — the {@link ApprovalService.request} precondition.
|
|
45
|
+
* The audit pair must be turn-enclosed: the turn is the durable log's
|
|
46
|
+
* commit/replay boundary, so a bare event appended between turns is
|
|
47
|
+
* indistinguishable from a crash tail and silently dropped on reload.
|
|
48
|
+
*/
|
|
49
|
+
function hasOpenTurn(session) {
|
|
50
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
51
|
+
const type = session.eventAt(SessionSeq(seq))?.type;
|
|
52
|
+
if (type === "turn/start") return true;
|
|
53
|
+
if (type === "turn/end") return false;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Append the sole durable representation of a session policy override. Invalid
|
|
59
|
+
* values throw before the log changes; consumers fold the new value on each read.
|
|
60
|
+
* @param session - the session the override belongs to.
|
|
61
|
+
* @param policy - the policy in effect until the next switch.
|
|
62
|
+
*/
|
|
63
|
+
function setApprovalPolicy(session, policy) {
|
|
64
|
+
if (!APPROVAL_POLICIES.includes(policy)) throw new TypeError("approval policy must be one of \"ask\" or \"never\"");
|
|
65
|
+
session.append("approval/policy", { policy });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Approval service that applies session policy before answerers and logs every
|
|
69
|
+
* ask/outcome pair to the requesting session. It exposes deterministic policy
|
|
70
|
+
* changes to the model through the runtime-context snapshot and switch notices.
|
|
71
|
+
*/
|
|
72
|
+
var ApprovalService = class extends Service {
|
|
73
|
+
config;
|
|
74
|
+
static Config = z.object({ policy: z.union(["ask", "never"]).default("ask") });
|
|
75
|
+
constructor(ctx, config) {
|
|
76
|
+
super(ctx, "approval");
|
|
77
|
+
this.config = config;
|
|
78
|
+
const effective = (agent) => this.effectivePolicy(agent.session);
|
|
79
|
+
ctx.inject(["systemPrompt"], (scope) => {
|
|
80
|
+
scope.systemPrompt.context({
|
|
81
|
+
name: "approval:policy",
|
|
82
|
+
order: scope.systemPrompt.getContextOrder("APPROVAL_POLICY"),
|
|
83
|
+
text: (context) => {
|
|
84
|
+
const agent = context.agent;
|
|
85
|
+
if (agent === void 0) return "";
|
|
86
|
+
return effective(agent) === "never" ? NEVER_SENTENCE : ASK_SENTENCE;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Switch one live agent's policy and queue the transition for its next model
|
|
93
|
+
* step. Session initialization uses {@link setApprovalPolicy} directly
|
|
94
|
+
* because there is no previously visible policy to change.
|
|
95
|
+
* @param agent - the live agent whose policy is changing.
|
|
96
|
+
* @param policy - the new effective policy.
|
|
97
|
+
*/
|
|
98
|
+
setPolicy(agent, policy) {
|
|
99
|
+
const previous = this.effectivePolicy(agent.session);
|
|
100
|
+
if (previous === policy) return;
|
|
101
|
+
setApprovalPolicy(agent.session, policy);
|
|
102
|
+
agent.inject(createUserMessage({
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `The approval policy changed from "${previous}" to "${policy}" (changed by the user).`
|
|
106
|
+
}],
|
|
107
|
+
source: { kind: "user-approval" }
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ask the composed answerers to decide one readonly same-process request.
|
|
112
|
+
* The service borrows the request, agent, session, and live signal directly.
|
|
113
|
+
* The request requires an open turn because the audit pair must be enclosed
|
|
114
|
+
* by the durable log's commit/replay boundary; an idle ask rejects before
|
|
115
|
+
* appending anything. The answerer phase always produces an outcome: an
|
|
116
|
+
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
|
|
117
|
+
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
|
|
118
|
+
* normalized to `'unavailable'`. A failure that prevents either audit append
|
|
119
|
+
* from committing still rejects because returning an unlogged decision would
|
|
120
|
+
* violate the pair. Session contains post-commit observer failures, so an
|
|
121
|
+
* authoritative append cannot reject the request or suppress its matching
|
|
122
|
+
* audit event.
|
|
123
|
+
* @param req - the pending decision (agent, tool identity, reason, signal).
|
|
124
|
+
* @returns the closed outcome; `'allowed-once'` is the only grant.
|
|
125
|
+
* @throws when no turn is open or either audit event fails before the session
|
|
126
|
+
* append commit point.
|
|
127
|
+
*/
|
|
128
|
+
async request(req) {
|
|
129
|
+
const session = req.agent.session;
|
|
130
|
+
if (!hasOpenTurn(session)) throw new Error("approval.request() outside an open turn: the approval/asked + approval/decided audit pair must be turn-enclosed (a bare event between turns is crash-tail garbage on reload). Ask from inside the turn that needs the decision.");
|
|
131
|
+
const id = ApprovalRequestId(randomUUID());
|
|
132
|
+
session.append("approval/asked", {
|
|
133
|
+
id,
|
|
134
|
+
toolName: req.toolName,
|
|
135
|
+
...req.callId !== void 0 ? { callId: req.callId } : {},
|
|
136
|
+
...req.reason !== void 0 ? { reason: req.reason } : {}
|
|
137
|
+
});
|
|
138
|
+
const outcome = await this.decide(req, session);
|
|
139
|
+
session.append("approval/decided", {
|
|
140
|
+
id,
|
|
141
|
+
outcome
|
|
142
|
+
});
|
|
143
|
+
return outcome;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The session's effective policy: its own `approval/policy` fold, else the
|
|
147
|
+
* configured default (the schema already defaulted an omitted policy to
|
|
148
|
+
* `'ask'`; the `??` only narrows the optional-input TYPE).
|
|
149
|
+
* @param session - the exact accepted session whose policy applies.
|
|
150
|
+
* @returns the policy every ask for this session resolves under right now.
|
|
151
|
+
*/
|
|
152
|
+
effectivePolicy(session) {
|
|
153
|
+
return this.overrideOf(session) ?? this.config.policy ?? "ask";
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read the session override without applying the configured default.
|
|
157
|
+
* @param session - session whose log supplies the override.
|
|
158
|
+
* @returns the last logged policy, or `undefined` without one.
|
|
159
|
+
*/
|
|
160
|
+
overrideOf(session) {
|
|
161
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
162
|
+
const event = session.eventAt(SessionSeq(seq));
|
|
163
|
+
if (event?.type === "approval/policy") return event.data.policy;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Dispatch the waterfall, contained and raced against the request signal.
|
|
168
|
+
* @param req - the borrowed public request.
|
|
169
|
+
* @param session - the request agent's session used for policy lookup.
|
|
170
|
+
* @returns the normalized closed outcome.
|
|
171
|
+
*/
|
|
172
|
+
async decide(req, session) {
|
|
173
|
+
const signal = req.signal;
|
|
174
|
+
if (signal?.aborted) return "cancelled";
|
|
175
|
+
if (this.effectivePolicy(session) === "never") return "rejected";
|
|
176
|
+
const answer = Promise.resolve().then(() => this.ctx.waterfall(scopeTarget(req.agent, req.agent), "approval/request", req, () => Promise.resolve("unavailable"))).then((outcome) => OUTCOMES.includes(outcome) ? outcome : "unavailable", () => "unavailable");
|
|
177
|
+
if (signal === void 0) return answer;
|
|
178
|
+
return await new Promise((resolve) => {
|
|
179
|
+
const onAbort = () => {
|
|
180
|
+
signal.removeEventListener("abort", onAbort);
|
|
181
|
+
resolve("cancelled");
|
|
182
|
+
};
|
|
183
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
answer.then((outcome) => {
|
|
185
|
+
signal.removeEventListener("abort", onAbort);
|
|
186
|
+
resolve(outcome);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
//#endregion
|
|
192
|
+
export { APPROVAL_POLICIES, ApprovalRequestId, ApprovalService, ApprovalService as default, setApprovalPolicy };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the approval capability seam, covering requests, cancellation, audit, and per-session policy. Missing
|
|
3
|
+
* answerers fail closed; grants apply only to the requested action.
|
|
4
|
+
* @module @qilin-agent/user-approval
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Service } from '@qilin-agent/kylin';
|
|
7
|
+
import z from '@qilin-agent/schemastery';
|
|
8
|
+
import type { Agent } from '@qilin-agent/agent';
|
|
9
|
+
import { type ToolCallId } from '@qilin-agent/llm';
|
|
10
|
+
import type { ContextFormed } from '@qilin-agent/llm';
|
|
11
|
+
declare module '@qilin-agent/llm' {
|
|
12
|
+
interface MessageSourceMap {
|
|
13
|
+
'user-approval': {
|
|
14
|
+
kind: 'user-approval';
|
|
15
|
+
} & ContextFormed;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
import type { Session } from '@qilin-agent/session';
|
|
19
|
+
declare module '@qilin-agent/kylin' {
|
|
20
|
+
interface Context {
|
|
21
|
+
approval: ApprovalService;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
declare module '@qilin-agent/session/types' {
|
|
25
|
+
interface SessionEventMap {
|
|
26
|
+
/**
|
|
27
|
+
* The session's approval policy was switched — log-only, durable,
|
|
28
|
+
* replayable, never in the model transcript (the model learns the policy
|
|
29
|
+
* from the runtime-context snapshot and live switch notices). The LAST
|
|
30
|
+
* such event is the session's override.
|
|
31
|
+
* `source: 'delegation'` marks an override seeded into a child; an absent
|
|
32
|
+
* source is a runtime switch.
|
|
33
|
+
*/
|
|
34
|
+
'approval/policy': {
|
|
35
|
+
policy: ApprovalPolicy;
|
|
36
|
+
/** Marks an override seeded into a child at delegation. */
|
|
37
|
+
source?: 'delegation';
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
import type { ApprovalOutcome, ApprovalRequestEvent } from './types.ts';
|
|
42
|
+
export { ApprovalRequestId } from './types.ts';
|
|
43
|
+
export type { ApprovalOutcome } from './types.ts';
|
|
44
|
+
/**
|
|
45
|
+
* A session's approval policy — what happens to an {@link ApprovalService}
|
|
46
|
+
* ask BEFORE any interactive answerer sees it:
|
|
47
|
+
*
|
|
48
|
+
* - `'ask'` (the default) — delegate to the composed answerers; with none
|
|
49
|
+
* composed the chain falls through to the fail-closed `'unavailable'`.
|
|
50
|
+
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
|
|
51
|
+
* deterministically. The strict headless stance (CI, unattended runs) and
|
|
52
|
+
* the policy whose outcome is knowable without asking.
|
|
53
|
+
*/
|
|
54
|
+
export type ApprovalPolicy = 'ask' | 'never';
|
|
55
|
+
/** Every {@link ApprovalPolicy}, for option advertisement and runtime validation of untrusted policy strings. */
|
|
56
|
+
export declare const APPROVAL_POLICIES: readonly ApprovalPolicy[];
|
|
57
|
+
/**
|
|
58
|
+
* Append the sole durable representation of a session policy override. Invalid
|
|
59
|
+
* values throw before the log changes; consumers fold the new value on each read.
|
|
60
|
+
* @param session - the session the override belongs to.
|
|
61
|
+
* @param policy - the policy in effect until the next switch.
|
|
62
|
+
*/
|
|
63
|
+
export declare function setApprovalPolicy(session: Session, policy: ApprovalPolicy): void;
|
|
64
|
+
/**
|
|
65
|
+
* Readonly same-process permission question. `callId` links to an already
|
|
66
|
+
* presented tool call, so arguments are not duplicated here.
|
|
67
|
+
*/
|
|
68
|
+
export interface ApprovalRequest extends ApprovalRequestEvent {
|
|
69
|
+
/**
|
|
70
|
+
* The agent on whose behalf the question is asked. Routes the question (a
|
|
71
|
+
* UI answerer only answers for agents it owns) and receives the audit
|
|
72
|
+
* events on its session log.
|
|
73
|
+
*/
|
|
74
|
+
readonly agent: Agent;
|
|
75
|
+
/** The tool the question is about (presentation and audit). */
|
|
76
|
+
readonly toolName: string;
|
|
77
|
+
/**
|
|
78
|
+
* The exact tool call being decided, when the asker has one — lets a UI
|
|
79
|
+
* attach the prompt to the tool call it already streamed.
|
|
80
|
+
*/
|
|
81
|
+
readonly callId?: ToolCallId;
|
|
82
|
+
/** The asker's human-readable explanation of WHY it is asking. */
|
|
83
|
+
readonly reason?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Aborting withdraws the question: the request settles `'cancelled'`
|
|
86
|
+
* immediately and a late answer from a still-pending answerer is discarded.
|
|
87
|
+
*/
|
|
88
|
+
readonly signal?: AbortSignal;
|
|
89
|
+
}
|
|
90
|
+
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
|
91
|
+
export interface Config {
|
|
92
|
+
/**
|
|
93
|
+
* The deployment's default {@link ApprovalPolicy} for sessions without an
|
|
94
|
+
* `approval/policy` override — `'ask'` delegates to the composed answerers
|
|
95
|
+
* (fail-closed with none); `'never'` auto-rejects every ask without
|
|
96
|
+
* prompting (the deterministic CI/unattended stance).
|
|
97
|
+
*/
|
|
98
|
+
readonly policy?: ApprovalPolicy;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Approval service that applies session policy before answerers and logs every
|
|
102
|
+
* ask/outcome pair to the requesting session. It exposes deterministic policy
|
|
103
|
+
* changes to the model through the runtime-context snapshot and switch notices.
|
|
104
|
+
*/
|
|
105
|
+
export declare class ApprovalService extends Service {
|
|
106
|
+
config: Config;
|
|
107
|
+
static Config: z<Config>;
|
|
108
|
+
constructor(ctx: Context, config: Config);
|
|
109
|
+
/**
|
|
110
|
+
* Switch one live agent's policy and queue the transition for its next model
|
|
111
|
+
* step. Session initialization uses {@link setApprovalPolicy} directly
|
|
112
|
+
* because there is no previously visible policy to change.
|
|
113
|
+
* @param agent - the live agent whose policy is changing.
|
|
114
|
+
* @param policy - the new effective policy.
|
|
115
|
+
*/
|
|
116
|
+
setPolicy(agent: Agent, policy: ApprovalPolicy): void;
|
|
117
|
+
/**
|
|
118
|
+
* Ask the composed answerers to decide one readonly same-process request.
|
|
119
|
+
* The service borrows the request, agent, session, and live signal directly.
|
|
120
|
+
* The request requires an open turn because the audit pair must be enclosed
|
|
121
|
+
* by the durable log's commit/replay boundary; an idle ask rejects before
|
|
122
|
+
* appending anything. The answerer phase always produces an outcome: an
|
|
123
|
+
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
|
|
124
|
+
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
|
|
125
|
+
* normalized to `'unavailable'`. A failure that prevents either audit append
|
|
126
|
+
* from committing still rejects because returning an unlogged decision would
|
|
127
|
+
* violate the pair. Session contains post-commit observer failures, so an
|
|
128
|
+
* authoritative append cannot reject the request or suppress its matching
|
|
129
|
+
* audit event.
|
|
130
|
+
* @param req - the pending decision (agent, tool identity, reason, signal).
|
|
131
|
+
* @returns the closed outcome; `'allowed-once'` is the only grant.
|
|
132
|
+
* @throws when no turn is open or either audit event fails before the session
|
|
133
|
+
* append commit point.
|
|
134
|
+
*/
|
|
135
|
+
request(req: ApprovalRequest): Promise<ApprovalOutcome>;
|
|
136
|
+
/**
|
|
137
|
+
* The session's effective policy: its own `approval/policy` fold, else the
|
|
138
|
+
* configured default (the schema already defaulted an omitted policy to
|
|
139
|
+
* `'ask'`; the `??` only narrows the optional-input TYPE).
|
|
140
|
+
* @param session - the exact accepted session whose policy applies.
|
|
141
|
+
* @returns the policy every ask for this session resolves under right now.
|
|
142
|
+
*/
|
|
143
|
+
private effectivePolicy;
|
|
144
|
+
/**
|
|
145
|
+
* Read the session override without applying the configured default.
|
|
146
|
+
* @param session - session whose log supplies the override.
|
|
147
|
+
* @returns the last logged policy, or `undefined` without one.
|
|
148
|
+
*/
|
|
149
|
+
overrideOf(session: Session): ApprovalPolicy | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Dispatch the waterfall, contained and raced against the request signal.
|
|
152
|
+
* @param req - the borrowed public request.
|
|
153
|
+
* @param session - the request agent's session used for policy lookup.
|
|
154
|
+
* @returns the normalized closed outcome.
|
|
155
|
+
*/
|
|
156
|
+
private decide;
|
|
157
|
+
}
|
|
158
|
+
export default ApprovalService;
|
|
159
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the approval capability seam, covering requests, cancellation, audit, and per-session policy. Missing
|
|
3
|
+
* answerers fail closed; grants apply only to the requested action.
|
|
4
|
+
* @module @qilin-agent/user-approval
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { Service } from '@qilin-agent/kylin';
|
|
8
|
+
import z from '@qilin-agent/schemastery';
|
|
9
|
+
import { createUserMessage } from '@qilin-agent/llm';
|
|
10
|
+
import { scopeTarget } from '@qilin-agent/scope';
|
|
11
|
+
import { SessionSeq } from '@qilin-agent/session';
|
|
12
|
+
import { ApprovalRequestId } from "./types.js";
|
|
13
|
+
export { ApprovalRequestId } from "./types.js";
|
|
14
|
+
/** Every {@link ApprovalOutcome}, for runtime normalization of answerer returns. */
|
|
15
|
+
const OUTCOMES = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
|
|
16
|
+
/** Every {@link ApprovalPolicy}, for option advertisement and runtime validation of untrusted policy strings. */
|
|
17
|
+
export const APPROVAL_POLICIES = ['ask', 'never'];
|
|
18
|
+
/** Model-facing statement for the deterministic `'never'` policy. */
|
|
19
|
+
const NEVER_SENTENCE = 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).';
|
|
20
|
+
/** Model-facing statement for an interactive policy that may still fail closed. */
|
|
21
|
+
const ASK_SENTENCE = 'Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.';
|
|
22
|
+
/**
|
|
23
|
+
* Whether the log currently sits inside an open turn (a `turn/start` not yet
|
|
24
|
+
* closed by a `turn/end`) — the {@link ApprovalService.request} precondition.
|
|
25
|
+
* The audit pair must be turn-enclosed: the turn is the durable log's
|
|
26
|
+
* commit/replay boundary, so a bare event appended between turns is
|
|
27
|
+
* indistinguishable from a crash tail and silently dropped on reload.
|
|
28
|
+
*/
|
|
29
|
+
function hasOpenTurn(session) {
|
|
30
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
31
|
+
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
32
|
+
const type = session.eventAt(SessionSeq(seq))?.type;
|
|
33
|
+
if (type === 'turn/start')
|
|
34
|
+
return true;
|
|
35
|
+
if (type === 'turn/end')
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Append the sole durable representation of a session policy override. Invalid
|
|
42
|
+
* values throw before the log changes; consumers fold the new value on each read.
|
|
43
|
+
* @param session - the session the override belongs to.
|
|
44
|
+
* @param policy - the policy in effect until the next switch.
|
|
45
|
+
*/
|
|
46
|
+
export function setApprovalPolicy(session, policy) {
|
|
47
|
+
if (!APPROVAL_POLICIES.includes(policy)) {
|
|
48
|
+
throw new TypeError('approval policy must be one of "ask" or "never"');
|
|
49
|
+
}
|
|
50
|
+
session.append('approval/policy', { policy });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Approval service that applies session policy before answerers and logs every
|
|
54
|
+
* ask/outcome pair to the requesting session. It exposes deterministic policy
|
|
55
|
+
* changes to the model through the runtime-context snapshot and switch notices.
|
|
56
|
+
*/
|
|
57
|
+
export class ApprovalService extends Service {
|
|
58
|
+
config;
|
|
59
|
+
static Config = z.object({
|
|
60
|
+
policy: z.union(['ask', 'never']).default('ask'),
|
|
61
|
+
});
|
|
62
|
+
constructor(ctx, config) {
|
|
63
|
+
super(ctx, 'approval');
|
|
64
|
+
this.config = config;
|
|
65
|
+
const effective = (agent) => this.effectivePolicy(agent.session);
|
|
66
|
+
// The complete current value travels after retained history, so switching
|
|
67
|
+
// policy does not rewrite the stable system-prompt cache prefix.
|
|
68
|
+
ctx.inject(['systemPrompt'], (scope) => {
|
|
69
|
+
scope.systemPrompt.context({
|
|
70
|
+
name: 'approval:policy',
|
|
71
|
+
order: scope.systemPrompt.getContextOrder('APPROVAL_POLICY'),
|
|
72
|
+
text: (context) => {
|
|
73
|
+
const agent = context.agent;
|
|
74
|
+
// A bare assemble() (tests, diagnostics) has no session to state.
|
|
75
|
+
if (agent === undefined)
|
|
76
|
+
return '';
|
|
77
|
+
const policy = effective(agent);
|
|
78
|
+
return policy === 'never' ? NEVER_SENTENCE : ASK_SENTENCE;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Switch one live agent's policy and queue the transition for its next model
|
|
85
|
+
* step. Session initialization uses {@link setApprovalPolicy} directly
|
|
86
|
+
* because there is no previously visible policy to change.
|
|
87
|
+
* @param agent - the live agent whose policy is changing.
|
|
88
|
+
* @param policy - the new effective policy.
|
|
89
|
+
*/
|
|
90
|
+
setPolicy(agent, policy) {
|
|
91
|
+
const previous = this.effectivePolicy(agent.session);
|
|
92
|
+
if (previous === policy)
|
|
93
|
+
return;
|
|
94
|
+
setApprovalPolicy(agent.session, policy);
|
|
95
|
+
agent.inject(createUserMessage({
|
|
96
|
+
content: [{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `The approval policy changed from "${previous}" to "${policy}" (changed by the user).`,
|
|
99
|
+
}],
|
|
100
|
+
source: { kind: 'user-approval' },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Ask the composed answerers to decide one readonly same-process request.
|
|
105
|
+
* The service borrows the request, agent, session, and live signal directly.
|
|
106
|
+
* The request requires an open turn because the audit pair must be enclosed
|
|
107
|
+
* by the durable log's commit/replay boundary; an idle ask rejects before
|
|
108
|
+
* appending anything. The answerer phase always produces an outcome: an
|
|
109
|
+
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
|
|
110
|
+
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
|
|
111
|
+
* normalized to `'unavailable'`. A failure that prevents either audit append
|
|
112
|
+
* from committing still rejects because returning an unlogged decision would
|
|
113
|
+
* violate the pair. Session contains post-commit observer failures, so an
|
|
114
|
+
* authoritative append cannot reject the request or suppress its matching
|
|
115
|
+
* audit event.
|
|
116
|
+
* @param req - the pending decision (agent, tool identity, reason, signal).
|
|
117
|
+
* @returns the closed outcome; `'allowed-once'` is the only grant.
|
|
118
|
+
* @throws when no turn is open or either audit event fails before the session
|
|
119
|
+
* append commit point.
|
|
120
|
+
*/
|
|
121
|
+
async request(req) {
|
|
122
|
+
const session = req.agent.session;
|
|
123
|
+
if (!hasOpenTurn(session)) {
|
|
124
|
+
throw new Error('approval.request() outside an open turn: the approval/asked + approval/decided audit pair '
|
|
125
|
+
+ 'must be turn-enclosed (a bare event between turns is crash-tail garbage on reload). '
|
|
126
|
+
+ 'Ask from inside the turn that needs the decision.');
|
|
127
|
+
}
|
|
128
|
+
const id = ApprovalRequestId(randomUUID());
|
|
129
|
+
session.append('approval/asked', {
|
|
130
|
+
id,
|
|
131
|
+
toolName: req.toolName,
|
|
132
|
+
...req.callId !== undefined ? { callId: req.callId } : {},
|
|
133
|
+
...req.reason !== undefined ? { reason: req.reason } : {},
|
|
134
|
+
});
|
|
135
|
+
const outcome = await this.decide(req, session);
|
|
136
|
+
session.append('approval/decided', { id, outcome });
|
|
137
|
+
return outcome;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The session's effective policy: its own `approval/policy` fold, else the
|
|
141
|
+
* configured default (the schema already defaulted an omitted policy to
|
|
142
|
+
* `'ask'`; the `??` only narrows the optional-input TYPE).
|
|
143
|
+
* @param session - the exact accepted session whose policy applies.
|
|
144
|
+
* @returns the policy every ask for this session resolves under right now.
|
|
145
|
+
*/
|
|
146
|
+
effectivePolicy(session) {
|
|
147
|
+
return this.overrideOf(session) ?? this.config.policy ?? 'ask';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read the session override without applying the configured default.
|
|
151
|
+
* @param session - session whose log supplies the override.
|
|
152
|
+
* @returns the last logged policy, or `undefined` without one.
|
|
153
|
+
*/
|
|
154
|
+
overrideOf(session) {
|
|
155
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
156
|
+
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
157
|
+
const event = session.eventAt(SessionSeq(seq));
|
|
158
|
+
if (event?.type === 'approval/policy')
|
|
159
|
+
return event.data.policy;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Dispatch the waterfall, contained and raced against the request signal.
|
|
165
|
+
* @param req - the borrowed public request.
|
|
166
|
+
* @param session - the request agent's session used for policy lookup.
|
|
167
|
+
* @returns the normalized closed outcome.
|
|
168
|
+
*/
|
|
169
|
+
async decide(req, session) {
|
|
170
|
+
const signal = req.signal;
|
|
171
|
+
if (signal?.aborted)
|
|
172
|
+
return 'cancelled';
|
|
173
|
+
// The 'never' policy is decided HERE, before any dispatch: a listener
|
|
174
|
+
// registered with `prepend: true` after this service mounts would sit
|
|
175
|
+
// ahead of any gate LISTENER, so a listener-shaped gate cannot keep the
|
|
176
|
+
// documented promise that 'never' rejects deterministically regardless
|
|
177
|
+
// of registration order — only the service's own request path can.
|
|
178
|
+
if (this.effectivePolicy(session) === 'never')
|
|
179
|
+
return 'rejected';
|
|
180
|
+
// Enter the promise chain BEFORE dispatching: a listener that throws
|
|
181
|
+
// SYNCHRONOUSLY (before its first await) must land in the same rejection
|
|
182
|
+
// path as an async one — `Promise.resolve(call())` would let it escape
|
|
183
|
+
// the containment into the caller.
|
|
184
|
+
const answer = Promise.resolve().then(() => this.ctx.waterfall(scopeTarget(req.agent, req.agent), 'approval/request', req, () => Promise.resolve('unavailable'))).then(
|
|
185
|
+
// Normalize a rogue (non-vocabulary) answerer return to the fail-closed
|
|
186
|
+
// outcome instead of leaking it into callers' closed-union switches.
|
|
187
|
+
outcome => OUTCOMES.includes(outcome) ? outcome : 'unavailable',
|
|
188
|
+
// A throwing answerer must fail the QUESTION closed, not the caller's
|
|
189
|
+
// tool call open — the seam contains its callbacks.
|
|
190
|
+
() => 'unavailable');
|
|
191
|
+
if (signal === undefined)
|
|
192
|
+
return answer;
|
|
193
|
+
return await new Promise((resolve) => {
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
signal.removeEventListener('abort', onAbort);
|
|
196
|
+
resolve('cancelled');
|
|
197
|
+
};
|
|
198
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
199
|
+
void answer.then((outcome) => {
|
|
200
|
+
signal.removeEventListener('abort', onAbort);
|
|
201
|
+
// After an abort won the race this resolve is a settled-promise no-op:
|
|
202
|
+
// the late answer is discarded by construction.
|
|
203
|
+
resolve(outcome);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export default ApprovalService;
|
|
209
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-safe approval identifiers and outcome vocabulary, free of
|
|
3
|
+
* cordis/service imports so browser type chains can
|
|
4
|
+
* consume them without loading this package's Context augmentation.
|
|
5
|
+
* @module @qilin-agent/user-approval/types
|
|
6
|
+
*/
|
|
7
|
+
import type { Branded } from '@qilin-agent/brand';
|
|
8
|
+
import type { Scoped } from '@qilin-agent/scope';
|
|
9
|
+
import type { Agent } from '@qilin-agent/agent/types';
|
|
10
|
+
import type { ToolCallId } from '@qilin-agent/llm/brand';
|
|
11
|
+
/**
|
|
12
|
+
* Pairs one `approval/asked` audit event with its `approval/decided`.
|
|
13
|
+
* Service-issued (one fresh id per {@link ApprovalService.request} call).
|
|
14
|
+
*/
|
|
15
|
+
export type ApprovalRequestId = Branded<'ApprovalRequestId'>;
|
|
16
|
+
/**
|
|
17
|
+
* Brand a string as an {@link ApprovalRequestId}.
|
|
18
|
+
* @param id - the raw id string to brand.
|
|
19
|
+
* @returns the same string carrying the brand.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ApprovalRequestId(id: string): ApprovalRequestId;
|
|
22
|
+
/**
|
|
23
|
+
* Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn
|
|
24
|
+
* request, or unavailable answerer. Callers fail closed on `unavailable`.
|
|
25
|
+
*/
|
|
26
|
+
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
27
|
+
declare module '@qilin-agent/session/types' {
|
|
28
|
+
interface SessionEventMap {
|
|
29
|
+
/**
|
|
30
|
+
* An approval question was put to the answerer chain — log-only audit
|
|
31
|
+
* (like `hook/*`; NOT a surface event, carries no `surfaceOp`). `id` pairs
|
|
32
|
+
* it with the `approval/decided` that always follows; `toolName` is the
|
|
33
|
+
* tool the question is about, `callId` the exact tool call when the asker
|
|
34
|
+
* had one, `reason` the asker's human-readable explanation (e.g. a hook's
|
|
35
|
+
* permission-decision reason).
|
|
36
|
+
*/
|
|
37
|
+
'approval/asked': {
|
|
38
|
+
id: ApprovalRequestId;
|
|
39
|
+
toolName: string;
|
|
40
|
+
callId?: ToolCallId;
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The outcome of a prior `approval/asked` (same `id`) — log-only audit.
|
|
45
|
+
* Exactly one per ask, appended when the outcome is known: a decision, a
|
|
46
|
+
* cancellation, or the fail-closed `'unavailable'`.
|
|
47
|
+
*/
|
|
48
|
+
'approval/decided': {
|
|
49
|
+
id: ApprovalRequestId;
|
|
50
|
+
outcome: ApprovalOutcome;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Client-safe payload declared for the approval answerer waterfall. */
|
|
55
|
+
export interface ApprovalRequestEvent {
|
|
56
|
+
/** Agent identity projected to the corresponding Client Context in transit. */
|
|
57
|
+
readonly agent: Agent;
|
|
58
|
+
/** Tool whose operation requires a decision. */
|
|
59
|
+
readonly toolName: string;
|
|
60
|
+
/** Exact tool call being decided, when available. */
|
|
61
|
+
readonly callId?: ToolCallId;
|
|
62
|
+
/** Human-readable reason supplied by the asker. */
|
|
63
|
+
readonly reason?: string;
|
|
64
|
+
/** Localized presentation only; never persisted in approval audit events. */
|
|
65
|
+
readonly displayReason?: {
|
|
66
|
+
readonly en: string;
|
|
67
|
+
readonly [locale: string]: string;
|
|
68
|
+
};
|
|
69
|
+
/** Cancellation lifetime of the pending request. */
|
|
70
|
+
readonly signal?: AbortSignal;
|
|
71
|
+
}
|
|
72
|
+
declare module '@qilin-agent/kylin' {
|
|
73
|
+
interface Events {
|
|
74
|
+
/**
|
|
75
|
+
* Ask composed answerers for one decision. Return an outcome to claim the
|
|
76
|
+
* request or call `next()` to delegate. Scope-filtered dispatch
|
|
77
|
+
* (`@qilin-agent/scope`): agent-scoped listeners receive only that agent.
|
|
78
|
+
* @param req - pending approval request.
|
|
79
|
+
* @mode waterfall
|
|
80
|
+
*/
|
|
81
|
+
'approval/request'(this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-safe approval identifiers and outcome vocabulary, free of
|
|
3
|
+
* cordis/service imports so browser type chains can
|
|
4
|
+
* consume them without loading this package's Context augmentation.
|
|
5
|
+
* @module @qilin-agent/user-approval/types
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Brand a string as an {@link ApprovalRequestId}.
|
|
9
|
+
* @param id - the raw id string to brand.
|
|
10
|
+
* @returns the same string carrying the brand.
|
|
11
|
+
*/
|
|
12
|
+
export function ApprovalRequestId(id) {
|
|
13
|
+
return id;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/user-approval",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "User-approval seam (ctx.approval) for the QiLin: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/interaction/user-approval"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./lib/types/types.d.ts",
|
|
26
|
+
"default": "./lib/types/types.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/index.js",
|
|
33
|
+
"lib/types/**/*.js",
|
|
34
|
+
"lib/types/**/*.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@qilin-agent/agent": "3.1.3",
|
|
39
|
+
"@qilin-agent/brand": "3.1.3",
|
|
40
|
+
"@qilin-agent/llm": "3.1.3",
|
|
41
|
+
"@qilin-agent/scope": "3.1.3",
|
|
42
|
+
"@qilin-agent/session": "3.1.3",
|
|
43
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
44
|
+
"@qilin-agent/system-prompt": "3.1.3"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@qilin-agent/schemastery": "~3.18.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@qilin-agent/agent": "3.1.3",
|
|
51
|
+
"@qilin-agent/brand": "3.1.3",
|
|
52
|
+
"@qilin-agent/llm": "3.1.3",
|
|
53
|
+
"@qilin-agent/scope": "3.1.3",
|
|
54
|
+
"@qilin-agent/session": "3.1.3",
|
|
55
|
+
"@qilin-agent/system-prompt": "3.1.3",
|
|
56
|
+
"@qilin-agent/kylin": "~4.0.3"
|
|
57
|
+
}
|
|
6
58
|
}
|