@hasna-internal/kai-sandbox 0.1.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
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
+ README.md: 3ebbffd623eee51dd6fcb588eec374e6d09be2f0
6
+ README.zh.md: b9215b59d5ed5314275e91261c5541be1e5586fe
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @hasna-internal/kai-sandbox
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Process-sandbox Service Definition. Owns the `ctx.sandbox` service contract ([`SandboxProvider`](src/index.ts)) and the confinement vocabulary the harness shares: `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`, file effects only), `SandboxEnforcement` (`full` / `partial`, per kernel ABI), `SandboxExecutionPolicy` (the complete per-call mode + workspace root), `SandboxPolicy` (its confined subset), and the fail-closed `SANDBOX_UNAVAILABLE` error. As the Service Definition role of the [capability-seam split](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md), it depends only on cordis (+ the harness error base), never on a backend.
6
+
7
+ The contract in one line: `ctx.sandbox.confine(argv, policy)` returns the argv to spawn INSTEAD of your own — wrapped so the process (and everything it spawns) runs confined — plus the selected backend's enforcement completeness, denial dialect (`denialSignatures`), and structured runner-failure evidence (`runnerFailureRules`); when no backend is usable it throws rather than passing the argv through unconfined. The [core type catalog](../../../docs/subsystems/sandbox.md#wrapped-argv-and-classification-dialects) owns the exact classifier shape.
8
+
9
+ Policy rides the call, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is just a new call with a wider policy.
10
+
11
+ **Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names the filesystem-canonical real host directory. Workspace identity is resolved before lexical normalization, so a valid cwd containing `symlink/..` grants the directory where `chdir` actually lands rather than an unrelated lexical parent. Containers, microVMs, and remote executors are NOT backends of this seam — they replace the Service Providers for whole capability seams (`ctx.shell`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
12
+
13
+ Implementations: [`@hasna-internal/kai-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). Consumers: [`@hasna-internal/kai-bash-sandbox`](../../shell/bash-sandbox/) (wraps `['bash', '-c', command]`).
14
+
15
+ ## Model Experience
16
+
17
+ ### Confinement error, indirectly
18
+
19
+ #### What the model sees
20
+
21
+ Through [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md) and [`dsh-tool-bash`](../../shell/tool-bash/README.md), failure to enforce a requested mode produces code `SANDBOX_UNAVAILABLE` and the exact error below. An execution-time runner failure adds ` Runner failure: <detail>`.
22
+
23
+ ##### Exact error
24
+
25
+ ```markdown
26
+ 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.
27
+ ```
28
+
29
+ #### Token effect
30
+
31
+ Conditional error text is visible for that call and retained in history until compaction.
32
+
33
+ #### KV Cache effect
34
+
35
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
36
+
37
+ ## Known Limitations and Deferred Work
38
+
39
+ - **File effects are the whole policy vocabulary** — the seam expresses no network, process, syscall, device, or credential restrictions.
40
+ - **Same-world confinement only** — containers, microVMs, and remote execution require replacing capability implementations rather than adding a provider here.
41
+ - **Denial reporting is a stderr dialect** — the seam returns backend signatures instead of a typed runtime denial channel, so consumers that need classification must infer it from the child process's output.
42
+ - **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 an availability/diagnostic false attribution. This cannot bypass confinement; an out-of-band runner-status channel is deferred.
43
+ - **One provider per context** — composing different sandbox mechanisms simultaneously requires a provider-level ladder or separate Cordis contexts; callers choose policy per call, not backend identity.
package/README.zh.md ADDED
@@ -0,0 +1,45 @@
1
+ # @hasna-internal/kai-sandbox
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 进程沙箱 Service Definition。负责定义 `ctx.sandbox` 服务约定([`SandboxProvider`](src/index.ts))与 harness 共享的限制词汇:`SandboxMode`(`read-only`/`workspace-write`/`danger-full-access`,仅限文件操作)、`SandboxEnforcement`(`full`/`partial`,针对每种内核 ABI)、`SandboxExecutionPolicy`(每次调用的完整模式及工作区根目录)、`SandboxPolicy`(其中受限制的子集),以及故障时拒绝放行的 `SANDBOX_UNAVAILABLE` 错误。作为[能力 seam 拆分](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)中的 Service Definition 角色,它只依赖 cordis(及 harness 错误基类),绝不依赖后端。
6
+
7
+ 用一句话概括约定:`ctx.sandbox.confine(argv, policy)` 返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行;还会附带所选后端达到的强制执行完整度、拒绝方言(`denialSignatures`)和结构化 runner 失败证据(`runnerFailureRules`)。没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。[核心类型目录](../../../docs/subsystems/sandbox.zh.md#wrapped-argv-and-classification-dialects)负责定义分类器的精确结构。
8
+
9
+ 策略随调用传递,而不属于提供方:两个消费方可以同时按不同策略施加限制(bash 使用 `read-only`,而受限制的子 agent(智能体)保持其状态目录可写);获批的升权重试只是使用更宽策略发起的新调用。
10
+
11
+ **只支持与宿主共享文件系统和内核的限制。** 后端与宿主共享文件系统和内核(`bwrap`、Landlock、Seatbelt);`workspaceRoot` 指向文件系统规范化后的真实主机目录。系统先解析工作区所指的目录,再做词法规范化,因此包含 `symlink/..` 的有效 cwd 会授权 `chdir` 实际到达的目录,而非无关的词法父目录。容器、microVM 与远程执行器都不是该 seam 的后端:它们会以环境一致的分组替换整个能力 seam 的 Service Provider(`ctx.shell`、`ctx.fs`)。边界及其设计理由见[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
12
+
13
+ 实现:[`@hasna-internal/kai-sandbox-local`](../sandbox-local/)(Linux:`bwrap`,否则使用相应平台的 Landlock launcher;macOS:`sandbox-exec`/Seatbelt)。消费方:[`@hasna-internal/kai-bash-sandbox`](../../shell/bash-sandbox/)(包装 `['bash', '-c', command]`)。
14
+
15
+ ## 模型体验
16
+
17
+ <a id="confinement-error-indirectly"></a>
18
+
19
+ ### 间接的限制错误
20
+
21
+ #### 模型看到的内容
22
+
23
+ 通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md) 和 [`dsh-tool-bash`](../../shell/tool-bash/README.zh.md),无法强制执行所请求模式时会产生错误码 `SANDBOX_UNAVAILABLE` 及以下精确错误。执行期 runner 失败会追加 ` Runner failure: <detail>`。
24
+
25
+ ##### 精确错误
26
+
27
+ ```markdown
28
+ 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.
29
+ ```
30
+
31
+ #### Token 影响
32
+
33
+ 条件性错误文本对该次调用可见,并保留在历史中直到压缩(compaction)。
34
+
35
+ #### KV Cache 影响
36
+
37
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
38
+
39
+ ## 已知限制与暂缓事项
40
+
41
+ - **文件操作是完整的策略词汇**:该 seam 不表达网络、进程、系统调用、设备或凭据限制。
42
+ - **只支持与宿主共享文件系统和内核的限制**:容器、microVM 与远程执行需要替换能力实现,而不是在此处增加提供方。
43
+ - **拒绝报告是一种 stderr 方言**:该 seam 返回后端签名,而非类型化运行时拒绝通道,因此需要分类的消费方必须从子进程输出推断。
44
+ - **Runner 诊断使用带内通道**:退出状态与 stderr 证据无法证明匹配行由哪个进程写入,因此受限子进程若故意模仿 runner,就可能造成可用性或诊断误归因。这无法绕过约束;带外 runner 状态通道暂缓实现。
45
+ - **每个上下文只有一个提供方**:同时组合不同沙箱机制需要提供方级阶梯或独立 Cordis 上下文;调用方逐调用选择策略,而非后端标识。
package/lib/index.js ADDED
@@ -0,0 +1,201 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { HarnessError, assertNever } from "@hasna-internal/kai-llm";
3
+ import { realpathSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ //#region lib/types/escalation.js
6
+ /**
7
+ * The escalation vocabulary and choreography shared by every sandbox-enforcing
8
+ * tool family (`@hasna-internal/kai-tool-bash`, `@hasna-internal/kai-tool-fs`): the
9
+ * strictly-wider ladder, the argument-pairing validation, the model-facing
10
+ * denial/hint markers, and {@link approveEscalation} — the ordered fail-closed
11
+ * sequence that resolves a `sandbox_permissions` request through a
12
+ * user-approval channel BEFORE anything executes. One home keeps the two
13
+ * families' approval ordering and verbatim error texts from drifting apart.
14
+ *
15
+ * The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}),
16
+ * not the approval service type: the tool layer — which owns the agent, the
17
+ * call id, and the tool name — closes over `ctx.approval.request(...)` and
18
+ * hands the closure down, so this package never depends on the approval or
19
+ * agent packages.
20
+ *
21
+ * @module dsh-sandbox/escalation
22
+ */
23
+ /**
24
+ * The strictly-wider table: what a call whose effective mode is the key may
25
+ * escalate TO. Checked at EXECUTION, never baked into a tool schema — the
26
+ * schema's enum is {@link ESCALATION_TARGETS}, because schemas are
27
+ * registry-global while the effective mode is per-call truth.
28
+ */
29
+ const WIDER_MODES = {
30
+ "read-only": ["workspace-write", "danger-full-access"],
31
+ "workspace-write": ["danger-full-access"]
32
+ };
33
+ /**
34
+ * The closed escalation-target vocabulary — every mode a call could ever
35
+ * escalate TO (`read-only` is the floor; nothing escalates to it). Advertised
36
+ * whenever the mounted capability confines: cutting the enum down to the modes
37
+ * wider than the composition's DEFAULT would strand a session whose effective
38
+ * mode sits below it (a `danger-full-access` default would advertise nothing
39
+ * while a narrower-switched session stays confined with no lever).
40
+ */
41
+ const ESCALATION_TARGETS = ["workspace-write", "danger-full-access"];
42
+ /**
43
+ * Validate the escalation argument pairing a tool schema cannot express:
44
+ * `sandbox_permissions` and `justification` travel together — an approval
45
+ * prompt without a reason, or a reason driving nothing, is a malformed ask —
46
+ * and the justification must be a non-empty sentence.
47
+ * @param sandboxPermissions - the raw `sandbox_permissions` argument, if given.
48
+ * @param justification - the raw `justification` argument, if given.
49
+ */
50
+ function validateEscalationArgs(sandboxPermissions, justification) {
51
+ if (sandboxPermissions !== void 0 && justification === void 0) throw new Error("invalid escalation: sandbox_permissions requires a justification");
52
+ if (justification !== void 0 && sandboxPermissions === void 0) throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
53
+ if (justification !== void 0 && justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
54
+ }
55
+ /**
56
+ * The model-facing denial marker — the one vocabulary both enforcing families
57
+ * teach and report, so the model recognizes a policy denial identically
58
+ * whether the kernel refused a bash file effect or the filesystem provider's
59
+ * fence refused a mutation.
60
+ * @param mode - the mode the denied call ran under.
61
+ * @returns the marker line, exactly as the model sees it.
62
+ */
63
+ function sandboxDenialMarker(mode) {
64
+ return `[sandbox: file access denied under ${mode} mode]`;
65
+ }
66
+ /**
67
+ * The same-turn escalation hint that rides a denial when the composition
68
+ * advertises the escalation fields — the nudge lives at the decision point so
69
+ * the sanctioned retry does not depend on the model recalling the tool
70
+ * description.
71
+ * @param subject - the family's noun for the denied action (`command` for
72
+ * bash, `operation` for a filesystem mutation).
73
+ * @returns the hint line, exactly as the model sees it.
74
+ */
75
+ function escalationHintMarker(subject) {
76
+ 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]`;
77
+ }
78
+ /**
79
+ * Resolve a sandbox-escalation request BEFORE anything executes: check strict
80
+ * widening against the call's effective mode, then resolve the approval
81
+ * channel, then map every outcome — the ordered fail-closed sequence both
82
+ * enforcing families share. Returns the granted mode to stamp onto exactly
83
+ * this call; throws the distinct verbatim text for every other path (a
84
+ * non-widening request, a missing approval service, an agent-less execution,
85
+ * a rejection, a cancellation, an unanswerable ask) — the tool registry turns
86
+ * the throw into the call's isError result, and nothing has run. A
87
+ * non-widening request never prompts a human.
88
+ * @param request - the escalation to judge (see {@link EscalationRequest}).
89
+ * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
90
+ * @returns the granted mode, consumed by the one call that asked.
91
+ */
92
+ async function approveEscalation(request, approval) {
93
+ const { requestedMode: mode, effectiveMode, justification, subject } = request;
94
+ if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode)) throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
95
+ if (approval.approver === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`);
96
+ if (approval.agent === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`);
97
+ const outcome = await approval.approver.request({
98
+ agent: approval.agent,
99
+ toolName: approval.toolName,
100
+ callId: approval.callId,
101
+ reason: `escalate sandbox to ${mode}: ${justification}`,
102
+ ...approval.signal ? { signal: approval.signal } : {}
103
+ });
104
+ switch (outcome) {
105
+ case "allowed-once": return mode;
106
+ case "rejected": throw new Error(`the user rejected escalating this ${subject} to "${mode}"`);
107
+ case "cancelled": throw new Error(`approval for escalating to "${mode}" was cancelled`);
108
+ case "unavailable": throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`);
109
+ default: return assertNever(outcome, "EscalationOutcome");
110
+ }
111
+ }
112
+ //#endregion
113
+ //#region lib/types/roots.js
114
+ /**
115
+ * The writable-root derivation shared by every enforcement dialect that
116
+ * expresses a mode as a canonical allow-list: `workspace-write` means "the
117
+ * workspace root plus the platform temp areas", and this module is that
118
+ * meaning's one home. The Seatbelt profile
119
+ * (`@hasna-internal/kai-sandbox-local`) and the in-process filesystem fence
120
+ * (`@hasna-internal/kai-fs-sandbox`) both derive their allow-list here, so "the
121
+ * write tool cannot write /tmp but bash can" asymmetries cannot arise between
122
+ * them. The bwrap and Landlock dialects keep their own grant spellings (an
123
+ * ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner
124
+ * differences recorded in the sandbox RFC — with parity pinned by test.
125
+ *
126
+ * @module dsh-sandbox/roots
127
+ */
128
+ /**
129
+ * Resolve a granted root to the path the enforcement layer actually compares:
130
+ * canonical (symlinks resolved), because both Seatbelt filters and the fs
131
+ * fence's containment check match resolved paths — `/tmp` IS `/private/tmp`
132
+ * on darwin, and an as-spelled grant would match nothing.
133
+ * @param path - the root as configured or platform-reported.
134
+ * @returns the canonical path, or the spelling as-is when resolution fails
135
+ * (a missing root matches nothing until it exists — the conservative
136
+ * outcome; inventing a fallback would grant a path the caller never named).
137
+ */
138
+ function canonicalPath(path) {
139
+ try {
140
+ return realpathSync.native(path);
141
+ } catch {
142
+ return path;
143
+ }
144
+ }
145
+ /**
146
+ * The roots one confined execution may WRITE under — the mode's meaning as a
147
+ * canonical, deduplicated allow-list. `read-only` allows nothing;
148
+ * `workspace-write` allows the policy's workspace root, the host `/tmp`, and
149
+ * the per-user platform temp dir (`os.tmpdir()` — the real temp area for
150
+ * mkstemp-family tools; omitting it would deny what the mode promises).
151
+ * @param policy - the file-effect policy to derive the allow-list from.
152
+ * @returns the canonical writable roots; empty exactly under `read-only`.
153
+ */
154
+ function writableRoots(policy) {
155
+ if (policy.mode !== "workspace-write") return [];
156
+ return [...new Set([
157
+ policy.workspaceRoot,
158
+ "/tmp",
159
+ tmpdir()
160
+ ].map(canonicalPath))];
161
+ }
162
+ //#endregion
163
+ //#region lib/types/index.js
164
+ /**
165
+ * Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a
166
+ * host-path file policy. Containers, microVMs, and remote execution replace the
167
+ * surrounding capability seam instead; this service shares the host kernel and filesystem.
168
+ * @module @hasna-internal/kai-sandbox
169
+ */
170
+ /**
171
+ * Error code for a requested confined mode when no backend is usable. The
172
+ * provider fails closed, and `HarnessError` carries the code through
173
+ * `tool/result` so callers can distinguish missing confinement from command
174
+ * failure.
175
+ */
176
+ const SANDBOX_UNAVAILABLE = "SANDBOX_UNAVAILABLE";
177
+ /**
178
+ * Thrown when {@link SandboxProvider.confine} cannot enforce the requested
179
+ * mode. Carries {@link SANDBOX_UNAVAILABLE} through the structured error
180
+ * channel.
181
+ */
182
+ var SandboxUnavailableError = class extends HarnessError {
183
+ constructor(mode, detail) {
184
+ 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);
185
+ this.name = "SandboxUnavailableError";
186
+ }
187
+ };
188
+ /**
189
+ * Abstract process-sandbox service. {@link confine} must return enforcing argv
190
+ * or fail closed at wrap or runner-execution time; silent unconfined passthrough
191
+ * is forbidden. Functional probes arbitrate multi-runner chains and may be
192
+ * skipped for a sole candidate, whose own refusal remains the fail-closed end.
193
+ */
194
+ var SandboxProvider = class extends Service {
195
+ /* v8 ignore next -- abstract service construction is covered through concrete provider packages. */
196
+ constructor(ctx) {
197
+ super(ctx, "sandbox");
198
+ }
199
+ };
200
+ //#endregion
201
+ export { ESCALATION_TARGETS, SANDBOX_UNAVAILABLE, SandboxProvider, SandboxProvider as default, SandboxUnavailableError, WIDER_MODES, approveEscalation, canonicalPath, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs, writableRoots };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hasna-internal/kai-sandbox`.
4
+ * @module @hasna-internal/kai-sandbox/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hasna-internal/kai-sandbox";
7
+ /** Cordis companion plugin name. */
8
+ const name = "sandbox-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or mutable data relation
13
+ * beyond contracts enforced at its owning seam.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The escalation vocabulary and choreography shared by every sandbox-enforcing
3
+ * tool family (`@hasna-internal/kai-tool-bash`, `@hasna-internal/kai-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 dsh-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`/`CallId`).
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-self-contained request (agent, tool, call id, reason, optional 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
+ signal?: AbortSignal;
88
+ }): Promise<EscalationOutcome>;
89
+ }
90
+ /**
91
+ * The approval ingredients an escalating tool hands {@link approveEscalation}:
92
+ * the approval requester (`ctx.approval`, or `undefined` when none is
93
+ * composed), the calling agent (or `undefined` for an agent-less execution),
94
+ * and the call's identity. The tool layer holds all of these; this package
95
+ * only judges them.
96
+ */
97
+ export interface EscalationApproval<A = object, C = string> {
98
+ /** The approval requester (`ctx.approval`), or `undefined` when none is composed. */
99
+ approver: EscalationApprover<A, C> | undefined;
100
+ /** The calling agent, or `undefined` for an agent-less execution (fails closed). */
101
+ agent: A | undefined;
102
+ /** The tool-call id the approval prompt attaches to. */
103
+ callId: C;
104
+ /** The tool name recorded on the approval request. */
105
+ toolName: string;
106
+ /** The tool-execution abort signal the approval request rides, when present. */
107
+ signal?: AbortSignal;
108
+ }
109
+ /** One escalation request, as {@link approveEscalation} judges it. */
110
+ export interface EscalationRequest {
111
+ /** The requested target mode (schema-pinned to {@link ESCALATION_TARGETS} when advertised). */
112
+ requestedMode: string;
113
+ /** The model's one-sentence reason, shown verbatim to the user inside the audit reason. */
114
+ justification: string;
115
+ /** The call's effective mode (session override ?? composition default) the request must strictly widen. */
116
+ effectiveMode: SandboxMode;
117
+ /** The family's noun for the escalated action in user-facing texts (`command` for bash, `operation` for fs). */
118
+ subject: string;
119
+ }
120
+ /**
121
+ * Resolve a sandbox-escalation request BEFORE anything executes: check strict
122
+ * widening against the call's effective mode, then resolve the approval
123
+ * channel, then map every outcome — the ordered fail-closed sequence both
124
+ * enforcing families share. Returns the granted mode to stamp onto exactly
125
+ * this call; throws the distinct verbatim text for every other path (a
126
+ * non-widening request, a missing approval service, an agent-less execution,
127
+ * a rejection, a cancellation, an unanswerable ask) — the tool registry turns
128
+ * the throw into the call's isError result, and nothing has run. A
129
+ * non-widening request never prompts a human.
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,143 @@
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 @hasna-internal/kai-sandbox
6
+ */
7
+ import { Context, Service } from '@deepseek-ai/cordis';
8
+ import { HarnessError } from '@hasna-internal/kai-llm';
9
+ import type { SessionId } from '@hasna-internal/kai-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 `dsh-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 '@deepseek-ai/cordis' {
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
+ * @returns the argv to spawn instead, plus the enforcement completeness
138
+ * the selected backend achieves for it.
139
+ */
140
+ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv;
141
+ }
142
+ export default SandboxProvider;
143
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hasna-internal/kai-sandbox`.
3
+ * @module @hasna-internal/kai-sandbox/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "sandbox-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.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
+ * (`@hasna-internal/kai-sandbox-local`) and the in-process filesystem fence
7
+ * (`@hasna-internal/kai-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 dsh-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 ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@hasna-internal/kai-sandbox",
3
+ "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract",
4
+ "version": "0.1.1-rc.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/sandbox/sandbox"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "@hasna-internal/kai-llm": "^0.1.1-rc.2",
36
+ "@hasna-internal/kai-invariants": "^0.1.1-rc.2",
37
+ "@hasna-internal/kai-session": "^0.1.1-rc.2",
38
+ "@deepseek-ai/cordis": "^4.0.1"
39
+ },
40
+ "devDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.1",
42
+ "@hasna-internal/kai-session": "^0.1.1-rc.2",
43
+ "@hasna-internal/kai-llm": "^0.1.1-rc.2",
44
+ "@hasna-internal/kai-invariants": "^0.1.1-rc.2"
45
+ }
46
+ }