@qilin-agent/sandbox 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 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.
@@ -0,0 +1,91 @@
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/sandbox/sandbox/README.md
5
+ /:
6
+ en: 4308065a46738364
7
+ zh: 2eb6ab7314e74976
8
+ /qilin-sandbox:
9
+ en: 64b8dc02e4d27537
10
+ zh: e6fd653f8f664a92
11
+ /qilin-sandbox/summary:
12
+ en: 3145aff8aac31187
13
+ zh: c1b6c1f8dc3f93c1
14
+ /qilin-sandbox/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /qilin-sandbox/use-this-package:
18
+ en: 5fbf0b48effb1143
19
+ zh: 5c570dcc1a17e0ad
20
+ /qilin-sandbox/use-this-package/when-to-choose-it:
21
+ en: 4ddc504d2626fe3e
22
+ zh: ceb3dd93fb3835db
23
+ /qilin-sandbox/use-this-package/confining-a-command:
24
+ en: c5540889017b6a7f
25
+ zh: 679bfbd5254bf92d
26
+ /qilin-sandbox/use-this-package/modes-and-enforcement:
27
+ en: ab093e9042b86b99
28
+ zh: d173df7cb3649446
29
+ /qilin-sandbox/use-this-package/denied-calls-and-escalation:
30
+ en: ff24616d2a68ff77
31
+ zh: 49cb99b3d941dab1
32
+ /qilin-sandbox/use-this-package/fail-closed-behavior:
33
+ en: 6bf240632a7a6bf5
34
+ zh: ebf414f5cf435875
35
+ /qilin-sandbox/understand-the-implementation:
36
+ en: 7e13d669ecc96d59
37
+ zh: b5fe7ae65812d926
38
+ /qilin-sandbox/understand-the-implementation/design-philosophy:
39
+ en: 0fc96495c192b513
40
+ zh: 38b51b9b3c208012
41
+ /qilin-sandbox/understand-the-implementation/source-map:
42
+ en: f58b67217cbc147a
43
+ zh: b2cbde0342f3f8aa
44
+ /qilin-sandbox/understand-the-implementation/escalation-choreography:
45
+ en: 1b418d41f571ab3d
46
+ zh: 62b289cdd6f4d272
47
+ /qilin-sandbox/understand-the-implementation/writable-roots:
48
+ en: 956d3195cf7a35f8
49
+ zh: f83d8342e22bb7cd
50
+ /qilin-sandbox/further-exploration:
51
+ en: d32c98d7d2b8d6b2
52
+ zh: da0decd6d48b9069
53
+ /qilin-sandbox/model-experience:
54
+ en: 215e7ba838619b7b
55
+ zh: a311da3843709f90
56
+ /qilin-sandbox/model-experience/confinement-error-indirectly:
57
+ en: 35e1ea00601f4003
58
+ zh: 08152430b840ea05
59
+ /qilin-sandbox/model-experience/confinement-error-indirectly/what-the-model-sees:
60
+ en: 42d3edf30f9d2f52
61
+ zh: 13ab629a0902581c
62
+ /qilin-sandbox/model-experience/confinement-error-indirectly/what-the-model-sees/exact-error:
63
+ en: e8b03df128b832a6
64
+ zh: dc89500c436a39cf
65
+ /qilin-sandbox/model-experience/confinement-error-indirectly/token-effect:
66
+ en: 8f514efe94edfa8a
67
+ zh: a46f9f4cd4527f4e
68
+ /qilin-sandbox/model-experience/confinement-error-indirectly/kv-cache-effect:
69
+ en: 2fbf2175168e8244
70
+ zh: f681f89596f500f5
71
+ /qilin-sandbox/model-experience/escalation-request-and-outcome:
72
+ en: 67c621ef55ec8320
73
+ zh: ae4bb9162a49e9fb
74
+ /qilin-sandbox/model-experience/escalation-request-and-outcome/what-the-model-sees:
75
+ en: c20128fa8f79c491
76
+ zh: 96173408d176b9e6
77
+ /qilin-sandbox/model-experience/escalation-request-and-outcome/token-effect:
78
+ en: 1a7b54d68b045beb
79
+ zh: 5dc6e824110c8fd8
80
+ /qilin-sandbox/model-experience/escalation-request-and-outcome/kv-cache-effect:
81
+ en: ea21ed3a768ed4b8
82
+ zh: 8e2f396a6875420e
83
+ /qilin-sandbox/known-limitations-and-deferred-work:
84
+ en: 3b0b864cc3a91b26
85
+ zh: 43f73818d1d2c7e2
86
+ /qilin-sandbox/known-limitations-and-deferred-work/dev-note:
87
+ en: cc273320f6a1beaf
88
+ zh: 5c57a312ddc50651
89
+ /qilin-sandbox/known-limitations-and-deferred-work/dev-note/future-consumers-and-environments:
90
+ en: 9217a1563d34029c
91
+ zh: 5812ebd9ef7d4f23
package/README.md CHANGED
@@ -1,3 +1,184 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "The process-sandbox service contract for users and maintainers composing, using, or extending same-world subprocess confinement."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @qilin-agent/sandbox
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Use `qilin-sandbox` to run a subprocess and everything it spawns under a per-call file-access policy. A command can run without writes (`read-only`), write only inside its workspace (`workspace-write`), or run unrestricted (`danger-full-access`). If the requested mode cannot be enforced, the call fails with `SANDBOX_UNAVAILABLE` instead of running unconfined. After a denied call, the model can request one strictly wider mode for human approval. This is same-world confinement: the process still shares the host kernel and filesystem; use a container, microVM, or remote executor when the whole environment must be isolated.
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 with one backend and a confined consumer, and every command the consumer runs executes under the policy you resolve — you see only the confinement result and its enforcement completeness, never the platform runner.
29
+
30
+ ### When to choose it
31
+
32
+ Choose this package when a composition confines subprocesses on the host: it is the contract the local backends and the confined executors both implement, so mounting `sandbox-local` behind `ctx.sandbox` and a confined executor behind `ctx.shell` gives every bash or pwsh call a confined default. Choose something else when the process must run in an isolated environment — a container, microVM, or remote executor replaces the whole `ctx.shell`/`ctx.fs` capability rather than adding a backend here.
33
+
34
+ ### Confining a command
35
+
36
+ Mount the service with a backend and a confined executor; the [base bundle](../../bundle/base/cordis.patch.yml) owns the shipped composition.
37
+
38
+ ```yaml
39
+ - id: sandbox
40
+ name: '@qilin-agent/sandbox-local' # the per-platform backend provider (ctx.sandbox)
41
+ - id: sandbox-policy
42
+ name: '@qilin-agent/sandbox-policy' # the deployment default mode and workspace-write root
43
+ config:
44
+ mode: workspace-write # the deployment default every session starts from
45
+ workspaceRoot: !!js process.cwd() # the boundary workspace-write may write under
46
+ - id: bash
47
+ name: '@qilin-agent/bash-sandbox' # the confined executor behind ctx.shell
48
+ ```
49
+
50
+ With this composition, a bash call runs confined under `workspace-write`: writes inside the workspace succeed, writes outside it are denied, and the model can recover through the escalation flow below.
51
+
52
+ ### Modes and enforcement
53
+
54
+ The mode names the file effects a command may perform; enforcement completeness reports how fully the backend governs them.
55
+
56
+ | Mode | Effect |
57
+ |---|---|
58
+ | `read-only` | Denies writes except required sinks such as `/dev/null` |
59
+ | `workspace-write` | Allows writes under the workspace root plus a backend-defined temp area |
60
+ | `danger-full-access` | Bypasses confinement; the consumer spawns its original argv |
61
+
62
+ Enforcement is reported per call: `full` means the backend governs every promised file effect, while `partial` means an active backend or older kernel ABI governs only a subset — the Windows ACL rung and older Landlock ABIs are the current partial cases, so a consumer that requires the absolute boundary can reject or surface them.
63
+
64
+ ### Denied calls and escalation
65
+
66
+ When a confined call is denied, the operation reports a denial marker naming the mode — `[sandbox: file access denied under <mode> mode]` — and, when the composition advertises escalation, an escalation hint. The model may retry the exact call once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a `justification`; the user sees one approval prompt and can allow once, reject, or cancel. A wider mode requires approval and applies to that one call only. Repeating the call's effective mode succeeds without approval; narrower targets remain invalid.
67
+
68
+ ### Fail-closed behavior
69
+
70
+ When no backend can enforce the requested mode, the call fails with `SANDBOX_UNAVAILABLE` rather than running unconfined; the error text names the missing platform runner. A backend that fails after starting also reports a structured runner-failure signature, so a broken sandbox is distinguishable from a command failure.
71
+
72
+ -----
73
+
74
+ <a id="understand-the-implementation"></a>
75
+ ## Understand the implementation
76
+
77
+ <details>
78
+ <summary>Implementation internals — click to expand</summary>
79
+
80
+ This section explains the design decisions behind the contract and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
81
+
82
+ ### Design philosophy
83
+
84
+ - **One execution world.** The filesystem, subprocess and sandbox providers operate on the same filesystem and kernel. Remote compositions replace all three providers; confinement resolves asynchronously in that world.
85
+ - **Policy rides the call.** `SandboxPolicy` is carried per call, never fixed on the provider: two consumers may confine under different policies at the same instant, and an escalated retry is a new call with a wider policy. Defaulting and resolution are explicit consumer steps.
86
+ - **Fail closed.** `confine()` resolves to enforcing argv or rejects with `SandboxUnavailableError`; silent unconfined passthrough is forbidden, and functional probes arbitrate multi-runner chains.
87
+ - **One vocabulary for denial and escalation.** The marker and hint texts and the strictly-wider ladder live here so the bash and fs families cannot drift apart.
88
+
89
+ ### Source map
90
+
91
+ | File | Role |
92
+ |---|---|
93
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `SandboxProvider` service, mode/enforcement/policy types, fail-closed error |
94
+ | [`src/escalation.ts`](src/escalation.ts) | Escalation vocabulary: wider-mode ladder, argument validation, denial and hint markers, approval choreography |
95
+ | [`src/roots.ts`](src/roots.ts) | Writable-root derivation shared by the Seatbelt profile and the in-process fs fence |
96
+
97
+ ### Escalation choreography
98
+
99
+ The ladder is a closed table — `read-only` may escalate to `workspace-write` or `danger-full-access`, `workspace-write` only to `danger-full-access` — checked at execution, never baked into a tool schema, whose enum stays the closed target vocabulary. [`approveEscalation`](src/escalation.ts) returns the current mode without approval when it is repeated, rejects narrower or unsupported targets, and requests approval for wider modes. Callers validate the `sandbox_permissions`/`justification` pairing first.
100
+
101
+ ### Writable roots
102
+
103
+ `workspace-write` means "the workspace root plus the host temp areas": `writableRoots` derives that allow-list canonically, resolving symlinks and deduplicating, so the Seatbelt profile and the in-process fs fence grant exactly the same roots.
104
+
105
+ </details>
106
+
107
+ -----
108
+
109
+ <a id="further-exploration"></a>
110
+ ## Further Exploration
111
+
112
+ Start with the subsystem reference for the exhaustive contract, then the backends, consumers, and policy source that realize it.
113
+
114
+ - [Process sandbox subsystem](../../../docs/subsystems/sandbox.md) — the complete vocabulary, per-call policy, and classification dialects.
115
+ - [The subprocess sandbox decision](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) — capability boundary, escalation design, and deferred phases.
116
+ - [Local sandbox backends](../sandbox-local/README.md) — the per-platform runners behind `ctx.sandbox`.
117
+ - [Bash sandbox executor](../../shell/bash-sandbox/README.md) — the confined bash consumer.
118
+ - [Sandbox policy package](../sandbox-policy/README.md) — where the per-call mode and workspace root come from.
119
+
120
+ -----
121
+
122
+ <a id="model-experience"></a>
123
+ ## Model Experience
124
+
125
+ ### Confinement error, indirectly
126
+
127
+ #### What the model sees
128
+
129
+ Through [`qilin-bash-sandbox`](../../shell/bash-sandbox/README.md) and [`qilin-tool-bash`](../../shell/tool-bash/README.md), a requested confined mode with no usable backend produces code `SANDBOX_UNAVAILABLE` and the exact error below; an execution-time runner failure appends ` Runner failure: <detail>`.
130
+
131
+ ##### Exact error
132
+
133
+ ```markdown
134
+ sandbox mode "<mode>" is requested but no sandbox backend is usable on this host; refusing to run the command unconfined. Install bubblewrap or run a Landlock-enforcing kernel (Linux), ensure sandbox-exec is usable (macOS), or ensure the ACL restricted-token runner can start (Windows) — otherwise switch the consumer to danger-full-access.
135
+ ```
136
+
137
+ #### Token effect
138
+
139
+ Conditional error text is visible for that call and retained in history until compaction.
140
+
141
+ #### KV Cache effect
142
+
143
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV Cache entries.
144
+
145
+ ### Escalation request and outcome
146
+
147
+ #### What the model sees
148
+
149
+ A denied call surfaces the marker `[sandbox: file access denied under <mode> mode]` and, where the composition advertises escalation, the hint `[sandbox: escalation available — retry this exact <subject> once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`. The retry carries `sandbox_permissions` and a `justification`; the user's `allowed-once` / `rejected` / `cancelled` decision becomes the call's result text.
150
+
151
+ #### Token effect
152
+
153
+ Only the denied call's error and any escalation outcome text are visible; both are retained in history until compaction.
154
+
155
+ #### KV Cache effect
156
+
157
+ Append-only; escalation text follows the retained prefix and does not invalidate cached entries.
158
+
159
+ ## Known Limitations and Deferred Work
160
+
161
+ <a id="known-limitations-and-deferred-work"></a>
162
+
163
+
164
+ These limits define when the seam is a poor fit or needs special operational care. They are current package constraints, not a general sandbox comparison or a task backlog.
165
+
166
+ - **File effects are the whole policy vocabulary** — the seam expresses no network, process, syscall, device, or credential restrictions.
167
+ - **Same-world confinement only** — containers, microVMs, and remote execution require replacing capability implementations rather than adding a provider here.
168
+ - **Denial reporting is a stderr dialect** — the seam returns backend signatures instead of a typed runtime denial channel, so consumers that need classification infer it from the child process's output.
169
+ - **Runner diagnostics are in-band** — exit status plus stderr evidence cannot prove which process wrote a matching line, so a confined child that deliberately mimics its runner can cause a false availability or diagnostic attribution; this cannot bypass confinement, and an out-of-band runner-status channel is deferred.
170
+ - **One provider per context** — composing different sandbox mechanisms simultaneously requires a provider-level ladder or separate Kylin contexts; callers choose policy per call, not backend identity.
171
+
172
+ <a id="dev-note"></a>
173
+ ### Dev Note
174
+
175
+ <details>
176
+ <summary>Working context for maintainers — click to expand</summary>
177
+
178
+ This Dev Note is working context for maintainers: undecided directions and open questions. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
179
+
180
+ #### Future: consumers and environments
181
+
182
+ The [sandbox decision](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) lists deferred phases — an optional `subagent-acp` consumer that confines child agents (unconfined default) and environment-coherent capability group examples. Neither is decided; the Windows chain that note listed as deferred has since shipped through the ACL restricted-token rung of `sandbox-local`.
183
+
184
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,184 @@
1
+ ---
2
+ description: "面向用户与维护者的进程沙箱服务约定,用于组合、使用或扩展与宿主共享文件系统和内核的子进程限制机制。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @qilin-agent/sandbox
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 使用 `qilin-sandbox`,可以让子进程及其派生的所有进程在逐调用文件访问策略下运行。命令可以禁止写入(`read-only`)、只写入工作区(`workspace-write`),或不受限制地运行(`danger-full-access`)。无法强制执行所请求的模式时,调用以 `SANDBOX_UNAVAILABLE` 失败,绝不会不受限制地运行。调用被拒绝后,模型可以请求一个严格更宽的模式,交由人类批准一次。这种限制只适用于与宿主共享内核和文件系统的进程;需要隔离整个环境时,请使用容器、microVM 或远程执行器。
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
+ 将此服务与一个后端和一个受限消费方组合,消费方运行的每条命令都会在你解析的策略下执行——你只看到隔离结果及其强制执行完整度,永远看不到 runner。
29
+
30
+ ### 何时选择
31
+
32
+ 当组合需要在宿主机上隔离子进程时选择本包:本地后端与受限执行器都实现这个约定,因此在 `ctx.sandbox` 后挂载 `sandbox-local`、在 `ctx.shell` 后挂载受限执行器,就能使每次 bash 或 pwsh 调用默认在限制下运行。当进程必须在隔离环境中运行时请另选方案——容器、microVM 或远程执行器会替换整个 `ctx.shell`/`ctx.fs` 能力,而不是在这里添加后端。
33
+
34
+ ### 隔离命令
35
+
36
+ 挂载服务、后端与受限执行器;随附的组合由[基础组合包(base bundle)](../../bundle/base/cordis.patch.yml)定义。
37
+
38
+ ```yaml
39
+ - id: sandbox
40
+ name: '@qilin-agent/sandbox-local' # the per-platform backend provider (ctx.sandbox)
41
+ - id: sandbox-policy
42
+ name: '@qilin-agent/sandbox-policy' # the deployment default mode and workspace-write root
43
+ config:
44
+ mode: workspace-write # the deployment default every session starts from
45
+ workspaceRoot: !!js process.cwd() # the boundary workspace-write may write under
46
+ - id: bash
47
+ name: '@qilin-agent/bash-sandbox' # the confined executor behind ctx.shell
48
+ ```
49
+
50
+ 使用该组合时,bash 调用在 `workspace-write` 下受限运行:工作区内写入成功,工作区外写入被拒绝,模型可以通过下面的升权流程恢复。
51
+
52
+ ### 模式与强制执行
53
+
54
+ 模式指明命令可以执行的文件操作;强制执行完整度报告后端对这些操作的管辖程度。
55
+
56
+ | 模式 | 效果 |
57
+ |---|---|
58
+ | `read-only` | 拒绝写入,必需 sink(如 `/dev/null`)除外 |
59
+ | `workspace-write` | 允许写入工作区根目录及后端定义的临时区域 |
60
+ | `danger-full-access` | 绕过隔离;消费方直接 spawn 原始 argv |
61
+
62
+ 强制执行逐调用报告:`full` 表示后端管辖模式承诺的每个文件操作,`partial` 表示活动后端或较旧的内核 ABI 只管辖子集——Windows ACL 档与较旧的 Landlock ABI 是当前的部分强制执行情形,需要绝对边界的消费方可以拒绝或向上暴露它们。
63
+
64
+ ### 被拒绝的调用与升权
65
+
66
+ 受限调用被拒绝时,操作会报告指明模式的拒绝标记——`[sandbox: file access denied under <mode> mode]`——组合声明升权能力时还会给出升权提示。模型可以用 `sandbox_permissions`(足以放行的最窄更宽模式)加 `justification` 重试一次完全相同的调用;用户会看到一次审批提示,可以选择允许一次、拒绝或取消。更宽的模式需要审批,且只作用于该次调用。重复指定调用的生效模式无需审批即可成功;更窄的目标仍然无效。
67
+
68
+ ### 故障关闭行为
69
+
70
+ 没有后端能强制执行所请求的模式时,调用以 `SANDBOX_UNAVAILABLE` 失败,而不是不受限制地运行;错误文本会指明缺失的平台 runner。启动后失败的后端还会报告结构化的 runner 失败签名,因此损坏的沙箱可以与命令失败区分开。
71
+
72
+ -----
73
+
74
+ <a id="understand-the-implementation"></a>
75
+ ## 理解实现
76
+
77
+ <details>
78
+ <summary>实现细节——点击展开</summary>
79
+
80
+ 本节解释约定背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
81
+
82
+ ### 设计理念
83
+
84
+ - **同一执行环境。** 文件系统、子进程与沙箱提供方使用同一文件系统和内核。远端组合替换这三个提供方,并在该环境中异步解析限制。
85
+ - **策略随调用传递。** `SandboxPolicy` 逐调用携带,绝不在提供方上固定:两个消费方可以同时按不同策略隔离,获批的升权重试只是用更宽策略发起的新调用。默认与解析是消费方显式步骤。
86
+ - **故障关闭。** `confine()` 异步返回用于强制执行限制的 argv,或以 `SandboxUnavailableError` 拒绝;绝不允许静默无限制放行,功能探测用于仲裁多 runner 链。
87
+ - **统一的拒绝与升权词汇。** 标记与提示文本以及严格更宽阶梯都放在这里,使 bash 与 fs 家族不会漂移。
88
+
89
+ ### 源码地图
90
+
91
+ | 文件 | 职责 |
92
+ |---|---|
93
+ | [`src/index.ts`](src/index.ts) | 插件入口:`SandboxProvider` 服务、模式/强制执行/策略类型、故障关闭错误 |
94
+ | [`src/escalation.ts`](src/escalation.ts) | 升权词汇:更宽模式阶梯、参数校验、拒绝与提示标记、审批编排 |
95
+ | [`src/roots.ts`](src/roots.ts) | 可写根目录推导,Seatbelt profile 与进程内 fs 栅栏共享 |
96
+
97
+ ### 升权编排
98
+
99
+ 阶梯是封闭表——`read-only` 可升权到 `workspace-write` 或 `danger-full-access`,`workspace-write` 只能升权到 `danger-full-access`——在执行时检查,绝不写入工具 schema,schema 的枚举保持封闭的目标词汇。[`approveEscalation`](src/escalation.ts) 在请求重复当前模式时无需审批就返回该模式,拒绝更窄或不支持的目标,并为更宽模式请求审批。调用方先校验 `sandbox_permissions`/`justification` 配对。
100
+
101
+ ### 可写根目录
102
+
103
+ `workspace-write` 意味着「工作区根目录加宿主临时区域」:`writableRoots` 以规范化方式推导该白名单,解析符号链接并去重,使 Seatbelt profile 与进程内 fs 栅栏授予完全相同的根目录。
104
+
105
+ </details>
106
+
107
+ -----
108
+
109
+ <a id="further-exploration"></a>
110
+ ## 进一步探索
111
+
112
+ 先从子系统参考文档了解穷尽式约定,再看实现它的后端、消费方与策略来源。
113
+
114
+ - [进程沙箱子系统](../../../docs/subsystems/sandbox.zh.md)——完整词汇、逐调用策略与分类方言。
115
+ - [子进程沙箱决策](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)——能力边界、升权设计与延期阶段。
116
+ - [本地沙箱后端](../sandbox-local/README.zh.md)——`ctx.sandbox` 背后的各平台 runner。
117
+ - [Bash 沙箱执行器](../../shell/bash-sandbox/README.zh.md)——受限的 bash 消费方。
118
+ - [沙箱策略包](../sandbox-policy/README.zh.md)——逐调用模式与工作区根目录的来源。
119
+
120
+ -----
121
+
122
+ <a id="model-experience"></a>
123
+ ## 模型体验
124
+
125
+ ### 间接的限制错误
126
+
127
+ #### 模型看到什么
128
+
129
+ 通过 [`qilin-bash-sandbox`](../../shell/bash-sandbox/README.zh.md) 和 [`qilin-tool-bash`](../../shell/tool-bash/README.zh.md),请求的受限模式没有可用后端时会产生错误码 `SANDBOX_UNAVAILABLE` 及下方精确错误;执行期 runner 失败会追加 ` Runner failure: <detail>`。
130
+
131
+ ##### 精确错误
132
+
133
+ ```markdown
134
+ sandbox mode "<mode>" is requested but no sandbox backend is usable on this host; refusing to run the command unconfined. Install bubblewrap or run a Landlock-enforcing kernel (Linux), ensure sandbox-exec is usable (macOS), or ensure the ACL restricted-token runner can start (Windows) — otherwise switch the consumer to danger-full-access.
135
+ ```
136
+
137
+ #### Token 影响
138
+
139
+ 条件性错误文本对该次调用可见,并保留在历史中直到压缩(compaction)。
140
+
141
+ #### KV Cache 影响
142
+
143
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
144
+
145
+ ### 升权请求与结果
146
+
147
+ #### 模型看到什么
148
+
149
+ 被拒绝的调用会呈现标记 `[sandbox: file access denied under <mode> mode]`,组合声明升权能力时还会呈现提示 `[sandbox: escalation available — retry this exact <subject> once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`。重试携带 `sandbox_permissions` 与 `justification`;用户的 `allowed-once`/`rejected`/`cancelled` 决定成为该调用的结果文本。
150
+
151
+ #### Token 影响
152
+
153
+ 只有被拒绝调用的错误与任何升权结果文本可见;两者都会保留在历史中直到压缩。
154
+
155
+ #### KV Cache 影响
156
+
157
+ 仅追加;升权文本位于保留前缀之后,不会使已缓存条目失效。
158
+
159
+ ## 已知限制与延期工作
160
+
161
+ <a id="known-limitations-and-deferred-work"></a>
162
+
163
+
164
+ 这些限制说明该 seam 何时不合适,或何时需要特别运维。它们是当前包约束,不是通用沙箱对比或任务积压。
165
+
166
+ - **文件操作是完整的策略词汇**——该 seam 不表达网络、进程、系统调用、设备或凭据限制。
167
+ - **只支持与宿主共享文件系统和内核的限制**——容器、microVM 与远程执行需要替换能力实现,而不是在此添加提供方。
168
+ - **拒绝报告是一种 stderr 方言**——该 seam 返回后端签名,而非类型化运行时拒绝通道,需要分类的消费方必须从子进程输出推断。
169
+ - **Runner 诊断使用带内通道**——退出状态与 stderr 证据无法证明匹配行由哪个进程写入,因此故意模仿 runner 的受限子进程可能造成错误的可用性或诊断归因;这无法绕过隔离,带外 runner 状态通道暂缓实现。
170
+ - **每个上下文只有一个提供方**——同时组合不同沙箱机制需要提供方级阶梯或独立 Kylin 上下文;调用方逐调用选择策略,而非后端标识。
171
+
172
+ <a id="dev-note"></a>
173
+ ### 开发备注
174
+
175
+ <details>
176
+ <summary>维护者的工作上下文——点击展开</summary>
177
+
178
+ 本开发备注是维护者的工作上下文:未决方向与开放问题。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
179
+
180
+ #### 未来:消费方与环境
181
+
182
+ [沙箱决策](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)列出延期阶段——可选的 `subagent-acp` 消费方(隔离子 agent(智能体),默认不隔离)与环境一致的能力组示例。两者均未决定;该笔记列为延期的 Windows 链已通过 `sandbox-local` 的 ACL 受限令牌档交付。
183
+
184
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,279 @@
1
+ import { Service } from "@qilin-agent/kylin";
2
+ import { HarnessError } from "@qilin-agent/llm";
3
+ import { assertNever } from "@qilin-agent/util-values";
4
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ //#region lib/types/escalation.js
7
+ /**
8
+ * The escalation vocabulary and choreography shared by every sandbox-enforcing
9
+ * tool family (`@qilin-agent/tool-bash`, `@qilin-agent/tool-fs`): the
10
+ * strictly-wider ladder, the argument-pairing validation, the model-facing
11
+ * denial/hint markers, and {@link approveEscalation} — the ordered fail-closed
12
+ * sequence that resolves a `sandbox_permissions` request through a
13
+ * user-approval channel BEFORE anything executes. One home keeps the two
14
+ * families' approval ordering and verbatim error texts from drifting apart.
15
+ *
16
+ * The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}),
17
+ * not the approval service type: the tool layer — which owns the agent, the
18
+ * call id, and the tool name — closes over `ctx.approval.request(...)` and
19
+ * hands the closure down, so this package never depends on the approval or
20
+ * agent packages.
21
+ *
22
+ * @module qilin-sandbox/escalation
23
+ */
24
+ /**
25
+ * The strictly-wider table: what a call whose effective mode is the key may
26
+ * escalate TO. Checked at EXECUTION, never baked into a tool schema — the
27
+ * schema's enum is {@link ESCALATION_TARGETS}, because schemas are
28
+ * registry-global while the effective mode is per-call truth.
29
+ */
30
+ const WIDER_MODES = {
31
+ "read-only": ["workspace-write", "danger-full-access"],
32
+ "workspace-write": ["danger-full-access"]
33
+ };
34
+ /**
35
+ * The closed escalation-target vocabulary — every mode a call could ever
36
+ * escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
37
+ * whenever the mounted capability confines: cutting the enum down to the modes
38
+ * wider than the composition's DEFAULT would strand a session whose effective
39
+ * mode sits below it (a `danger-full-access` default would advertise nothing
40
+ * while a narrower-switched session stays confined with no lever).
41
+ */
42
+ const ESCALATION_TARGETS = ["workspace-write", "danger-full-access"];
43
+ /**
44
+ * Validate the escalation argument pairing a tool schema cannot express:
45
+ * `sandbox_permissions` and `justification` travel together — an approval
46
+ * prompt without a reason, or a reason driving nothing, is a malformed ask —
47
+ * and the justification must be a non-empty sentence.
48
+ * @param sandboxPermissions - the raw `sandbox_permissions` argument, if given.
49
+ * @param justification - the raw `justification` argument, if given.
50
+ */
51
+ function validateEscalationArgs(sandboxPermissions, justification) {
52
+ if (sandboxPermissions !== void 0 && justification === void 0) throw new Error("invalid escalation: sandbox_permissions requires a justification");
53
+ if (justification !== void 0 && sandboxPermissions === void 0) throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
54
+ if (justification !== void 0 && justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
55
+ }
56
+ /**
57
+ * The model-facing denial marker — the one vocabulary both enforcing families
58
+ * teach and report, so the model recognizes a policy denial identically
59
+ * whether the kernel refused a bash file effect or the filesystem provider's
60
+ * fence refused a mutation.
61
+ * @param mode - the mode the denied call ran under.
62
+ * @returns the marker line, exactly as the model sees it.
63
+ */
64
+ function sandboxDenialMarker(mode) {
65
+ return `[sandbox: file access denied under ${mode} mode]`;
66
+ }
67
+ /**
68
+ * The same-turn escalation hint that rides a denial when the composition
69
+ * advertises the escalation fields — the nudge lives at the decision point so
70
+ * the sanctioned retry does not depend on the model recalling the tool
71
+ * description.
72
+ * @param subject - the family's noun for the denied action (`command` for
73
+ * bash, `operation` for a filesystem mutation).
74
+ * @returns the hint line, exactly as the model sees it.
75
+ */
76
+ function escalationHintMarker(subject) {
77
+ return `[sandbox: escalation available — retry this exact ${subject} once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`;
78
+ }
79
+ /**
80
+ * Resolve a sandbox permission request before execution. Repeating the call's
81
+ * effective mode returns it without approval. A strictly wider mode requires
82
+ * approval and applies only to this call. Narrower or unsupported targets,
83
+ * missing approval services or agents for widening, and non-grant outcomes
84
+ * throw before execution.
85
+ * @param request - the escalation to judge (see {@link EscalationRequest}).
86
+ * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
87
+ * @returns the granted mode, consumed by the one call that asked.
88
+ */
89
+ async function approveEscalation(request, approval) {
90
+ const { requestedMode: mode, effectiveMode, justification, subject } = request;
91
+ if (mode === effectiveMode) return effectiveMode;
92
+ if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode)) throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
93
+ if (approval.approver === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`);
94
+ if (approval.agent === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`);
95
+ const outcome = await approval.approver.request({
96
+ agent: approval.agent,
97
+ toolName: approval.toolName,
98
+ callId: approval.callId,
99
+ reason: `escalate sandbox to ${mode}: ${justification}`,
100
+ displayReason: {
101
+ en: `Allow this operation with ${mode} permissions: ${justification}`,
102
+ zh: `允许本次操作使用 ${mode} 权限:${justification}`
103
+ },
104
+ ...approval.signal ? { signal: approval.signal } : {}
105
+ });
106
+ switch (outcome) {
107
+ case "allowed-once": return mode;
108
+ case "rejected": throw new Error(`the user rejected escalating this ${subject} to "${mode}"`);
109
+ case "cancelled": throw new Error(`approval for escalating to "${mode}" was cancelled`);
110
+ case "unavailable": throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`);
111
+ default: return assertNever(outcome, "EscalationOutcome");
112
+ }
113
+ }
114
+ //#endregion
115
+ //#region lib/types/roots.js
116
+ /**
117
+ * The writable-root derivation shared by every enforcement dialect that
118
+ * expresses a mode as a canonical allow-list: `workspace-write` means "the
119
+ * workspace root plus the platform temp areas", and this module is that
120
+ * meaning's one home. The Seatbelt profile
121
+ * (`@qilin-agent/sandbox-local`) and the in-process filesystem fence
122
+ * (`@qilin-agent/fs-sandbox`) both derive their allow-list here, so "the
123
+ * write tool cannot write /tmp but bash can" asymmetries cannot arise between
124
+ * them. The bwrap and Landlock dialects keep their own grant spellings (an
125
+ * ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner
126
+ * differences recorded in the sandbox RFC — with parity pinned by test.
127
+ *
128
+ * @module qilin-sandbox/roots
129
+ */
130
+ /**
131
+ * Resolve a granted root to the path the enforcement layer actually compares:
132
+ * canonical (symlinks resolved), because both Seatbelt filters and the fs
133
+ * fence's containment check match resolved paths — `/tmp` IS `/private/tmp`
134
+ * on darwin, and an as-spelled grant would match nothing.
135
+ * @param path - the root as configured or platform-reported.
136
+ * @returns the canonical path, or the spelling as-is when resolution fails
137
+ * (a missing root matches nothing until it exists — the conservative
138
+ * outcome; inventing a fallback would grant a path the caller never named).
139
+ */
140
+ function canonicalPath(path) {
141
+ try {
142
+ return realpathSync.native(path);
143
+ } catch {
144
+ return path;
145
+ }
146
+ }
147
+ /**
148
+ * The roots one confined execution may WRITE under — the mode's meaning as a
149
+ * canonical, deduplicated allow-list. `read-only` allows nothing;
150
+ * `workspace-write` allows the policy's workspace root, the host `/tmp`, and
151
+ * the per-user platform temp dir (`os.tmpdir()` — the real temp area for
152
+ * mkstemp-family tools; omitting it would deny what the mode promises).
153
+ * @param policy - the file-effect policy to derive the allow-list from.
154
+ * @returns the canonical writable roots; empty exactly under `read-only`.
155
+ */
156
+ function writableRoots(policy) {
157
+ if (policy.mode !== "workspace-write") return [];
158
+ return [...new Set([
159
+ policy.workspaceRoot,
160
+ "/tmp",
161
+ tmpdir()
162
+ ].map(canonicalPath))];
163
+ }
164
+ //#endregion
165
+ //#region lib/types/diagnostics.js
166
+ /** Classify observed sandbox runner failures and file-effect denials. */
167
+ /** Node-local spawn codes proven to identify executable resolution or permission failure. */
168
+ const EXECUTABLE_SPAWN_CODES = new Set(["EACCES", "ENOENT"]);
169
+ /** Whether the caller-owned spawn cwd can be entered. */
170
+ function isUsableWorkdir(path) {
171
+ try {
172
+ if (!statSync(path).isDirectory()) return false;
173
+ accessSync(path, constants.X_OK);
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * Attribute only Node ENOENT/EACCES failures whose error path equals argv[0]
181
+ * after independently ruling out the caller-owned cwd. A supplied error path
182
+ * must exactly identify the runner; without one, the syscall must. With a
183
+ * usable cwd, these codes describe resolution or execute permission for that
184
+ * argv[0] or its shebang interpreter.
185
+ * The workdir is checked at classification time, not atomically with spawn;
186
+ * concurrent path replacement may change attribution but cannot permit an
187
+ * unconfined execution.
188
+ * @param error - the original spawn rejection.
189
+ * @param runnerProgram - provider argv[0], the executable that establishes confinement.
190
+ * @param workdir - the caller-owned spawn cwd, checked independently for usability.
191
+ * @returns whether the rejection has executable-specific runner evidence.
192
+ */
193
+ function isRunnerSpawnFailure(error, runnerProgram, workdir) {
194
+ if (runnerProgram === void 0 || !isUsableWorkdir(workdir)) return false;
195
+ if (typeof error !== "object" || error === null) return false;
196
+ const { code, path, syscall } = error;
197
+ if (typeof code !== "string" || !EXECUTABLE_SPAWN_CODES.has(code)) return false;
198
+ if (typeof syscall !== "string") return false;
199
+ const exactSyscall = `spawn ${runnerProgram}`;
200
+ if (path === void 0) return syscall === exactSyscall;
201
+ if (typeof path !== "string" || path.length === 0 || path !== runnerProgram) return false;
202
+ return syscall === "spawn" || syscall === exactSyscall;
203
+ }
204
+ /**
205
+ * Classify one settled process against the selected backend's structured
206
+ * runner-failure rules. Each rule requires a nonzero exit, its optional
207
+ * exit-code gate, and a fatal signature on one stderr line after exact
208
+ * informational lines are excluded.
209
+ * @param exitCode - process exit code; null means signal termination.
210
+ * @param stderr - collected stderr text, left unchanged.
211
+ * @param rules - structured runner-failure rules from the active wrap.
212
+ * @returns the first matching fatal line, or undefined when evidence is insufficient.
213
+ */
214
+ function classifyRunnerFailure(exitCode, stderr, rules) {
215
+ if (exitCode === null || exitCode === 0) return void 0;
216
+ const lines = stderr.split(/\r?\n/);
217
+ for (const rule of rules) {
218
+ if (rule.allowedExitCodes !== void 0 && !rule.allowedExitCodes.includes(exitCode)) continue;
219
+ const informationalLines = new Set((rule.informationalLines ?? []).map((line) => line.toLowerCase()));
220
+ const fatalSignatures = rule.fatalSignatures.filter((signature) => signature.trim().length > 0).map((signature) => signature.toLowerCase());
221
+ for (const line of lines) {
222
+ const lowered = line.toLowerCase();
223
+ if (informationalLines.has(lowered)) continue;
224
+ if (fatalSignatures.some((signature) => lowered.includes(signature))) return { detail: line };
225
+ }
226
+ }
227
+ }
228
+ /**
229
+ * Match a non-zero exit against case-insensitive stderr signatures.
230
+ * @param exitCode - process exit code; null means signal termination.
231
+ * @param stderr - collected stderr text.
232
+ * @param signatures - substrings identifying the selected backend's dialect.
233
+ * @returns whether this is a non-zero exit whose stderr matches a signature.
234
+ */
235
+ function matchesSignature(exitCode, stderr, signatures) {
236
+ if (exitCode === null || exitCode === 0) return false;
237
+ const lowered = stderr.toLowerCase();
238
+ return signatures.some((signature) => lowered.includes(signature.toLowerCase()));
239
+ }
240
+ //#endregion
241
+ //#region lib/types/index.js
242
+ /**
243
+ * Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a
244
+ * host-path file policy. Containers, microVMs, and remote execution replace the
245
+ * surrounding capability seam instead; this service shares the host kernel and filesystem.
246
+ * @module @qilin-agent/sandbox
247
+ */
248
+ /**
249
+ * Error code for a requested confined mode when no backend is usable. The
250
+ * provider fails closed, and `HarnessError` carries the code through
251
+ * `tool/result` so callers can distinguish missing confinement from command
252
+ * failure.
253
+ */
254
+ const SANDBOX_UNAVAILABLE = "SANDBOX_UNAVAILABLE";
255
+ /**
256
+ * Thrown when {@link SandboxProvider.confine} cannot enforce the requested
257
+ * mode. Carries {@link SANDBOX_UNAVAILABLE} through the structured error
258
+ * channel.
259
+ */
260
+ var SandboxUnavailableError = class extends HarnessError {
261
+ constructor(mode, detail) {
262
+ super(`sandbox mode "${mode}" is requested but no sandbox backend is usable on this host; refusing to run the command unconfined. Install bubblewrap or run a Landlock-enforcing kernel (Linux), ensure sandbox-exec is usable (macOS), or ensure the ACL restricted-token runner can start (Windows) — otherwise switch the consumer to danger-full-access.` + (detail === void 0 ? "" : ` Runner failure: ${detail}`), SANDBOX_UNAVAILABLE);
263
+ this.name = "SandboxUnavailableError";
264
+ }
265
+ };
266
+ /**
267
+ * Abstract process-sandbox service. {@link confine} must return enforcing argv
268
+ * or fail closed at wrap or runner-execution time; silent unconfined passthrough
269
+ * is forbidden. Functional probes arbitrate multi-runner chains and may be
270
+ * skipped for a sole candidate, whose own refusal remains the fail-closed end.
271
+ */
272
+ var SandboxProvider = class extends Service {
273
+ /* v8 ignore next -- abstract service construction is covered through concrete provider packages. */
274
+ constructor(ctx) {
275
+ super(ctx, "sandbox");
276
+ }
277
+ };
278
+ //#endregion
279
+ export { ESCALATION_TARGETS, SANDBOX_UNAVAILABLE, SandboxProvider, SandboxProvider as default, SandboxUnavailableError, WIDER_MODES, approveEscalation, canonicalPath, classifyRunnerFailure, escalationHintMarker, isRunnerSpawnFailure, matchesSignature, sandboxDenialMarker, validateEscalationArgs, writableRoots };
@@ -0,0 +1,42 @@
1
+ import type { RunnerFailureRule } from './index.ts';
2
+ /**
3
+ * Attribute only Node ENOENT/EACCES failures whose error path equals argv[0]
4
+ * after independently ruling out the caller-owned cwd. A supplied error path
5
+ * must exactly identify the runner; without one, the syscall must. With a
6
+ * usable cwd, these codes describe resolution or execute permission for that
7
+ * argv[0] or its shebang interpreter.
8
+ * The workdir is checked at classification time, not atomically with spawn;
9
+ * concurrent path replacement may change attribution but cannot permit an
10
+ * unconfined execution.
11
+ * @param error - the original spawn rejection.
12
+ * @param runnerProgram - provider argv[0], the executable that establishes confinement.
13
+ * @param workdir - the caller-owned spawn cwd, checked independently for usability.
14
+ * @returns whether the rejection has executable-specific runner evidence.
15
+ */
16
+ export declare function isRunnerSpawnFailure(error: unknown, runnerProgram: string | undefined, workdir: string): boolean;
17
+ /** Fatal runner evidence retained for infrastructure-error detail. */
18
+ interface RunnerFailureMatch {
19
+ /** The original stderr line that matched a fatal signature. */
20
+ detail: string;
21
+ }
22
+ /**
23
+ * Classify one settled process against the selected backend's structured
24
+ * runner-failure rules. Each rule requires a nonzero exit, its optional
25
+ * exit-code gate, and a fatal signature on one stderr line after exact
26
+ * informational lines are excluded.
27
+ * @param exitCode - process exit code; null means signal termination.
28
+ * @param stderr - collected stderr text, left unchanged.
29
+ * @param rules - structured runner-failure rules from the active wrap.
30
+ * @returns the first matching fatal line, or undefined when evidence is insufficient.
31
+ */
32
+ export declare function classifyRunnerFailure(exitCode: number | null, stderr: string, rules: readonly RunnerFailureRule[]): RunnerFailureMatch | undefined;
33
+ /**
34
+ * Match a non-zero exit against case-insensitive stderr signatures.
35
+ * @param exitCode - process exit code; null means signal termination.
36
+ * @param stderr - collected stderr text.
37
+ * @param signatures - substrings identifying the selected backend's dialect.
38
+ * @returns whether this is a non-zero exit whose stderr matches a signature.
39
+ */
40
+ export declare function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean;
41
+ export {};
42
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The escalation vocabulary and choreography shared by every sandbox-enforcing
3
+ * tool family (`@qilin-agent/tool-bash`, `@qilin-agent/tool-fs`): the
4
+ * strictly-wider ladder, the argument-pairing validation, the model-facing
5
+ * denial/hint markers, and {@link approveEscalation} — the ordered fail-closed
6
+ * sequence that resolves a `sandbox_permissions` request through a
7
+ * user-approval channel BEFORE anything executes. One home keeps the two
8
+ * families' approval ordering and verbatim error texts from drifting apart.
9
+ *
10
+ * The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}),
11
+ * not the approval service type: the tool layer — which owns the agent, the
12
+ * call id, and the tool name — closes over `ctx.approval.request(...)` and
13
+ * hands the closure down, so this package never depends on the approval or
14
+ * agent packages.
15
+ *
16
+ * @module qilin-sandbox/escalation
17
+ */
18
+ import type { SandboxMode } from './index.ts';
19
+ /**
20
+ * The strictly-wider table: what a call whose effective mode is the key may
21
+ * escalate TO. Checked at EXECUTION, never baked into a tool schema — the
22
+ * schema's enum is {@link ESCALATION_TARGETS}, because schemas are
23
+ * registry-global while the effective mode is per-call truth.
24
+ */
25
+ export declare const WIDER_MODES: Record<string, readonly SandboxMode[]>;
26
+ /**
27
+ * The closed escalation-target vocabulary — every mode a call could ever
28
+ * escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
29
+ * whenever the mounted capability confines: cutting the enum down to the modes
30
+ * wider than the composition's DEFAULT would strand a session whose effective
31
+ * mode sits below it (a `danger-full-access` default would advertise nothing
32
+ * while a narrower-switched session stays confined with no lever).
33
+ */
34
+ export declare const ESCALATION_TARGETS: readonly SandboxMode[];
35
+ /**
36
+ * Validate the escalation argument pairing a tool schema cannot express:
37
+ * `sandbox_permissions` and `justification` travel together — an approval
38
+ * prompt without a reason, or a reason driving nothing, is a malformed ask —
39
+ * and the justification must be a non-empty sentence.
40
+ * @param sandboxPermissions - the raw `sandbox_permissions` argument, if given.
41
+ * @param justification - the raw `justification` argument, if given.
42
+ */
43
+ export declare function validateEscalationArgs(sandboxPermissions: string | undefined, justification: string | undefined): void;
44
+ /**
45
+ * The model-facing denial marker — the one vocabulary both enforcing families
46
+ * teach and report, so the model recognizes a policy denial identically
47
+ * whether the kernel refused a bash file effect or the filesystem provider's
48
+ * fence refused a mutation.
49
+ * @param mode - the mode the denied call ran under.
50
+ * @returns the marker line, exactly as the model sees it.
51
+ */
52
+ export declare function sandboxDenialMarker(mode: SandboxMode): string;
53
+ /**
54
+ * The same-turn escalation hint that rides a denial when the composition
55
+ * advertises the escalation fields — the nudge lives at the decision point so
56
+ * the sanctioned retry does not depend on the model recalling the tool
57
+ * description.
58
+ * @param subject - the family's noun for the denied action (`command` for
59
+ * bash, `operation` for a filesystem mutation).
60
+ * @returns the hint line, exactly as the model sees it.
61
+ */
62
+ export declare function escalationHintMarker(subject: string): string;
63
+ /**
64
+ * The closed outcome vocabulary of one escalation ask — structurally identical
65
+ * to the approval seam's `ApprovalOutcome` so an `ApprovalService.request`
66
+ * return is assignable without this package importing it.
67
+ */
68
+ export type EscalationOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
69
+ /**
70
+ * The minimal approval-request shape {@link approveEscalation} needs —
71
+ * structurally the approval seam's `ApprovalService`, generic over the agent
72
+ * type `A` and call-id type `C` so this package resolves escalations through
73
+ * `ctx.approval` without importing the approval or agent packages (the tool
74
+ * layer infers `A`/`C` as its own `Agent`/`ToolCallId`).
75
+ */
76
+ export interface EscalationApprover<A = object, C = string> {
77
+ /**
78
+ * Ask the human to approve one action, resolving to a closed outcome.
79
+ * @param req - the audit request with optional localized displayReason and presentation lifetime signal.
80
+ * @returns the human's decision as a closed {@link EscalationOutcome}.
81
+ */
82
+ request(req: {
83
+ agent: A;
84
+ toolName: string;
85
+ callId: C;
86
+ reason: string;
87
+ displayReason?: {
88
+ readonly en: string;
89
+ readonly [locale: string]: string;
90
+ };
91
+ signal?: AbortSignal;
92
+ }): Promise<EscalationOutcome>;
93
+ }
94
+ /**
95
+ * The approval ingredients an escalating tool hands {@link approveEscalation}:
96
+ * the approval requester (`ctx.approval`, or `undefined` when none is
97
+ * composed), the calling agent (or `undefined` for an agent-less execution),
98
+ * and the call's identity. The tool layer holds all of these; this package
99
+ * only judges them.
100
+ */
101
+ export interface EscalationApproval<A = object, C = string> {
102
+ /** The approval requester (`ctx.approval`), or `undefined` when none is composed. */
103
+ approver: EscalationApprover<A, C> | undefined;
104
+ /** The calling agent, or `undefined` for an agent-less execution (fails closed). */
105
+ agent: A | undefined;
106
+ /** The tool-call id the approval prompt attaches to. */
107
+ callId: C;
108
+ /** The tool name recorded on the approval request. */
109
+ toolName: string;
110
+ /** The tool-execution abort signal the approval request rides, when present. */
111
+ signal?: AbortSignal;
112
+ }
113
+ /** One escalation request, as {@link approveEscalation} judges it. */
114
+ export interface EscalationRequest {
115
+ /** The requested target mode (schema-pinned to {@link ESCALATION_TARGETS} when advertised). */
116
+ requestedMode: string;
117
+ /** The model's one-sentence reason, shown verbatim to the user inside the audit reason. */
118
+ justification: string;
119
+ /** The call's effective mode (session override ?? composition default); repeating it needs no approval. */
120
+ effectiveMode: SandboxMode;
121
+ /** The family's noun for the escalated action in user-facing texts (`command` for bash, `operation` for fs). */
122
+ subject: string;
123
+ }
124
+ /**
125
+ * Resolve a sandbox permission request before execution. Repeating the call's
126
+ * effective mode returns it without approval. A strictly wider mode requires
127
+ * approval and applies only to this call. Narrower or unsupported targets,
128
+ * missing approval services or agents for widening, and non-grant outcomes
129
+ * throw before execution.
130
+ * @param request - the escalation to judge (see {@link EscalationRequest}).
131
+ * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
132
+ * @returns the granted mode, consumed by the one call that asked.
133
+ */
134
+ export declare function approveEscalation<A, C>(request: EscalationRequest, approval: EscalationApproval<A, C>): Promise<SandboxMode>;
135
+ //# sourceMappingURL=escalation.d.ts.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a
3
+ * host-path file policy. Containers, microVMs, and remote execution replace the
4
+ * surrounding capability seam instead; this service shares the host kernel and filesystem.
5
+ * @module @qilin-agent/sandbox
6
+ */
7
+ import { Context, Service } from '@qilin-agent/kylin';
8
+ import { HarnessError } from '@qilin-agent/llm';
9
+ import type { SessionId } from '@qilin-agent/session';
10
+ export { ESCALATION_TARGETS, WIDER_MODES, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs, } from './escalation.ts';
11
+ export type { EscalationApproval, EscalationApprover, EscalationOutcome, EscalationRequest } from './escalation.ts';
12
+ export { canonicalPath, writableRoots } from './roots.ts';
13
+ /**
14
+ * File-effect policy for confined processes. `read-only` permits only required
15
+ * sinks such as `/dev/null`; `workspace-write` also permits the workspace and a
16
+ * backend-defined temp area; `danger-full-access` bypasses confinement. Network
17
+ * and process visibility are outside this vocabulary.
18
+ */
19
+ export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
20
+ /** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */
21
+ export type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>;
22
+ /**
23
+ * The complete file-effect policy resolved for one capability call. The root
24
+ * is carried even under modes that do not consume it so callers can resolve
25
+ * policy once before choosing the enforcement path.
26
+ */
27
+ export interface SandboxExecutionPolicy {
28
+ /** The file-effect mode this execution runs under. */
29
+ mode: SandboxMode;
30
+ /** Absolute root directory `workspace-write` may write under. */
31
+ workspaceRoot: string;
32
+ /**
33
+ * Opaque identity of the calling session (the branded `qilin-session`
34
+ * SessionId). Backends key per-session state off it (e.g. windows-acl gives
35
+ * each live session/workspace pair a random private temp directory and SID,
36
+ * while the workspace SID and standing grant remain per-workspace); absent
37
+ * for agentless calls, which fall back to per-call backend state.
38
+ */
39
+ sessionId?: SessionId;
40
+ }
41
+ /**
42
+ * Enforcement completeness for this host. `partial` means an active backend or
43
+ * older kernel ABI cannot govern every promised file effect; callers requiring
44
+ * an absolute boundary must not treat it as `full`.
45
+ */
46
+ export type SandboxEnforcement = 'full' | 'partial';
47
+ /**
48
+ * What one confined execution is allowed to touch — carried PER CALL, not
49
+ * fixed on the provider: two consumers may confine under different policies
50
+ * at the same instant (bash under `read-only` while a confined child agent
51
+ * needs its state directory writable), and an approved escalated retry is a
52
+ * new call with a wider policy. Defaulting/resolution is an explicit step at
53
+ * the consumer boundary; the provider treats the policy as fully specified.
54
+ */
55
+ export interface SandboxPolicy extends SandboxExecutionPolicy {
56
+ /** The file-effect mode this execution runs under. */
57
+ mode: ConfinedSandboxMode;
58
+ }
59
+ /**
60
+ * Evidence that identifies a sandbox runner failing before it executes the
61
+ * wrapped command. A consumer first applies {@link allowedExitCodes} when
62
+ * present, removes {@link informationalLines} by case-insensitive exact line
63
+ * equality, then matches {@link fatalSignatures} case-insensitively within
64
+ * each remaining stderr line. Exit status alone never proves runner failure.
65
+ */
66
+ export interface RunnerFailureRule {
67
+ /** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
68
+ allowedExitCodes?: readonly number[];
69
+ /** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
70
+ fatalSignatures: readonly string[];
71
+ /** Benign stderr lines excluded by exact full-line equality before fatal matching. */
72
+ informationalLines?: readonly string[];
73
+ }
74
+ /**
75
+ * A {@link SandboxProvider.confine} result: the argv to spawn in place of
76
+ * the caller's own, plus the enforcement completeness the selected backend
77
+ * achieves for it.
78
+ */
79
+ export interface ConfinedArgv {
80
+ /** The wrapped argv (runner, profile, separator, then the caller's argv). */
81
+ argv: string[];
82
+ /** How completely the selected backend enforces the policy's file effects. */
83
+ enforcement: SandboxEnforcement;
84
+ /**
85
+ * The selected backend's denial DIALECT: the case-insensitive stderr
86
+ * substrings a file effect denied by THIS backend produces (EROFS text
87
+ * under bwrap's read-only binds, EACCES under Landlock, EPERM under
88
+ * Seatbelt). A consumer that infers denials from a failed run's stderr
89
+ * matches against exactly these rather than a cross-backend union — the
90
+ * union claims denials a given backend never produces.
91
+ */
92
+ denialSignatures: readonly string[];
93
+ /**
94
+ * Structured runner-failure evidence rules. Consumers require a matching
95
+ * fatal stderr line (after informational exclusions) and any rule-specific
96
+ * exit-code gate before checking denial signatures: runner failure means the
97
+ * command never ran, while denial means confinement worked and blocked it.
98
+ */
99
+ runnerFailureRules: readonly RunnerFailureRule[];
100
+ }
101
+ /**
102
+ * Error code for a requested confined mode when no backend is usable. The
103
+ * provider fails closed, and `HarnessError` carries the code through
104
+ * `tool/result` so callers can distinguish missing confinement from command
105
+ * failure.
106
+ */
107
+ export declare const SANDBOX_UNAVAILABLE = "SANDBOX_UNAVAILABLE";
108
+ /**
109
+ * Thrown when {@link SandboxProvider.confine} cannot enforce the requested
110
+ * mode. Carries {@link SANDBOX_UNAVAILABLE} through the structured error
111
+ * channel.
112
+ */
113
+ export declare class SandboxUnavailableError extends HarnessError {
114
+ constructor(mode: ConfinedSandboxMode, detail?: string);
115
+ }
116
+ declare module '@qilin-agent/kylin' {
117
+ interface Context {
118
+ sandbox: SandboxProvider;
119
+ }
120
+ }
121
+ /**
122
+ * Abstract process-sandbox service. {@link confine} must return enforcing argv
123
+ * or fail closed at wrap or runner-execution time; silent unconfined passthrough
124
+ * is forbidden. Functional probes arbitrate multi-runner chains and may be
125
+ * skipped for a sole candidate, whose own refusal remains the fail-closed end.
126
+ */
127
+ export declare abstract class SandboxProvider extends Service {
128
+ constructor(ctx: Context);
129
+ /**
130
+ * Wrap `argv` so it executes confined under `policy` on this host; the
131
+ * caller spawns the returned argv in place of its own.
132
+ * @param argv - the exact argv the caller is about to spawn (program plus
133
+ * arguments), NOT a shell string — a shell-shaped consumer passes
134
+ * `['bash', '-c', command]`.
135
+ * @param policy - the file-effect policy this execution runs under,
136
+ * carried per call (see {@link SandboxPolicy}).
137
+ * @param signal - cancellation while the provider resolves the policy and runner.
138
+ * @returns the argv to spawn instead, plus the enforcement completeness
139
+ * the selected backend achieves for it.
140
+ */
141
+ abstract confine(argv: readonly string[], policy: SandboxPolicy, signal?: AbortSignal): Promise<ConfinedArgv>;
142
+ }
143
+ export default SandboxProvider;
144
+ export { classifyRunnerFailure, isRunnerSpawnFailure, matchesSignature } from './diagnostics.ts';
145
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The writable-root derivation shared by every enforcement dialect that
3
+ * expresses a mode as a canonical allow-list: `workspace-write` means "the
4
+ * workspace root plus the platform temp areas", and this module is that
5
+ * meaning's one home. The Seatbelt profile
6
+ * (`@qilin-agent/sandbox-local`) and the in-process filesystem fence
7
+ * (`@qilin-agent/fs-sandbox`) both derive their allow-list here, so "the
8
+ * write tool cannot write /tmp but bash can" asymmetries cannot arise between
9
+ * them. The bwrap and Landlock dialects keep their own grant spellings (an
10
+ * ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner
11
+ * differences recorded in the sandbox RFC — with parity pinned by test.
12
+ *
13
+ * @module qilin-sandbox/roots
14
+ */
15
+ import type { SandboxExecutionPolicy } from './index.ts';
16
+ /**
17
+ * Resolve a granted root to the path the enforcement layer actually compares:
18
+ * canonical (symlinks resolved), because both Seatbelt filters and the fs
19
+ * fence's containment check match resolved paths — `/tmp` IS `/private/tmp`
20
+ * on darwin, and an as-spelled grant would match nothing.
21
+ * @param path - the root as configured or platform-reported.
22
+ * @returns the canonical path, or the spelling as-is when resolution fails
23
+ * (a missing root matches nothing until it exists — the conservative
24
+ * outcome; inventing a fallback would grant a path the caller never named).
25
+ */
26
+ export declare function canonicalPath(path: string): string;
27
+ /**
28
+ * The roots one confined execution may WRITE under — the mode's meaning as a
29
+ * canonical, deduplicated allow-list. `read-only` allows nothing;
30
+ * `workspace-write` allows the policy's workspace root, the host `/tmp`, and
31
+ * the per-user platform temp dir (`os.tmpdir()` — the real temp area for
32
+ * mkstemp-family tools; omitting it would deny what the mode promises).
33
+ * @param policy - the file-effect policy to derive the allow-list from.
34
+ * @returns the canonical writable roots; empty exactly under `read-only`.
35
+ */
36
+ export declare function writableRoots(policy: SandboxExecutionPolicy): string[];
37
+ //# sourceMappingURL=roots.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@qilin-agent/sandbox",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Abstract process-sandbox seam (ctx.sandbox) for the QiLin: same-world confinement vocabulary and the SandboxProvider contract",
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/sandbox/sandbox"
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
+ "./src/*": "./src/*",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/types/**/*.d.ts"
30
+ ],
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "@qilin-agent/kylin": "~4.0.3",
34
+ "@qilin-agent/llm": "3.1.3",
35
+ "@qilin-agent/session": "3.1.3"
36
+ },
37
+ "devDependencies": {
38
+ "@qilin-agent/llm": "3.1.3",
39
+ "@qilin-agent/session": "3.1.3",
40
+ "@qilin-agent/kylin": "~4.0.3"
41
+ },
42
+ "dependencies": {
43
+ "@qilin-agent/util-values": "3.1.3"
44
+ }
6
45
  }