@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 +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +43 -0
- package/README.zh.md +45 -0
- package/lib/index.js +201 -0
- package/lib/invariant.js +23 -0
- package/lib/types/escalation.d.ts +135 -0
- package/lib/types/index.d.ts +143 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/roots.d.ts +37 -0
- package/package.json +46 -0
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.
|
package/README.i18n.yaml
ADDED
|
@@ -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 };
|
package/lib/invariant.js
ADDED
|
@@ -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
|
+
}
|