refactor(landlock-run): unify workspace release (review round 1)

This commit is contained in:
Hypatia May
2026-08-06 10:23:26 +08:00
parent e88bace07e
commit 598f9719f4
40 changed files with 535 additions and 530 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
2026-07-06-sandbox.md: aed5ac1ceb02130ce97a8c83c0f77869fdc32146
2026-07-06-sandbox.zh.md: db95b1a5b7a7cae1e0fcdd8deba9dcb6ad020a67
2026-07-06-sandbox.md: 69a3f1bd181bc06d9a176fa45b1e091991cfa682
2026-07-06-sandbox.zh.md: eeca55b61da24df215f7a9b7ba8dbf9ab2387f20

View File

@@ -64,7 +64,7 @@ Left open, for the phase that needs them: whether network restriction arrives as
The launcher is a ~300-line C program (plain C11 over the raw Landlock UAPI — no libraries beyond a statically linked musl, so the audit surface is that one file plus the kernel's stable syscall contract): `--ro <path>` / `--rw <path>` grants, `--`, the wrapped argv; it installs the ruleset on itself and `exec`s (rulesets are inherited across `execve`, and it sets `no_new_privs` before restricting); `--probe` enforces a maximal ruleset in a short-lived child and exits 0 only when the kernel actually enforces; every launcher failure exits 125 without running the child and prints a fatal `landlock-run:` line. A successfully exec'd child may also return 125, so status alone is not launcher evidence. An older ABI prints the exact `landlock-run: partial enforcement (older Landlock ABI)` notice before it executes the child, so that line is not fatal evidence.
The Landlock launcher source and package workspace live at `native/landlock-run`, next to the harness consumers. The standalone [`node-addon-landlock-run`](https://github.com/deepseek-harness/node-addon-landlock-run) repository is the release mirror used to pack and publish the npm package family; `native/README.md` owns the export procedure. Platform binaries are selected by npm, and the entry package owns path resolution, probing, CLI flags, the fatal prefix, and the partial-enforcement notice while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
The Landlock launcher source and package family live at `native/landlock-run`, next to the harness consumers and inside the root pnpm workspace. The [in-repository Landlock release decision](../process/2026-08-06-in-repository-landlock-release.md) owns the shared lockfile, native build, pack rehearsal, and npm publication boundary. Platform binaries are selected by npm, and the entry package owns path resolution, probing, CLI flags, the fatal prefix, and the partial-enforcement notice while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
Backend profiles share the mode contract but differ in necessary host grants. Landlock and Seatbelt allow only `/dev/null` in read-only mode; workspace-write also permits their required host temp roots. Each wrap carries backend-specific denial signatures. Landlock reports partial enforcement on older ABIs that cannot govern every operation, while successful bwrap and Seatbelt profiles report full enforcement.
@@ -118,7 +118,7 @@ fs/web/todo execute in-process, so their sandbox semantics are policy at their s
### Testing
- **Unit:** pin platform selection and profiles, direct provider-argv handoff, spawn-level failures with invalid-workdir controls, missing/non-executable/missing-interpreter evidence, malformed-runner negative controls, confined `BASH_ENV` ordering, structured runner classification (including partial-Landlock notice-only child outcomes, gated fatal evidence, child exits 126/127, and foreground/background parity), per-call mode/root resolution, per-process facts, escalation validation and outcomes, permission preset folding and write-through, and runtime-context ordering and materialization.
- **Keyless real-runner:** exercise bwrap, Landlock, and Seatbelt against real filesystem effects at provider and bash-consumer layers; one real Cordis context concurrently drives two project sessions through shipped bash and fs tools, proving own-root success and sibling-root denial. Packed-install coverage proves the registry launcher remains executable. CI rejects a silent all-skip.
- **Keyless real-runner:** exercise bwrap, Landlock, and Seatbelt against real filesystem effects at provider and bash-consumer layers; one real Cordis context concurrently drives two project sessions through shipped bash and fs tools, proving own-root success and sibling-root denial. Packed-install coverage installs the current checkout's native tarballs and proves the launcher remains executable and byte-identical. CI rejects a silent all-skip.
- **With-key:** start the real ACP composition in read-only mode, let a model-driven bash write hit the runner's denial marker, then drive the bridge answerer and disk effect through granted and rejected workspace-write retries; unavailable credentials or runners self-skip.
- **Snapshot:** pin the atomic current-policy context and both scripted approval branches. A real ACP example scenario places its session under the user home while the deployment fallback points at `/tmp`, then pins both the workspace-write runtime-context message and a successful deployment-selected mutation; this distinguishes session-root resolution from the process fallback without depending on runner-specific denial text. A POSIX fake partial-Landlock provider pins direct bash `false` as an ordinary child result and a missing provider executable as foreground/background infrastructure failure through the assembled app. Other snapshots start unconfined so unrelated fixtures remain platform-independent.
@@ -134,9 +134,9 @@ Each phase gets its full design when picked up, validated against the code at th
- **Command-string heuristic preflight** — rejected: cannot understand expansion/subprocesses/symlinks; the strict attempt (run it, let the kernel decide) is the only trustworthy denial signal.
- **Functionally probe even a platform's sole backend** — rejected: probing arbitrates between candidates; with one there is nothing to decide, and probe cost taxes the first confined command of every session (prohibitive for heavy future backends). The runner's own exec-time fail-closed refusal plus structured `runnerFailureRules` classification carries the safety property instead.
- **Commit the built launcher binaries** — rejected: a binary in a diff is unreviewable and churns history; reviewed source + native CI builds + the launcher repo's byte-pinned publish rehearsal keep bytes out of every tree.
- **Commit the built launcher binaries** — rejected: a binary in a diff is unreviewable and churns history; reviewed source + native CI builds + the main repository's byte-pinned publish rehearsal keep bytes out of every tree.
- **Compile the launcher on install** — rejected: pushes a C toolchain onto every consumer; a fallback that exists only where a compiler happens to be is not a fallback.
- **Cross-compile both architectures from one builder** — rejected: requires carrying a pinned cross toolchain (rustup targets, zig, or a container image) solely to rebuild two ~70 KB binaries; per-architecture native runners already exist and each builds its own platform package (the `node-addon-require-builtin` model, the launcher repo's own pipeline).
- **Cross-compile both architectures from one builder** — rejected: requires carrying a pinned cross toolchain (rustup targets, zig, or a container image) solely to rebuild two ~70 KB binaries; per-architecture native runners already exist and each builds its own platform package (the `node-addon-require-builtin` model, retained by the main repository's native pipeline).
- **No fallback (bwrap or fail closed)** — rejected: concentrates failure on the hosts a sandbox matters most, degrading to `danger-full-access` by resignation.
- **Keep the mechanism inside `dsh-bash-sandbox`** — rejected: blocks the existing second consumer, makes future phases read mode out of a bash plugin's config, and cannot express escalation.
- **Config-fixed mode on the provider** — rejected: one mode per process; cannot serve concurrent consumers with different policies nor the one-shot widened retry.
@@ -175,7 +175,7 @@ Costs and accepted limits:
- **The Seatbelt rung leans on Apple's deprecated-but-shipped `sandbox-exec` CLI.** As darwin's sole candidate it is selected without probing, so a future removal under a usable workdir surfaces as a runner-attributable spawn failure and an executable refusal through its fatal signature — both become `SANDBOX_UNAVAILABLE`, and the command never runs; fail closed, never open.
- **Landlock confinement is only as complete as the running kernel's ABI.** Reported as `enforcement: 'partial'` rather than refused — the deliberate trade that keeps the fallback available on older-kernel hosts.
- **Runner attribution uses an in-band protocol.** Exit status plus stderr cannot cryptographically identify the writer, so a confined child can mimic a fatal runner line and status to cause an availability/diagnostic false attribution. The conjunction and exact notice exclusion reduce accidental matches; this is not a sandbox bypass because the child is already confined.
- **The launcher arrives as a registry dependency.** Trusted through its own repository's release pipeline (reviewed C source, native CI builders, byte-pinned publish rehearsal) plus this repo's version pin — the real-kernel e2e legs are what vouch for behavior through the installed bytes.
- **The launcher is a workspace dependency in source and an npm dependency after publication.** The main repository tests reviewed C source, native CI builds, and byte-pinned local tarballs together before publishing the same package family; the real-kernel e2e legs vouch for behavior through those installed bytes.
- **The model may over-ask.** Escalating without denial grounding, or picking `danger-full-access` where `workspace-write` suffices: the description steers and the enum forces the ladder, but the human prompt is the actual gate; the `approval/asked` reasons make over-asking auditable, and a `prepend` policy answerer can auto-reject patterns a deployment never wants.
- **The advertised target set is static while the effective mode is per-session** (schemas are registry-global) — a session already at the widest mode is still offered the fields. Harmless by construction: the strict-wider check at execution, not the enum, is the safety boundary — a non-widening request fails with its own text and never prompts anyone.
- **A granted escalation is not a working sandbox.** An unavailable backend still fails closed even for a granted escalation to a confining mode — at `confine()` when the platform has no chain or every probe fails, through the spawn channel when the selected executable cannot start, or through a structured rule when a started runner refuses — while a granted `danger-full-access` run never touches the provider at all: there the grant, not the probe, is the authority.
@@ -187,7 +187,7 @@ Costs and accepted limits:
- **A command came back with `[sandbox: file access denied under read-only mode]` — did it fail?** It RAN, and the kernel refused a file effect: the denial is a result fact orthogonal to exit code. The teaching forbids retrying around it; the one sanctioned move is the same command retried once with an escalation request.
- **How is a BROKEN sandbox told apart from a failing command?** Any provider-argv spawn rejection proves the confined launch never started, but it identifies a broken runner only when the caller-owned workdir is usable and Node reports attributable `ENOENT` or `EACCES` for that argv[0]. A bare `syscall: 'spawn'` without an exact error path and all other rejections remain ordinary command-start errors. After a process starts, runner failure outranks denial only when one `runnerFailureRules` entry matches both its optional exit-code gate and a fatal stderr line after exact informational exclusions. Foreground failures throw structured `SANDBOX_UNAVAILABLE` with spawn or matched-line detail; an asynchronously rejected or settled background task stamps `sandbox.runnerFailed` and renders its own marker. A `SubprocessService` that synchronously throws the same provenanced `ENOENT`/`EACCES` shape makes background start throw the structured error; other synchronous errors propagate unchanged. A Landlock partial-enforcement notice plus an ordinary child failure remains a command result.
- **What happens on a platform with no backend — Windows today?** `confine()` throws the fail-closed `SANDBOX_UNAVAILABLE` and the command never spawns; `win32` is a reserved EMPTY chain, pinned by test to fail closed identically until a Windows runner fills it (§ Deferred phases).
- **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the registry-installed Landlock launcher, and the verdict is cached for the provider's lifetime.
- **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the packaged Landlock launcher, and the verdict is cached for the provider's lifetime.
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary).
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.

View File

@@ -64,7 +64,7 @@ OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还
launcher 是一个约 300 行的 C 程序(纯 C11直接使用 Landlock UAPI——除静态链接的 musl 外无其他库,因此审计面仅为该文件加内核的稳定 syscall 契约):`--ro <path>` / `--rw <path>` 授权,`--`,被包装的 argv它为自身安装规则集并执行 `exec`(规则集跨 `execve` 继承,且它在限制前设置 `no_new_privs``--probe` 在一个短生命周期子进程中强制最大规则集,仅当内核确实强制时才以 0 退出;所有 launcher 失败都会以 125 退出且不运行子进程,并打印一行致命的 `landlock-run:` 诊断。成功完成 exec 的子进程也可能返回 125因此仅凭退出状态不能作为 launcher 失败的证据。较旧的 ABI 会在执行子进程之前打印精确的 `landlock-run: partial enforcement (older Landlock ABI)` 通知,因此该行不是致命证据。
Landlock launcher 源码和包工作区位于 `native/landlock-run`,与 harness 消费方同仓。独立的 [`node-addon-landlock-run`](https://github.com/deepseek-harness/node-addon-landlock-run) 仓库是用于打包并发布 npm 包族的发布镜像;导出流程归 `native/README.md` 所有。平台二进制由 npm 选择入口包拥有路径解析、探测、CLI 参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。
Landlock launcher 源码和包家族位于 `native/landlock-run`,与 harness 消费方同仓,并属于根 pnpm workspace。[仓库内 Landlock 发布决策](../process/2026-08-06-in-repository-landlock-release.md)负责共享锁文件、原生构建、打包演练和 npm 发布边界。平台二进制由 npm 选择入口包拥有路径解析、探测、CLI 参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。
后端 profile 共享模式契约但在必要的主机授权上有所不同。Landlock 和 Seatbelt 在 read-only 模式下仅允许 `/dev/null`workspace-write 还允许各自所需的主机临时目录根。每次包装携带后端特定的拒绝签名。Landlock 在较旧的 ABI 无法管控所有操作时报告 partial enforcement而成功的 bwrap 和 Seatbelt profile 报告 full enforcement。
@@ -118,7 +118,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
### 测试
- **单元测试:** 固定平台选择和 profile、直接交接提供方返回的 argv、带有无效 workdir 对照的 spawn 层失败、runner 缺失/不可执行/解释器缺失证据、格式错误 runner 阴性对照、受约束的 `BASH_ENV` 求值顺序、结构化 runner 分类(包括只有部分强制执行通知的子进程结果、带门控的致命证据、子进程退出码 126127以及前台后台一致性、按调用的模式/根目录解析、按进程事实、升级验证和结果、权限 preset fold 和写入透传,以及运行时上下文排序与具体化。
- **Keyless 真实 runner** 在提供方和 bash 消费方层面对 bwrap、Landlock 和 Seatbelt 执行真实文件系统效果测试;一个真实 Cordis 上下文通过已交付的 bash 和 fs 工具并发驱动两个项目会话,证明在自身根目录写入成功、在兄弟根目录写入被拒绝。打包安装测试证明注册表 launcher 保持可执行。CI 拒绝静默全跳过。
- **Keyless 真实 runner** 在提供方和 bash 消费方层面对 bwrap、Landlock 和 Seatbelt 执行真实文件系统效果测试;一个真实 Cordis 上下文通过已交付的 bash 和 fs 工具并发驱动两个项目会话,证明在自身根目录写入成功、在兄弟根目录写入被拒绝。打包安装测试会安装当前 checkout 的原生 tarball证明 launcher 保持可执行且字节完全一致。CI 拒绝静默全跳过。
- **With-key** 以只读模式启动真实 ACP 组合,让模型驱动的 bash 写入命中 runner 的拒绝标记,再通过已授权与被拒绝的 workspace-write 重试驱动 bridge 应答器和磁盘效果;不可用的凭证或 runner 自动跳过。
- **快照:** 固定原子化的当前策略上下文和两个脚本化的 approval 分支。一个真实 ACP 示例场景把会话放在用户主目录下,同时让部署后备根目录指向 `/tmp`,然后固定 workspace-write 运行时上下文消息与一次成功的、由部署选定的变更;这能区分会话根目录解析与进程后备值,而不依赖 runner 特定的拒绝文本。一个模拟 Landlock 部分强制执行行为的 POSIX 提供方会在组装后的应用中固定直接执行 bash `false` 时仍得到普通子进程结果,并固定提供方可执行文件缺失时在前台/后台均为基础设施失败。其他快照以无约束启动,使无关 fixture测试前置数据保持平台无关。
@@ -134,9 +134,9 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **命令字符串启发式预检**:否决。无法理解展开/子进程/符号链接;严格尝试(运行它,让内核决定)是唯一可信的拒绝信号。
- **即使平台仅有一个后端也功能性探测**否决。探测用于在候选者之间仲裁只有一个时无需决策且探测开销对每个会话的首次约束命令征税对未来重量级后端而言代价过高。runner 自身执行时的失败关闭拒绝加结构化 `runnerFailureRules` 分类承载了安全属性。
- **提交构建好的 launcher 二进制**否决。diff 中的二进制不可审查且膨胀历史;经审查的源码 + 原生 CI 构建 + launcher 仓库的字节固定发布演练使二进制远离所有代码树。
- **提交构建好的 launcher 二进制**否决。diff 中的二进制不可审查且膨胀历史;经审查的源码 + 原生 CI 构建 + 仓库的字节固定发布演练使二进制远离所有代码树。
- **安装时编译 launcher**:否决。将 C 工具链强加给每个消费方;仅在碰巧有编译器时才存在的备选不是备选。
- **从一个构建器交叉编译两种架构**:否决。仅为重建两个约 70 KB 的二进制就需要携带一个固定的交叉工具链rustup targets、zig 或容器镜像);每架构的原生 runner 已存在,各自构建自己的平台包(`node-addon-require-builtin` 模式,launcher 仓库自己的流水线)。
- **从一个构建器交叉编译两种架构**:否决。仅为重建两个约 70 KB 的二进制就需要携带一个固定的交叉工具链rustup targets、zig 或容器镜像);每架构的原生 runner 已存在,各自构建自己的平台包(`node-addon-require-builtin` 模式,由主仓库的原生流水线保留)。
- **无备选bwrap 或失败关闭)**:否决。将失败集中在沙箱最重要的主机上,最终因放弃而降级到 `danger-full-access`
- **将机制保留在 `dsh-bash-sandbox` 内部**:否决。阻塞既有的第二个消费方,使未来阶段从一个 bash 插件的配置中读取模式,且无法表达升级。
- **提供方上的配置固定模式**:否决。每进程一个模式;无法服务具有不同策略的并发消费方,也无法表达一次性放宽重试。
@@ -175,7 +175,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **Seatbelt 层级依赖 Apple 已弃用但仍交付的 `sandbox-exec` CLI。** 作为 darwin 的唯一候选,它无需探测即被选中,因此在 workdir 可用时,未来移除会表现为可归因于 runner 的 spawn 失败,可执行文件拒绝则通过其致命签名体现——两者都会变为 `SANDBOX_UNAVAILABLE`,且命令绝不会运行;失败关闭,绝不开放。
- **Landlock 约束的完整度取决于运行内核的 ABI。** 报告为 `enforcement: 'partial'` 而非拒绝——这是有意的权衡,使备选在旧内核主机上仍可用。
- **Runner 归因使用带内协议。** 退出状态与 stderr 无法以密码学方式识别写入者,因此受限子进程可以模仿 runner 的致命诊断行和状态,造成可用性或诊断误归因。多项证据的合取与精确通知排除减少了意外匹配;这不是沙箱绕过,因为子进程已经受到限制。
- **launcher 作为注册表依赖到达。** 通过其自身仓库的发布流水线(经审查的 C 源码、原生 CI 构建器、字节固定的发布演练)加上本仓库的版本固定获得信任——真实内核 e2e 测试环节会验证安装产物的实际行为。
- **launcher 在源码中是 workspace 依赖,发布后是 npm 依赖。** 主仓库会在发布同一个包家族之前,一起测试经审查的 C 源码、原生 CI 构建字节固定的本地 tarball真实内核 e2e 测试环节会验证这些安装字节的实际行为。
- **模型可能过度请求。** 在没有拒绝依据的情况下升级,或在 `workspace-write` 足够时选择 `danger-full-access`:描述引导且枚举强制阶梯,但人的提示词是实际门控;`approval/asked` 原因使过度请求可审计,且 `prepend` 策略应答器可以自动拒绝部署永远不想要的模式。
- **公布的目标集是静态的,而有效模式是按会话的**schema 是注册表全局的)——已处于最宽模式的会话仍被提供这些字段。构造上无害:执行时的严格放宽检查(而非枚举)是安全边界——非放宽请求以自身文本失败且不提示任何人。
- **授权的升级不等于可工作的沙箱。** 不可用的后端即使对授权升级到约束模式也仍然失败关闭——平台没有链或所有探测失败时在 `confine()` 阶段失败,所选可执行文件无法启动时通过 spawn 通道失败,已启动的 runner 拒绝时则通过结构化规则失败——而授权的 `danger-full-access` 运行根本不触及提供方:此时授权(而非探测)是权威。
@@ -187,7 +187,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **一个命令返回了 `[sandbox: file access denied under read-only mode]`——它失败了吗?** 它运行了,内核拒绝了一个文件操作:拒绝是与退出码正交的结果事实。相关指令禁止通过绕过限制来重试;唯一被认可的动作是以升级请求重试同一命令一次。
- **如何区分损坏的沙箱与失败的命令?** 提供方 argv 的任何 spawn 拒绝都能证明受限启动从未开始,但只有在调用方拥有的 workdir 可用,且 Node 为该 argv[0] 报告可归因的 `ENOENT``EACCES` 时,才能据此判定 runner 损坏。没有精确错误路径的裸 `syscall: 'spawn'` 和其他所有拒绝仍是普通的命令启动错误。进程启动后,只有当 `runnerFailureRules` 中某一条目同时匹配其可选退出码门控,以及排除整行精确信息性行后的一行致命 stderr 诊断时runner 失败才会优先于拒绝。前台失败会抛出结构化的 `SANDBOX_UNAVAILABLE`,并附带 spawn 错误或匹配行作为详细信息;遭异步拒绝或已结算的后台任务则盖章 `sandbox.runnerFailed` 并渲染自己的标记。如果 `SubprocessService` 同步抛出同样带有来源信息的 `ENOENT``EACCES` 形态后台启动会抛出该结构化错误其他同步错误原样传播。Landlock 部分强制执行通知加上普通子进程失败时,仍返回命令结果。
- **在没有后端的平台上会发生什么——今天的 Windows** `confine()` 抛出失败关闭的 `SANDBOX_UNAVAILABLE`,命令永不 spawn`win32` 是保留的空链,由测试固定为同样失败关闭,直到 Windows runner 填充它(§ 延迟阶段)。
- **`bwrap` 已安装在我的主机上但不可用(禁用了非特权 userns、LSM 拒绝 `mount`)——会发生什么?** 链探测是功能性的——它构建并强制一个真实 profile 而非检查 `--version`——因此存在但不可用的 `bwrap` 探测失败,选择落到注册表安装的 Landlock launcher结论在提供方生命周期内缓存。
- **`bwrap` 已安装在我的主机上但不可用(禁用了非特权 userns、LSM 拒绝 `mount`)——会发生什么?** 链探测是功能性的——它构建并强制一个真实 profile 而非检查 `--version`——因此存在但不可用的 `bwrap` 探测失败,选择落到已打包的 Landlock launcher结论在提供方生命周期内缓存。
- **沙箱限制网络或进程可见性吗?** 不——`SandboxMode` 仅声称文件操作bwrap profile 刻意不 unshare pid没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。
- **哪些工具实际在约束下运行?** 通过 `ctx.bash` 的 OS 子进程——bash 工具及传递性的钩子命令——再加上通过沙箱化 `ctx.fs` 提供方运行的文件系统工具(`read`/`write`/`edit`,见[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md)bash 通过 OS runner 约束fs 通过进程内路径围栏约束,二者都以同一个 `ctx.sandboxPolicy` 模式为键。web/todo 仍在进程内且不受限制web 的唯一效果是网络,不在文件效果模式词汇内)。
- **授权的升级会持久化吗?** 不会。授权由发起请求的确切前台或后台调用消费;每个相邻调用保留自己的有效模式。后续的后台拒绝通过 `task_output` 呈现,并且可以作为一次新的精确命令重试的依据。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md
2026-08-06-in-repository-landlock-release.md: f682078250adde8d56a4270e9d01ce4b1cd1bee9
2026-08-06-in-repository-landlock-release.zh.md: 4950d80d87afd18c5605f4f5bca56b8d85564fc2

View File

@@ -0,0 +1,42 @@
# Agent Note: In-repository Landlock release
Status: implemented
English | [中文](2026-08-06-in-repository-landlock-release.zh.md)
## Problem
The `node-addon-landlock-run` source already lives beside its DeepSeek Harness consumers under `native/landlock-run`, but it previously kept a separate pnpm workspace and lockfile and depended on a standalone repository for npm publication. Harness packages consumed a fixed registry version, so one pull request could change the launcher contract and its consumer without testing those changes together. The source repository's native workflow could rehearse the package, but it did not publish the artifact it tested.
The mirror also duplicated release coordination: export the source, update another lockfile, run another release workflow, publish the native family, then return to this repository to bump registry dependencies. That split made source-to-binary provenance, rollback, and security-fix coordination harder without changing what npm users actually needed.
The consolidation must preserve platform selection. The public distribution is deliberately one JavaScript entry package plus separate Linux x64 and arm64 binary packages; merging repository ownership does not imply putting every binary into one tarball or publishing every DeepSeek Harness package at the launcher version.
## Decision
`native/landlock-run` and `native/landlock-run/packages/*` belong to the repository's root pnpm workspace and use the root `pnpm-lock.yaml`. Harness consumers declare `node-addon-landlock-run` with `workspace:*`, so development, type checking, builds, and pull-request tests resolve the entry package from the same checkout. The root TypeScript project graph builds that entry package before consumers, and the repository cleaner owns its direct `lib/` output.
The public npm boundary remains three packages with one launcher-family version: `node-addon-landlock-run`, `node-addon-landlock-run-linux-x64`, and `node-addon-landlock-run-linux-arm64`. The entry package retains both platform packages as `optionalDependencies`; their `os` and `cpu` manifest fields let npm install only the compatible package. Repository constraints allow public publication only for those three names, require `publishConfig.access: public`, and require their versions to match the private launcher workspace root. Other repository workspaces remain private under the existing constraint.
The main repository owns both native CI and publication. `Landlock Run` runs for relevant pull requests and `master` pushes and builds each platform on its matching native runner. The manually dispatched `Landlock Run Release` workflow builds both platform binaries, transfers them as workflow artifacts, assembles and verifies the complete package family, packs immutable npm tarballs, installs and exercises those tarballs, and only then permits the protected publish job. Platform tarballs publish before the entry tarball that optionally depends on them. Publication uses `landlock-run-vX.Y.Z` tags so launcher releases cannot collide with other release families in the monorepo; prereleases use the npm `next` dist-tag.
The sandbox packed-install rehearsal no longer permits the npm registry to supply the launcher. It packs the current checkout's entry and matching native package alongside the harness dependency closure, installs those local tarballs into an external plain-Node consumer, and proves that the installed launcher is executable, byte-identical to the native build, and the correct ELF architecture before testing confinement or fail-closed behavior.
## Alternatives considered
- **Keep the standalone repository as a release mirror** — rejected because it preserves the split lockfiles, source export, stale-registry test window, and cross-repository release sequence after the source of record has already moved here.
- **Publish one npm package containing every platform binary** — rejected because users would download binaries they cannot run and npm could no longer use package-level `os`/`cpu` filtering. Repository ownership and npm package layout are separate choices.
- **Give the launcher the root DeepSeek Harness version and publish the complete monorepo recursively** — rejected because this change owns one three-package public family, not the independent `@deepseek-ai/*` baseline. The [artifact-first npm baseline proposal](../../proposed/process/2026-08-04-artifact-first-npm-baseline-publication.md) explicitly keeps native workspaces outside its target set.
- **Cross-compile both binaries in one release job** — rejected because the checked-in package matrix already assigns each architecture a native GitHub runner and avoids adding a cross-toolchain trust surface.
## Consequences
Launcher protocol, TypeScript entry code, native source, harness consumption, and publish-path tests can change in one pull request and resolve from one lockfile. A release tag now identifies the source, consumer integration, build instructions, and tarballs tested by the main repository. The standalone mirror is no longer part of the release path and can be archived after the first successful in-repository publication.
npm consumers keep the same install command and package names. A supported Linux host downloads the entry package and its matching architecture package; the other architecture package is skipped. An unsupported host receives no platform binary and follows the existing deterministic fail-closed probe path.
The implementation touches more files than a dependency-line edit because the repository must also own workspace constraints, TypeScript build order, cleanup, CI triggers, release tags, lockfile generation, packed-install provenance, release documentation, and generated notices. The behavioral boundary stays narrow: it changes only the Landlock package family and its three direct workspace consumers, not the version or publication state of other DeepSeek Harness packages.
The main repository's `npm-publish` environment must authorize npm trusted publishing or provide `NPM_TOKEN`; moving workflow code cannot configure those external settings. npm still publishes packages sequentially and offers no cross-package transaction, so a failed publish can leave a partial version. Because npm rejects an already-published name and version, an operator must inspect the registry and publish only the missing tarballs rather than rerunning the workflow unchanged. Linux x64 and arm64 runners remain the authoritative binary and real-kernel checks; a macOS checkout can verify the entry package and unsupported-platform behavior but cannot replace those jobs.
This note supersedes only the release-mirror and registry-pinned source-development statements in the [sandbox Agent Note](../feature/2026-07-06-sandbox.md); that note continues to own sandbox behavior, runner selection, and enforcement semantics.

View File

@@ -0,0 +1,42 @@
# Agent Note: 仓库内 Landlock 发布
Status: implemented
[English](2026-08-06-in-repository-landlock-release.md) | 中文
## 问题
`node-addon-landlock-run` 源码已经与其 DeepSeek Harness 消费方一同位于 `native/landlock-run` 下,但此前仍保留独立的 pnpm workspace 和锁文件,并依赖一个独立仓库发布到 npm。Harness 包使用 npm 注册表中的固定版本,因此同一个 PRPull Request可以同时修改启动器契约及其消费方却无法一起测试这些改动。源码仓库的原生工作流可以演练打包流程但不会发布它实际测试过的产物。
发布镜像还造成重复的发布协调工作导出源码、更新另一份锁文件、运行另一套发布工作流、发布原生包家族然后回到本仓库更新注册表依赖。npm 用户的实际需求并未改变,这种拆分却增加了从源码到二进制的溯源、回滚和安全修复协调难度。
此次整合必须保留平台选择机制。公开分发有意采用一个 JavaScript 入口包,并为 Linux x64 和 arm64 分别提供二进制包;合并仓库归属并不意味着要把所有二进制文件放进同一个 tarball也不意味着要按照启动器版本发布所有 DeepSeek Harness 包。
## 决策
`native/landlock-run``native/landlock-run/packages/*` 属于仓库根 pnpm workspace并使用根 `pnpm-lock.yaml`。Harness 消费方将 `node-addon-landlock-run` 声明为 `workspace:*`,因此开发、类型检查、构建和 PR 测试都会从同一个 checkout 解析入口包。根 TypeScript 项目图会先构建该入口包,再构建消费方;仓库清理器负责清理其直接生成的 `lib/` 输出目录。
公开 npm 分发边界仍由 3 个包组成,它们共用一个启动器包家族版本:`node-addon-landlock-run``node-addon-landlock-run-linux-x64``node-addon-landlock-run-linux-arm64`。入口包继续通过 `optionalDependencies` 声明两个平台包;它们在 manifest元数据清单中的 `os``cpu` 字段让 npm 只安装兼容的包。仓库约束只允许公开发布这 3 个包名,要求设置 `publishConfig.access: public`,并要求其版本与私有启动器 workspace 根包一致。仓库中的其他 workspace 仍受现有约束保护,保持私有状态。
主仓库同时负责原生 CI 和发布。`Landlock Run` 会为相关 PR 和 `master` 推送运行,并在各自匹配的原生 runner 上构建每个平台包。手动触发的 `Landlock Run Release` 工作流会构建两个平台的二进制文件,将其作为工作流产物传递,组装并验证完整的包家族,打包出内容不可变的 npm tarball安装并实际运行这些 tarball之后才允许受保护的发布作业执行。发布顺序是平台 tarball 在前,最后发布将它们列为可选依赖的入口 tarball。发布使用 `landlock-run-vX.Y.Z` tag避免启动器版本与 monorepo 中其他发布家族发生冲突;预发布版本使用 npm 的 `next` dist-tag。
沙箱打包安装演练不再允许 npm 注册表提供启动器。它会将当前 checkout 的入口包、匹配的原生包和 harness 依赖闭包一起打包,把这些本地 tarball 安装到仓库外部的纯 Node 消费方中,并在测试约束效果或失败闭合行为之前,证明所安装的启动器可执行、与原生构建产物字节完全一致,且具有正确的 ELF 架构。
## 曾考虑的替代方案
- **保留独立仓库作为发布镜像**:不予采纳,因为在权威源码已经迁入本仓库后,这仍会保留拆分的锁文件、源码导出、测试使用陈旧注册表版本的时间窗,以及跨仓库发布序列。
- **发布一个包含所有平台二进制文件的 npm 包**:不予采纳,因为用户会下载无法在其主机上运行的二进制文件,而且 npm 无法再利用包级 `os``cpu` 筛选。仓库归属与 npm 包布局是两个彼此独立的选择。
- **让启动器使用 DeepSeek Harness 根版本,并递归发布整个 monorepo**:不予采纳,因为本次改动负责的是一个由 3 个包组成的公开包家族,而不是独立的 `@deepseek-ai/*` 基线。[产物优先的 npm 基线提案](../../proposed/process/2026-08-04-artifact-first-npm-baseline-publication.md)明确将原生 workspace 排除在其目标集合之外。
- **在一个发布作业中交叉编译两个二进制文件**:不予采纳,因为仓库内已提交的包矩阵已经为每种架构分配了原生 GitHub runner无需再把交叉工具链纳入信任边界。
## 后果
同一个 PR 可以同时修改启动器协议、TypeScript 入口代码、原生源码、harness 消费方式和发布路径测试,并从同一份锁文件解析这些内容。发布 tag 现在标识源码、消费方集成、构建指令,以及主仓库测试过的 tarball。第一次成功从本仓库发布后独立镜像便不再属于发布路径可以归档。
npm 消费方继续使用相同的安装命令和包名。受支持的 Linux 主机会下载入口包及与其架构匹配的包,并跳过另一架构的包。不受支持的主机不会收到平台二进制文件,并继续沿用现有的确定性失败闭合探测路径。
实现涉及的文件比只修改一行依赖更多,因为仓库还必须负责 workspace 约束、TypeScript 构建顺序、清理、CI 触发条件、发布 tag、锁文件生成、打包安装来源证明、发布文档和生成的第三方声明。行为边界仍然很窄此次改动只影响 Landlock 包家族及其 3 个直接 workspace 消费方,不改变其他 DeepSeek Harness 包的版本或发布状态。
主仓库的 `npm-publish` 环境必须授权 npm trusted publishing或提供 `NPM_TOKEN`只迁移工作流代码无法配置这些外部设置。npm 仍会按顺序发布各个包,且不提供跨包事务,因此发布失败可能留下只完成了一部分的版本。由于 npm 会拒绝已经发布的同名同版本包,操作人员必须检查注册表并只发布缺失的 tarball而不能原样重新运行工作流。Linux x64 和 arm64 runner 仍提供权威的二进制构建与真实内核检查macOS checkout 可以验证入口包和不受支持平台上的行为,但不能取代这些作业。
本说明仅取代[沙箱 Agent Note](../feature/2026-07-06-sandbox.md)中有关发布镜像和开发源码时依赖注册表固定版本的表述;该 Agent Note 仍负责沙箱行为、runner 选择和强制执行语义。

View File

@@ -0,0 +1,170 @@
# Build and publish the node-addon-landlock-run package family from the
# harness source of record. Rehearsal and publication consume the same packed
# tarballs; each native binary is built on its matching architecture.
name: Landlock Run Release
on:
workflow_dispatch:
inputs:
publish:
description: Publish packed tarballs to npm. Must run from a landlock-run-v* tag.
required: true
type: boolean
default: false
permissions:
contents: read
concurrency:
# Stable/prerelease dist-tags are shared registry state; serialize release
# runs so two versions cannot race the final tag assignment.
group: ${{ github.workflow }}
cancel-in-progress: false
defaults:
run:
working-directory: native/landlock-run
jobs:
matrix:
name: Matrix
runs-on: ubuntu-24.04
outputs:
prebuilds: ${{ steps.matrix.outputs.prebuilds }}
steps:
- uses: actions/checkout@v4
- id: matrix
run: echo "prebuilds=$(node ./scripts/github-matrix.mjs release-prebuild)" >> "$GITHUB_OUTPUT"
build-prebuilds:
name: ${{ matrix.package }}
needs: matrix
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.matrix.outputs.prebuilds) }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --filter node-addon-landlock-run-workspace... --frozen-lockfile
- name: Install musl toolchain
run: |
sudo apt-get update -q
sudo apt-get install -yq musl-tools
- name: Build native binaries
run: pnpm build:native
- name: Verify binary metadata
run: node ./scripts/verify-launcher-binary.mjs ${{ matrix.dir }}
- name: Upload prebuild artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: native/landlock-run/${{ matrix.dir }}/bin/*
if-no-files-found: error
retention-days: 7
pack:
name: Pack npm tarballs
needs: build-prebuilds
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --filter node-addon-landlock-run-workspace... --frozen-lockfile
- name: Build TypeScript
run: pnpm build:ts
- name: Verify release version
run: node ./scripts/verify-release.mjs
env:
RELEASE_PUBLISH: ${{ inputs.publish }}
- name: Download prebuild artifacts
uses: actions/download-artifact@v4
with:
pattern: prebuild-*
path: native/landlock-run/.release/prebuild-artifacts
- name: Assemble and verify prebuilds
run: node ./scripts/assemble-prebuilds.mjs .release/prebuild-artifacts
- name: Verify release payload
run: node ./scripts/verify-release.mjs --prebuilds
env:
RELEASE_PUBLISH: ${{ inputs.publish }}
- name: Pack release tarballs
run: node ./scripts/pack-release.mjs dist/npm
- name: Verify packed install
run: node ./scripts/verify-packed-install.mjs dist/npm
env:
NALR_REQUIRE_LANDLOCK: 1
- name: Upload npm tarballs
uses: actions/upload-artifact@v4
with:
name: npm-tarballs
path: native/landlock-run/dist/npm/*
if-no-files-found: error
retention-days: 7
publish:
name: Publish to npm
if: inputs.publish
needs: pack
runs-on: ubuntu-24.04
environment: npm-publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Download npm tarballs
uses: actions/download-artifact@v4
with:
name: npm-tarballs
path: native/landlock-run/dist/npm
- name: Publish tarballs
run: |
version="${GITHUB_REF#refs/tags/landlock-run-v}"
tag_args=()
case "$version" in *-*) tag_args=(--tag next);; esac
while IFS= read -r tarball; do
npm publish "dist/npm/${tarball}" --access public "${tag_args[@]}"
done < dist/npm/publish-order.txt
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,15 +1,27 @@
# Manually-dispatched CI for the landlock-run source of record
# (native/landlock-run). A separate workflow from ci.yml on purpose: the
# subtree is a self-contained pnpm workspace with its own gates, exercised on
# demand — per-architecture native legs (build + behavioral tests + pack
# rehearsal on real kernels) plus one darwin leg proving the documented
# degradation on hosts without a platform package. Legs derive from the
# subtree's checked-in package matrix (scripts/github-matrix.mjs). Packing
# for npm happens in the release mirror (node-addon-landlock-run) after an
# export — see native/README.md; this workflow never packs for release.
# CI for the landlock-run packages under native/landlock-run. A separate
# workflow from ci.yml keeps the native OS/architecture matrix independent of
# the harness Node matrix. Release assembly and publication use the companion
# Landlock Run Release workflow.
name: Landlock Run
on:
pull_request:
paths:
- '.github/workflows/landlock-run.yml'
- '.github/workflows/landlock-run-release.yml'
- 'native/landlock-run/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
push:
branches: [master]
paths:
- '.github/workflows/landlock-run.yml'
- '.github/workflows/landlock-run-release.yml'
- 'native/landlock-run/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
workflow_dispatch:
concurrency:
@@ -52,16 +64,16 @@ jobs:
- uses: pnpm/action-setup@v4
with:
package_json_file: native/landlock-run/package.json
package_json_file: package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: native/landlock-run/pnpm-lock.yaml
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --filter node-addon-landlock-run-workspace... --frozen-lockfile
- name: Install musl toolchain
run: |
@@ -103,16 +115,16 @@ jobs:
- uses: pnpm/action-setup@v4
with:
package_json_file: native/landlock-run/package.json
package_json_file: package.json
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: native/landlock-run/pnpm-lock.yaml
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --filter node-addon-landlock-run-workspace... --frozen-lockfile
- name: Build TypeScript
run: pnpm build:ts

View File

@@ -3,9 +3,8 @@
# .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md.
# A separate workflow from ci.yml because the axis is different — these jobs
# fan out over OS×runner (kernel capabilities), not node versions. The Landlock
# launcher arrives from the registry with `pnpm install` (the npm package family
# `node-addon-landlock-run`, built and released from its own repository), so
# these legs exercise the true consumer path — nothing is compiled here.
# launcher is built from native/landlock-run on each Landlock leg and installed
# from the same tarballs the main-repository release workflow publishes.
name: Sandbox
on:
@@ -30,7 +29,7 @@ jobs:
# an OS×runner matrix — bwrap and Landlock on Linux (separate legs: the
# Landlock files force the bwrap rung off, so each leg proves exactly one
# rung; Landlock twice, once per architecture, each confining through the
# registry-installed launcher), Seatbelt on macOS (sandbox-exec ships with
# locally built launcher), Seatbelt on macOS (sandbox-exec ships with
# the OS). One node
# version only: kernel confinement does not vary by node, and ci.yml's
# node matrix already covers the node axis.
@@ -80,6 +79,14 @@ jobs:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \
|| echo "apparmor userns knob absent — the functional probe decides"
- name: Build Landlock launcher for this architecture
if: matrix.runner == 'landlock'
run: |
sudo apt-get update -q
sudo apt-get install -yq musl-tools
pnpm --dir native/landlock-run run build:ts
pnpm --dir native/landlock-run run build:native
# The unit suite runs on ubuntu in `checks`; this is the one darwin leg
# in the workflow, so run it here too — the platform-dependent unit
# expectations (Seatbelt path canonicalization: /tmp IS /private/tmp)
@@ -105,14 +112,10 @@ jobs:
# the very platform that exists to prove it) is a failure, not a pass.
echo "$out" | grep -qE 'Test Files[[:space:]]+2 passed \(2\)'
# Publish-path rehearsal, Landlock legs only (the pack gates need built
# lib/). The e2e packs the workspace closure, installs the tarballs
# into a throwaway consumer — npm pulling `node-addon-landlock-run`
# and its platform package from the registry, the true consumer path —
# and confines through the INSTALLED launcher, asserting it executable
# apart (a mode-stripped binary must not masquerade as a non-enforcing
# kernel). Same no-silent-skip guard as above.
- name: Build packages (lib/ for the pack rehearsal)
# Publish-path rehearsal, Landlock legs only. Build the launcher on its
# native architecture, then install the local native and harness tarballs
# together so registry state cannot mask source/package drift.
- name: Build packages for the pack rehearsal
if: matrix.runner == 'landlock'
run: pnpm run build

View File

@@ -170,6 +170,6 @@ Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the developm
| --- | --- | --- |
| [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg) | MIT | invoked by `scripts/build-exe-for-python-sdk.ts` to assemble the single-file SDK runtime executable |
## First-party sibling releases
## First-party native packages
`node-addon-landlock-run` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
`node-addon-landlock-run` (and its platform packages) is built and released from this repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.

View File

@@ -5,6 +5,7 @@
],
"ignoreBinaries": [
"bwrap",
"musl-gcc",
"python3",
"sandbox-exec",
"taskkill"

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write native/README.md
README.md: a79d9ca5747d4c4fbfa50745b3eece07b96aea58
README.zh.md: 708430c0a087e5a9da1dae6fd078cb477db65d9b
README.md: 51c8da7b57df15e65b8e431ee18ce6cebb89d54b
README.zh.md: baf530157c6731893a66aea301c6dd962592defc

View File

@@ -2,12 +2,10 @@
English | [中文](README.zh.md)
Source of record for `node-addon-landlock-run`, the Landlock self-restrict-then-exec launcher consumed by the harness. The [`landlock-run/` workspace](landlock-run/README.md) owns its architecture, package family, platform support, development workflow, and release procedure. The standalone repository is a release mirror.
Native source and public packages maintained with DeepSeek Harness. The [`landlock-run/` workspace](landlock-run/README.md) owns the Landlock self-restrict-then-exec launcher consumed by the harness, including its architecture, three-package npm family, platform support, development workflow, and [release procedure](landlock-run/docs/release.md).
## Release mirror
## Workspace and release boundary
| Directory | Mirror repo | Last exported release | Commit |
|---|---|---|---|
| `landlock-run/` | https://github.com/deepseek-harness/node-addon-landlock-run | `v0.0.1` | `614f7fd7dc11e6eaceefba9e7ff1fbe28b51ba22` |
`landlock-run/` and its packages belong to the repository's root pnpm workspace and lockfile. Harness consumers use the current workspace entry package during development and CI, so a launcher contract change and its consumer update can land and be tested together.
The subtree is a self-contained pnpm workspace and is not part of the harness workspace. The [launcher release reference](landlock-run/docs/release.md) owns the export and publication workflow. The mirror must not diverge: port any direct mirror hotfix back here before the next export.
The main repository's `Landlock Run` workflow builds and tests each supported architecture. `Landlock Run Release` assembles those native artifacts, packs and verifies the three npm tarballs, then optionally publishes them under one launcher version. The entry package retains platform packages as npm optional dependencies, so npm still installs only the package matching the user's operating system and CPU.

View File

@@ -2,12 +2,10 @@
[English](README.md) | 中文
`node-addon-landlock-run` 的真源;它是供 harness 使用、先施加 Landlock 自限再执行命令的启动器。[`landlock-run/` workspace](landlock-run/README.md)负责其架构、包家族、平台支持、开发工作流和发布流程。独立仓库是发布镜像
与 DeepSeek Harness 一同维护的原生源码和公开包。[`landlock-run/` workspace](landlock-run/README.md)负责 harness 使用的 Landlock 自限后执行启动器,包括其架构、由三个包组成的 npm 包家族、平台支持、开发工作流和[发布流程](landlock-run/docs/release.md)
## 发布镜像
## Workspace 与发布边界
| 目录 | 镜像仓库 | 最近导出的版本 | Commit |
|---|---|---|---|
| `landlock-run/` | https://github.com/deepseek-harness/node-addon-landlock-run | `v0.0.1` | `614f7fd7dc11e6eaceefba9e7ff1fbe28b51ba22` |
`landlock-run/` 及其包属于仓库根 pnpm workspace并共用根锁文件。开发和 CI 中的 harness 消费方直接使用当前 workspace 的入口包,因此启动器契约变更与消费方更新可以在同一个改动中落地并一起测试。
该子树是自包含的 pnpm workspace不属于 harness workspace。[启动器发布参考](landlock-run/docs/release.md)负责导出和发布工作流。镜像不得发生分歧:下次导出前,必须把任何直接施加于镜像的热修复移植回此处
主仓库的 `Landlock Run` 工作流为每个受支持架构构建并测试。`Landlock Run Release` 汇集这些原生产物,打包并验证三个 npm tarball随后可选择以同一个启动器版本发布。入口包继续将平台包声明为 npm 可选依赖,因此 npm 仍然只会安装与用户操作系统和 CPU 匹配的包

View File

@@ -1,6 +1,6 @@
# AGENTS.md
This workspace builds `landlock-run`, a Landlock self-restrict-then-exec launcher: a small, auditable confinement binary distributed as prebuilt per-platform npm packages, plus the thin JS entry package that resolves it and speaks its CLI contract. The source of record is the `deepseek-harness` repository's `native/landlock-run/`; the `node-addon-landlock-run` repository is the release mirror this tree is exported to for packing and publishing (procedure: `native/README.md` in the harness repo). Make changes in the source of record, never only in the mirror.
This directory builds `landlock-run`, a Landlock self-restrict-then-exec launcher: a small, auditable confinement binary distributed as prebuilt per-platform npm packages, plus the thin JS entry package that resolves it and speaks its CLI contract. It belongs to the repository's root pnpm workspace and lockfile. The main repository owns native CI, tarball assembly, verification, and npm publication; keep package-family changes coordinated with harness consumers in the same repository.
## Pre-release stance

View File

@@ -4,45 +4,45 @@ Pre-1.0: treat this as a release checklist, not a stability policy.
## Versioning
One version across every package in the repo. Use the bump helper:
The launcher workspace root and its three public packages share one version. Run the bump helper from the repository root:
```sh
pnpm release:bump patch # or minor / major / x.y.z
pnpm --dir native/landlock-run release:bump patch # or minor / major / x.y.z
```
It updates the root and every `packages/*` manifest, refreshes the lockfile (`--ignore-scripts --lockfile-only`), and runs `release:verify`. Explicit versions accept full semver including prereleases (`pnpm release:bump 0.0.0-test.0`); the publish workflow puts prerelease versions under the `next` dist-tag, so `latest` never points at a test build. Keep `workspace:*` dependencies in source; pnpm converts them to concrete versions during pack.
It updates `native/landlock-run/package.json` and every `native/landlock-run/packages/*` manifest, refreshes the repository root lockfile (`--ignore-scripts --lockfile-only`), and runs `release:verify`. Explicit versions accept full semver including prereleases (`pnpm --dir native/landlock-run release:bump 0.0.0-test.0`); the publish workflow puts prerelease versions under the `next` dist-tag, so `latest` never points at a test build. Keep `workspace:*` dependencies in source; pnpm converts them to concrete versions during pack.
Version bumps are normal source changes: open a release PR (or commit) with the manifests and lockfile, merge it, then create the matching `vX.Y.Z` tag from that commit. The publish workflow validates that the tag matches every package version.
Version bumps are normal source changes: open a release PR (or commit) with the launcher manifests and root lockfile, merge it, then create the matching `landlock-run-vX.Y.Z` tag from that commit. The namespace avoids colliding with release tags for other package families in the repository. The publish workflow validates that the tag matches every launcher package version.
```sh
pnpm release:commit patch # bump + stage + commit in one command
git tag v0.0.2
pnpm --dir native/landlock-run release:commit patch # bump + stage + commit in one command
git tag landlock-run-v0.0.2
```
## Preflight
```sh
pnpm install --frozen-lockfile
pnpm build:ts
pnpm typecheck
pnpm test:entry
pnpm --dir native/landlock-run build:ts
pnpm --dir native/landlock-run typecheck
pnpm --dir native/landlock-run test:entry
```
On a Linux host, also rehearse the pack path locally:
```sh
pnpm build:native
pnpm test:launcher
node ./scripts/pack-release.mjs .release/npm --current-platform-only
node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only
pnpm --dir native/landlock-run build:native
pnpm --dir native/landlock-run test:launcher
node native/landlock-run/scripts/pack-release.mjs native/landlock-run/.release/npm --current-platform-only
node native/landlock-run/scripts/verify-packed-install.mjs native/landlock-run/.release/npm --current-platform-only
```
## Publish
Use the `Release` workflow so every binary is built on its matching native runner:
Use the main repository's `Landlock Run Release` workflow so every binary is built on its matching native runner:
1. Run it with `publish=false` (from the release commit) to build all platform binaries, assemble and verify the payloads, pack the tarballs in publish order, rehearse the packed install, and upload the `npm-tarballs` artifact for inspection.
2. Create and push the `vX.Y.Z` tag matching the package versions.
2. Create and push the `landlock-run-vX.Y.Z` tag matching the package versions.
3. Run the same workflow from that tag with `publish=true`.
The workflow publishes only from the final packed tarballs, in `publish-order.txt` order (platform packages before the entry that optionally depends on them). It supports npm trusted publishing through GitHub OIDC; without it, provide an `NPM_TOKEN` secret in the `npm-publish` environment. Packages publish with `--access public`.
@@ -50,9 +50,9 @@ The workflow publishes only from the final packed tarballs, in `publish-order.tx
Manual local fallback (current platform's packages only) — always through `pack-release.mjs`, never `pnpm publish` directly (pnpm's pack path strips the launcher's executable bit; see [packaging.md](packaging.md)):
```sh
node ./scripts/pack-release.mjs dist/npm --current-platform-only
node ./scripts/verify-packed-install.mjs dist/npm --current-platform-only
while IFS= read -r tarball; do npm publish "dist/npm/${tarball}" --access public; done < dist/npm/publish-order.txt
node native/landlock-run/scripts/pack-release.mjs native/landlock-run/dist/npm --current-platform-only
node native/landlock-run/scripts/verify-packed-install.mjs native/landlock-run/dist/npm --current-platform-only
while IFS= read -r tarball; do npm publish "native/landlock-run/dist/npm/${tarball}" --access public; done < native/landlock-run/dist/npm/publish-order.txt
```
Do not commit `.npmrc` files with tokens or registry overrides.

View File

@@ -1,345 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@types/node':
specifier: ^26.0.1
version: 26.0.1
node-addon-landlock-run:
specifier: workspace:*
version: link:packages/entry
tsx:
specifier: ^4.20.6
version: 4.23.0
typescript:
specifier: ^6.0.3
version: 6.0.3
packages/entry:
optionalDependencies:
node-addon-landlock-run-linux-arm64:
specifier: workspace:*
version: link:../linux-arm64
node-addon-landlock-run-linux-x64:
specifier: workspace:*
version: link:../linux-x64
packages/linux-arm64: {}
packages/linux-x64: {}
packages:
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@types/node@26.0.1':
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
tsx@4.23.0:
resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
snapshots:
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.1':
optional: true
'@types/node@26.0.1':
dependencies:
undici-types: 8.3.0
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
fsevents@2.3.3:
optional: true
tsx@4.23.0:
dependencies:
esbuild: 0.28.1
optionalDependencies:
fsevents: 2.3.3
typescript@6.0.3: {}
undici-types@8.3.0: {}

View File

@@ -1,8 +0,0 @@
packages:
- packages/*
# pnpm 10+ blocks any dependency shipping an install/build script until it is
# explicitly reviewed here. Deny by default; esbuild (tsx's bundled native
# binary) genuinely needs its script.
allowBuilds:
esbuild: true

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Bump every package (workspace root + packages/*) to one version, refresh
* the lockfile, and verify. Usage: `pnpm release:bump <major|minor|patch|x.y.z>`.
* Bump the launcher workspace root and packages/* to one version, refresh the
* repository lockfile, and verify. Usage: `pnpm release:bump <major|minor|patch|x.y.z>`.
*/
import fs from 'node:fs';
@@ -11,14 +11,15 @@ import { packageDirs, readJson, root } from './repo.mjs';
const bump = process.argv[2];
const releaseTypes = new Set(['major', 'minor', 'patch']);
const repositoryRoot = path.resolve(root, '../..');
function writeJson(file, value) {
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function run(command, args) {
function run(command, args, cwd = root) {
const result = spawnSync(command, args, {
cwd: root,
cwd,
stdio: 'inherit',
env: { ...process.env, CI: 'true' },
});
@@ -84,7 +85,7 @@ for (const file of files) {
console.log(`${file}: ${targetVersion}`);
}
run('pnpm', ['install', '--ignore-scripts', '--lockfile-only']);
run('pnpm', ['install', '--ignore-scripts', '--lockfile-only'], repositoryRoot);
run('node', ['./scripts/verify-release.mjs']);
console.log(`Release version bumped to ${targetVersion}`);

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env node
/**
* Bump, stage, and commit a release in one command:
* `pnpm release:commit <major|minor|patch|x.y.z>`. The tag stays manual —
* create it from the merged release commit.
* `pnpm release:commit <major|minor|patch|x.y.z>`. The namespaced tag stays
* manual — create it from the merged release commit.
*/
import path from 'node:path';
@@ -35,8 +35,8 @@ run('git', [
'add',
'package.json',
'packages/*/package.json',
'pnpm-lock.yaml',
'../../pnpm-lock.yaml',
]);
run('git', ['commit', '-m', `release: ${version}`]);
run('git', ['commit', '-m', `release(landlock-run): ${version}`]);
console.log(`Committed release ${version}. Create the tag manually: git tag v${version}`);
console.log(`Committed release ${version}. Create the tag manually: git tag landlock-run-v${version}`);

View File

@@ -12,10 +12,10 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const root = fileURLToPath(new URL('..', import.meta.url));
export const packagesRoot = path.join(root, 'packages');
const packagesRoot = path.join(root, 'packages');
/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */
export const E_MACHINE = { x64: 62, arm64: 183 };
const E_MACHINE = { x64: 62, arm64: 183 };
export function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env node
/**
* Release verification. Always: every published package carries one shared
* version, and — when running from a tag or publishing — the `vX.Y.Z` tag
* matches it. With `--prebuilds`: every platform package's declared
* version, and — when running from a tag or publishing — the
* `landlock-run-vX.Y.Z` tag matches it. With `--prebuilds`: every platform package's declared
* binaries exist with the right ELF architecture (run after
* `assemble-prebuilds.mjs` or a local `build:native`).
*/
@@ -10,6 +10,8 @@
import path from 'node:path';
import { packageDirs, platformDirs, readJson, root, verifyPlatformBinaries } from './repo.mjs';
const TAG_PREFIX = 'refs/tags/landlock-run-v';
function verifyVersions() {
const packages = packageDirs().map((dir) => ({
dir,
@@ -26,13 +28,13 @@ function verifyVersions() {
const version = packages[0].manifest.version;
const ref = process.env.GITHUB_REF || '';
const publish = process.env.RELEASE_PUBLISH === 'true';
if (publish && !ref.startsWith('refs/tags/v')) {
throw new Error('publishing requires running the workflow from a v* tag');
if (publish && !ref.startsWith(TAG_PREFIX)) {
throw new Error('publishing requires running the workflow from a landlock-run-v* tag');
}
if (ref.startsWith('refs/tags/v')) {
const tagVersion = ref.slice('refs/tags/v'.length);
if (ref.startsWith(TAG_PREFIX)) {
const tagVersion = ref.slice(TAG_PREFIX.length);
if (tagVersion !== version) {
throw new Error(`tag/version mismatch: tag v${tagVersion}, packages ${version}`);
throw new Error(`tag/version mismatch: tag landlock-run-v${tagVersion}, packages ${version}`);
}
}

View File

@@ -10,6 +10,8 @@
"workspaces": [
"vendor/*",
"packages/*/*",
"native/landlock-run",
"native/landlock-run/packages/*",
"apps/*",
"website"
],

View File

@@ -41,6 +41,6 @@
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"cordis": "^4.0.0-rc.7",
"node-addon-landlock-run": "0.0.0-test.0"
"node-addon-landlock-run": "workspace:*"
}
}

View File

@@ -14,6 +14,9 @@
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../native/landlock-run/packages/entry"
},
{
"path": "../../util/brand"
},

View File

@@ -84,7 +84,7 @@
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"node-addon-landlock-run": "0.0.0-test.0",
"node-addon-landlock-run": "workspace:*",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {

View File

@@ -17,6 +17,9 @@
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../../native/landlock-run/packages/entry"
},
{
"path": "../../llm/llm"
},

View File

@@ -31,7 +31,7 @@
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"node-addon-landlock-run": "0.0.0-test.0",
"node-addon-landlock-run": "workspace:*",
"schemastery": "^3.18.0"
},
"devDependencies": {

View File

@@ -10,7 +10,7 @@ import { launcherPath } from 'node-addon-landlock-run'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
/**
* Keyless backend integration through `confine()` and the registry `landlock-run` launcher, with
* Keyless backend integration through `confine()` and the workspace `landlock-run` launcher, with
* bwrap forced off. Tests assert real world effects; consumer coverage lives in dsh-bash-sandbox.
* Skips when the platform package or enforcing kernel is unavailable. HOME-based workspaces avoid
* Landlock's wholesale `/tmp` grant, so workspace-write proves the workspace-root grant itself.

View File

@@ -7,20 +7,22 @@ import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
/**
* Keyless publish-path rehearsal. It packs the package and workspace peers, installs those exact
* tarballs in an external plain-Node consumer, and lets npm resolve the registry Landlock launcher
* plus its platform package. No tsx, path mapping, or workspace resolution can hide missing files,
* dependency errors, or lost executable modes.
* Keyless publish-path rehearsal. It packs the provider, its workspace peers, and the current
* repository's Landlock entry/platform packages, then installs those exact tarballs in an external
* plain-Node consumer. No registry copy, tsx, path mapping, or workspace resolution can hide
* missing files, dependency errors, or lost executable modes.
*
* The installed launcher must match the host architecture, remain executable, and either confine a
* real process with bwrap disabled or fail closed on a non-enforcing kernel. Skips off Linux or
* before `pnpm run build`; launcher byte provenance belongs to its upstream release pipeline.
* before the harness and native packages are built.
*/
const packageDir = fileURLToPath(new URL('..', import.meta.url))
const repoRoot = fileURLToPath(new URL('../../../..', import.meta.url))
const nativeDir = join(repoRoot, 'native/landlock-run')
const sourceLauncher = join(nativeDir, 'packages', `linux-${process.arch}`, 'bin', 'landlock-run')
/** The closure the consumer needs: the package and its transitive `@deepseek-ai` peers; the launcher family arrives from the registry. */
/** The harness closure the consumer needs; native tarballs are packed through their mode-preserving release script. */
const WORKSPACE_CLOSURE = [
'packages/sandbox/sandbox-local',
'packages/sandbox/sandbox',
@@ -36,6 +38,8 @@ const E_MACHINE = { x64: 62, arm64: 183 }[process.arch as 'x64' | 'arm64']
const packable = process.platform === 'linux'
&& E_MACHINE !== undefined
&& existsSync(join(packageDir, 'lib', 'index.js'))
&& existsSync(join(nativeDir, 'packages/entry/lib/index.js'))
&& existsSync(sourceLauncher)
let consumerDir = ''
let workDir = ''
@@ -57,7 +61,20 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-
consumerDir = mkdtempSync(join(tmpdir(), 'dsh-packed-consumer-'))
workDir = mkdtempSync(join(tmpdir(), 'dsh-packed-work-'))
// Pack each closure member with the exact bytes publish would upload.
const nativePackDest = join(packDest, 'native')
const nativePack = spawnSync('node', ['./scripts/pack-release.mjs', nativePackDest, '--current-platform-only'], {
cwd: nativeDir,
encoding: 'utf8',
timeout: 120_000,
})
expect(nativePack.status, `native pack failed:\n${nativePack.stdout}\n${nativePack.stderr}`).toBe(0)
const nativeTarballs = readFileSync(join(nativePackDest, 'publish-order.txt'), 'utf8')
.trim()
.split('\n')
.map(tarball => join(nativePackDest, tarball))
// Pack each harness closure member with the exact bytes publish would upload.
const tarballs: string[] = []
for (const pkg of WORKSPACE_CLOSURE) {
const pack = spawnSync('pnpm', ['pack', '--pack-destination', packDest], {
@@ -69,6 +86,7 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-
const lines = pack.stdout.trim().split('\n')
tarballs.push(lines[lines.length - 1] as string)
}
tarballs.push(...nativeTarballs)
// Peer ranges resolve to the tarballs; Cordis is pinned to their peer range. Do not omit optional
// dependencies because the launcher selects its OS/CPU package through one.
@@ -124,12 +142,13 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-
await Promise.all([consumerDir, workDir].filter(Boolean).map(dir => rm(dir, { recursive: true, force: true })))
})
it('installs the registry launcher for this host: present, EXECUTABLE, right ELF arch', () => {
it('installs this checkout\'s launcher for the host: present, executable, byte-identical, and right ELF arch', () => {
const installed = join(consumerDir, 'node_modules', `node-addon-landlock-run-linux-${process.arch}`, 'bin', 'landlock-run')
expect(existsSync(installed), 'platform package missing from the installed tree').toBe(true)
// A tarball or extraction step that strips the mode bit would leave the
// probe failing exactly like a non-enforcing kernel — assert it apart.
expect(() => { accessSync(installed, constants.X_OK) }, 'installed launcher is not executable').not.toThrow()
expect(readFileSync(installed), 'installed launcher bytes').toEqual(readFileSync(sourceLauncher))
expect(readFileSync(installed).readUInt16LE(18), 'ELF e_machine').toBe(E_MACHINE)
})

View File

@@ -17,6 +17,9 @@
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../../native/landlock-run/packages/entry"
},
{
"path": "../../llm/llm"
},

103
pnpm-lock.yaml generated
View File

@@ -851,6 +851,34 @@ importers:
specifier: workspace:*
version: link:../packages/context/workspace-context
native/landlock-run:
devDependencies:
'@types/node':
specifier: ^26.0.1
version: 26.1.2
node-addon-landlock-run:
specifier: workspace:*
version: link:packages/entry
tsx:
specifier: ^4.20.6
version: 4.22.4
typescript:
specifier: ^6.0.3
version: 6.0.3
native/landlock-run/packages/entry:
optionalDependencies:
node-addon-landlock-run-linux-arm64:
specifier: workspace:*
version: link:../linux-arm64
node-addon-landlock-run-linux-x64:
specifier: workspace:*
version: link:../linux-x64
native/landlock-run/packages/linux-arm64: {}
native/landlock-run/packages/linux-x64: {}
packages/acp/acp:
dependencies:
'@agentclientprotocol/sdk':
@@ -986,8 +1014,8 @@ importers:
specifier: ^4.0.0-rc.7
version: link:../../../vendor/cordis
node-addon-landlock-run:
specifier: 0.0.0-test.0
version: 0.0.0-test.0
specifier: workspace:*
version: link:../../../native/landlock-run/packages/entry
packages/bash/pwsh-local:
dependencies:
@@ -1306,7 +1334,7 @@ importers:
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
devDependencies:
'@deepseek-ai/dsh-client-runtime':
specifier: workspace:^
@@ -2939,8 +2967,8 @@ importers:
specifier: ^4.0.0-rc.7
version: link:../../../vendor/cordis
node-addon-landlock-run:
specifier: 0.0.0-test.0
version: 0.0.0-test.0
specifier: workspace:*
version: link:../../../native/landlock-run/packages/entry
packages/examples/cli-demo:
devDependencies:
@@ -4219,8 +4247,8 @@ importers:
packages/sandbox/sandbox-local:
dependencies:
node-addon-landlock-run:
specifier: 0.0.0-test.0
version: 0.0.0-test.0
specifier: workspace:*
version: link:../../../native/landlock-run/packages/entry
schemastery:
specifier: ^3.18.0
version: link:../../../vendor/schemastery
@@ -5464,7 +5492,7 @@ importers:
version: link:../loader-smoke
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
devDependencies:
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
@@ -9113,6 +9141,9 @@ packages:
'@types/node@25.9.3':
resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==}
'@types/node@26.1.2':
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
'@types/picomatch@3.0.2':
resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==}
@@ -11011,22 +11042,6 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-addon-landlock-run-linux-arm64@0.0.0-test.0:
resolution: {integrity: sha512-oJsXcC33qKl9mWYx0n9YPJ2pUAoY39PoIX0Gx4lDrSCTEvENFrEaODAsQYNY+eEGpn9YMN7E+FOftvea3/1FqQ==}
engines: {node: '>=20'}
cpu: [arm64]
os: [linux]
node-addon-landlock-run-linux-x64@0.0.0-test.0:
resolution: {integrity: sha512-eXvdfnH/UV55MTZzroKvM3CD68SP5OlCsuth908YOcJOnn0LPD5KJjmBz6ToDlBYjF52NNK62+g7TvmUWjbKWQ==}
engines: {node: '>=20'}
cpu: [x64]
os: [linux]
node-addon-landlock-run@0.0.0-test.0:
resolution: {integrity: sha512-c5qopltRonjW6+VinXYMp4FVi9Sxf8eQEb/9G82EksQQ+JC4CDprv+ko5URWmtyZ3wD4GFdeRTyw1AG5yCwJhQ==}
engines: {node: '>=20'}
node-addon-native-custom-loader@0.1.4:
resolution: {integrity: sha512-DreegO6EoC1JHWYBv3j8Miwp2Zl/CyBeNyoeyCbnEdjyYFEulR4Gcb3wj9fXF7KMDY0ZJ5MWwHcXP8GVNyScnA==}
engines: {node: '>=20'}
@@ -11815,6 +11830,9 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
engines: {node: '>=20.18.1'}
@@ -14189,6 +14207,10 @@ snapshots:
dependencies:
undici-types: 7.24.6
'@types/node@26.1.2':
dependencies:
undici-types: 8.3.0
'@types/picomatch@3.0.2': {}
'@types/prop-types@15.7.15': {}
@@ -14344,13 +14366,13 @@ snapshots:
optionalDependencies:
vite: 8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))':
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
'@vitest/pretty-format@4.1.8':
dependencies:
@@ -16476,17 +16498,6 @@ snapshots:
node-addon-api@7.1.1: {}
node-addon-landlock-run-linux-arm64@0.0.0-test.0:
optional: true
node-addon-landlock-run-linux-x64@0.0.0-test.0:
optional: true
node-addon-landlock-run@0.0.0-test.0:
optionalDependencies:
node-addon-landlock-run-linux-arm64: 0.0.0-test.0
node-addon-landlock-run-linux-x64: 0.0.0-test.0
node-addon-native-custom-loader@0.1.4: {}
node-addon-require-builtin-darwin-arm64@0.1.4:
@@ -17398,6 +17409,8 @@ snapshots:
undici-types@7.24.6: {}
undici-types@8.3.0: {}
undici@7.28.0: {}
unicorn-magic@0.3.0: {}
@@ -17533,7 +17546,7 @@ snapshots:
tsx: 4.22.4
yaml: 2.9.0
vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0):
vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -17541,7 +17554,7 @@ snapshots:
rolldown: 1.0.3
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 25.9.3
'@types/node': 26.1.2
esbuild: 0.28.1
fsevents: 2.3.3
jiti: 2.7.0
@@ -17635,10 +17648,10 @@ snapshots:
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -17655,7 +17668,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -17695,10 +17708,10 @@ snapshots:
transitivePeerDependencies:
- msw
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -17715,7 +17728,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1

View File

@@ -1,6 +1,10 @@
packages:
- vendor/*
- packages/*/*
# The Landlock launcher is developed with its harness consumers but keeps
# its native build and publication scripts under native/landlock-run.
- native/landlock-run
- native/landlock-run/packages/*
# Product assemblies over the package tier; apps/cli owns the `dsh` bin.
- apps/*
- website
@@ -46,14 +50,7 @@ allowBuilds:
# restores the executable bit on node-pty's macOS spawn helper.
'@deepseek-ai/dsh-pty-local@file:packages/pty/pty-local': true
# The Landlock launcher family is our own sibling-repo release, consumed
# fresh (hours old at each coordinated bump) — the release-age quarantine
# would block every such bump, so the family is exempted BY NAME, not by
# pinned version.
minimumReleaseAgeExclude:
- node-addon-landlock-run
- node-addon-landlock-run-linux-arm64
- node-addon-landlock-run-linux-x64
# Cordis release candidates are source-vendored and pinned in vendor/README.md
# during the same-day sync that updates package manifests and the lockfile.
- '@cordisjs/plugin-loader@1.0.0-rc.5'

View File

@@ -15,6 +15,8 @@ const root = resolve(import.meta.dirname, '..')
const workspaceGlobs = [
{ dir: 'vendor', depth: 1 },
{ dir: 'packages', depth: 2 },
{ dir: 'native', depth: 1 },
{ dir: 'native/landlock-run/packages', depth: 1 },
{ dir: 'apps', depth: 1 },
] as const
const vendoredPackages = new Set([
@@ -28,6 +30,11 @@ const vendoredPackages = new Set([
'@cordisjs/plugin-hmr',
'@cordisjs/plugin-logger-console',
])
const publicLandlockPackages = new Set([
'node-addon-landlock-run',
'node-addon-landlock-run-linux-arm64',
'node-addon-landlock-run-linux-x64',
])
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
@@ -55,6 +62,7 @@ interface PackageManifest {
| undefined
>
files?: string[]
publishConfig?: { access?: string }
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
}
@@ -71,6 +79,8 @@ function readJson(path: string): PackageManifest {
const rootManifest = readJson(join(root, 'package.json'))
const repositoryVersion = rootManifest.version
const landlockWorkspaceManifest = readJson(join(root, 'native/landlock-run/package.json'))
const landlockVersion = landlockWorkspaceManifest.version
/** Repo-relative dirs holding a package.json, walked to the configured depth. */
function packageDirs(base: string, depth: number): string[] {
@@ -161,8 +171,19 @@ function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
const errors: string[] = []
const label = manifest.name ?? dir
const isLandlockPackageDir = dir.startsWith('native/landlock-run/packages/')
const isPublicLandlockPackage = isLandlockPackageDir
&& manifest.name !== undefined
&& publicLandlockPackages.has(manifest.name)
if (manifest.private !== true) {
if (isPublicLandlockPackage) {
if (manifest.private === true) {
errors.push(`${label}: published Landlock package must not set "private": true`)
}
if (manifest.publishConfig?.access !== 'public') {
errors.push(`${label}: published Landlock package must set publishConfig.access to "public"`)
}
} else if (manifest.private !== true) {
errors.push(`${label}: package.json must set "private": true`)
}
@@ -187,6 +208,15 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
}
}
if (isLandlockPackageDir) {
if (!isPublicLandlockPackage) {
errors.push(`${label}: unexpected package in the public Landlock package family`)
}
if (manifest.version !== landlockVersion) {
errors.push(`${label}: package.json version must match Landlock workspace version ${landlockVersion ?? '(missing)'}`)
}
}
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
const peer = manifest.peerDependencies?.cordis
const dev = manifest.devDependencies?.cordis

View File

@@ -18,10 +18,10 @@ function write(path: string, content = ''): void {
writeFileSync(path, content)
}
function addProject(root: string, path: string): void {
function addProject(root: string, path: string, outDir = 'lib/types'): void {
write(join(root, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path }] }))
write(join(root, path, 'tsconfig.json'), JSON.stringify({
compilerOptions: { composite: true, outDir: 'lib/types' },
compilerOptions: { composite: true, outDir },
include: ['src'],
}))
write(join(root, path, 'src/index.ts'), 'export {}\n')
@@ -60,6 +60,18 @@ describe('RepositoryCleaner', () => {
expect(existsSync(join(root, 'products/shell/lib'))).toBe(true)
})
it('removes the native Landlock entry output that emits directly to lib', async () => {
const root = fixture()
const entry = 'native/landlock-run/packages/entry'
addProject(root, entry, 'lib')
write(join(root, entry, 'lib/index.js'))
await new RepositoryCleaner(root).clean()
expect(existsSync(join(root, entry, 'lib'))).toBe(false)
expect(existsSync(join(root, entry, 'src/index.ts'))).toBe(true)
})
it('refuses project outputs reached through a symlink outside the repository', async () => {
const root = fixture()
const externalProject = fixture()

View File

@@ -114,6 +114,7 @@ export class RepositoryCleaner {
const outputs = new Set<string>()
const pending = [join(this.root, 'tsconfig.json')]
const visited = new Set<string>()
const nativeEntryOutput = join(this.root, 'native/landlock-run/packages/entry/lib')
while (pending.length > 0) {
const nextConfigPath = pending.pop()
@@ -125,10 +126,14 @@ export class RepositoryCleaner {
const parsed = parseConfig(configPath)
if (parsed.options.outDir !== undefined) {
const typesDirectory = resolve(parsed.options.outDir)
if (basename(typesDirectory) !== 'types') {
const outputDirectory = basename(typesDirectory) === 'types'
? dirname(typesDirectory)
: typesDirectory === nativeEntryOutput
? typesDirectory
: undefined
if (outputDirectory === undefined) {
throw new Error(`clean: expected TypeScript outDir to end in /types: ${repositoryPath(this.root, typesDirectory)}`)
}
const outputDirectory = dirname(typesDirectory)
this.assertRepositoryTarget(outputDirectory)
outputs.add(outputDirectory)
}

View File

@@ -247,13 +247,13 @@ describe('isPermissive', () => {
describe('manifestPatterns', () => {
it('derives globs from the declared members, so a new member area is read', () => {
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([
expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([
'package.json',
'packages/*/*/package.json',
'tools/*/package.json',
'examples/*/package.json',
'native/landlock-run/package.json',
'native/landlock-run/packages/*/package.json',
'examples/*/package.json',
])
})
})

View File

@@ -39,10 +39,7 @@ const DEV_ONLY_AREAS = [
'native/',
] as const
/**
* First-party packages released from sibling repositories under the project's
* own license: reachable from workspace manifests but not third-party.
*/
/** First-party public native packages: reachable at runtime but not third-party. */
const FIRST_PARTY = new Set([
'node-addon-landlock-run',
'node-addon-landlock-run-linux-arm64',
@@ -119,16 +116,13 @@ function readManifest(rel: string): Manifest {
* here, so a new member area (`tools/*`) is read the day it is declared.
* @returns one glob per manifest-bearing location, repository-relative.
*/
export function manifestPatterns(rootMembers: readonly string[], nativeMembers: readonly string[]): string[] {
export function manifestPatterns(rootMembers: readonly string[]): string[] {
return [
'package.json',
...rootMembers.map(member => `${member}/package.json`),
// The demo leaves join the workspace through `examples/package.json`, so
// their own manifests are members of nothing and no glob above reaches them.
'examples/*/package.json',
// `native/landlock-run` is a nested workspace with its own lock file.
'native/landlock-run/package.json',
...nativeMembers.map(member => `native/landlock-run/${member}/package.json`),
]
}
@@ -149,7 +143,7 @@ function workspaceMembers(rel: string): string[] {
* would silently push dev-area manifests into the runtime tier.
*/
function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Set<string> } {
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'), workspaceMembers('native/landlock-run/pnpm-workspace.yaml'))
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'))
const manifests = new Map<string, Manifest>()
const names = new Set<string>()
for (const pattern of patterns) {
@@ -591,9 +585,9 @@ ${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.r
| --- | --- | --- |
${BUILD_TIME_TOOLS.map(tool => `| [\`${tool.name}\`](${tool.repo}) | ${tool.license} | ${tool.role} |`).join('\n')}
## First-party sibling releases
## First-party native packages
\`node-addon-landlock-run\` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
\`node-addon-landlock-run\` (and its platform packages) is built and released from this repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
`
}

View File

@@ -38,6 +38,7 @@
"@cordisjs/plugin-timer": ["./vendor/timer/src"],
"@cordisjs/plugin-hmr": ["./vendor/hmr/src"],
"@cordisjs/plugin-logger-console": ["./vendor/logger-console/src"],
"node-addon-landlock-run": ["./native/landlock-run/packages/entry/src/index.ts"],
"@deepseek-ai/dsh-invariants": ["./packages/support/invariants/src/index.ts"],
"@deepseek-ai/dsh-typert-registry": ["./packages/typert/registry/src/index.ts"],
"@deepseek-ai/dsh-typert-loader": ["./packages/typert/loader/src/index.ts"],

View File

@@ -156,6 +156,7 @@
{ "path": "./packages/bash/bash-env" },
{ "path": "./packages/bash/pwsh-local" },
{ "path": "./packages/bash/tool-pwsh" },
{ "path": "./native/landlock-run/packages/entry" },
{ "path": "./packages/sandbox/sandbox" },
{ "path": "./packages/sandbox/sandbox-local" },
{ "path": "./packages/sandbox/sandbox-policy" },