diff --git a/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.i18n.yaml new file mode 100644 index 0000000000..c11c54f6a9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.i18n.yaml @@ -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/architecture/2026-09-07-prebuilt-system-primitives.md +2026-09-07-prebuilt-system-primitives.md: 183cd3ea984cd779417493f0de17a770f38fc001 +2026-09-07-prebuilt-system-primitives.zh.md: f051bba92910086601aa48a877f360e276df524d diff --git a/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md new file mode 100644 index 0000000000..183cd3ea98 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md @@ -0,0 +1,33 @@ +# Agent Note: Prebuilt system primitives + +Status: implemented + +English | [中文](2026-09-07-prebuilt-system-primitives.zh.md) + +## Problem + +The JSONL writer's `fs-ext` dependency compiled a NAN addon during consumer installation. Native compiler availability and Node module ABI changes therefore affected ordinary installs, including Node 26. The repository already maintained the Landlock launcher and its per-platform publication workflow. + +## Decision + +The independently versioned `@deepseek-ai/node-addon-system` family in [native/system](../../../../native/system/README.md) distributes the existing `landlock-run` executable and a stable Node-API v8 `system.node` addon. Platform packages select OS and CPU; Linux carries distinct glibc and musl addon files. macOS carries the addon without a Landlock executable. Neither the entry nor platform packages compile during installation. + +The package has no root export. The `./landlock-run` JavaScript entry retains Landlock's API and [CLI protocol](../../../../native/system/docs/cli-contract.md). The `./flock` entry loads its addon only when `tryLockExclusive(fd)` is called. It runs `flock(fd, LOCK_EX | LOCK_NB)` in asynchronous native work and captures errno on that worker. The caller owns the descriptor through completion and releases its lock by closing it. Missing bindings reject acquisition rather than granting an unprotected lock. + +The [Session write-lease decision](../feature/2026-08-31-cross-process-session-write-lease.md) continues to own acquisition timing, inode checks, close ownership, and crash semantics. Windows retains its existing koffi semaphore. The browser worker substitutes only the flock subpath; it uses the unchanged `./landlock-run` JavaScript API. + +Source builds explicitly compile the host addon before repository tests and builds that need it. Native CI builds the complete platform payload and tests the same addon bytes across Node releases; Linux also exercises the musl payload in Alpine. Platform prepack rejects malformed or incomplete binaries, and an offline npm install rehearsal checks installed bytes and real lock behavior. Native [tests](../../../../native/system/test/flock.test.js) cover descriptor/process contention, close and crash release, independent errno values, and worker teardown. + +## Alternatives considered + +**Keep NAN and publish one build per Node ABI.** This retains a Node-major build matrix for a binding that needs only stable Node-API operations. The evaluated `fs-ext-extra-prebuilt@2.2.14` selected a Node 25 ABI 141 binary under Node 26 ABI 147; its default-install fallback also exited without building when NAN was hoisted. + +**Bundle fs-ext into the parent tarball.** npm normally still runs bundled dependency installation hooks. Bundling alone neither suppresses compilation nor makes one binary portable across operating systems, CPUs, libc implementations, or Node ABIs. + +**Replace flock with OFD/fcntl locks.** On ordinary Linux filesystems these locks do not necessarily exclude existing flock holders. A tmpfs probe admitted an OFD lock while fs-ext held flock, so this is not a behavior-preserving replacement. + +**Use koffi for the POSIX call.** A synchronous call changes event-loop blocking behavior; reading errno after its asynchronous callback reads the wrong thread's value. A native async-work adapter keeps the syscall result and errno together without another FFI coordination layer. + +## Consequences + +The family owns a small C binding, platform builds, and installed-artifact verification rather than an entire filesystem-extension API. Node-API removes the per-Node-major binary requirement, not OS/CPU/libc requirements. The shared native release includes both capabilities, but importing or using one does not load the other. Landlock binary semantics, Windows locking, and released Session data formats remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.zh.md b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.zh.md new file mode 100644 index 0000000000..f051bba929 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 预编译系统原语 + +Status: implemented + +[English](2026-09-07-prebuilt-system-primitives.md) | 中文 + +## Problem + +JSONL 写入方依赖的 `fs-ext` 在用户安装时编译 NAN addon。因此,原生编译器是否可用以及 Node 模块 ABI 的变化会影响普通安装,包括 Node 26。仓库已经维护了 Landlock 启动器及其按平台发布的工作流。 + +## Decision + +[native/system](../../../../native/system/README.zh.md) 中独立版本的 `@deepseek-ai/node-addon-system` 包族分发既有 `landlock-run` 可执行文件和使用稳定 Node-API v8 的 `system.node` addon。平台包按操作系统和 CPU 选择;Linux 分别携带 glibc 与 musl addon 文件。macOS 携带 addon,但不包含 Landlock 可执行文件。入口包和平台包都不在安装期间编译。 + +包不提供根导出。`./landlock-run` JavaScript 入口保留 Landlock API 和 [CLI 协议](../../../../native/system/docs/cli-contract.md)。`./flock` 入口仅在调用 `tryLockExclusive(fd)` 时加载 addon。它在异步原生工作中执行 `flock(fd, LOCK_EX | LOCK_NB)`,并在该工作线程保存 errno。调用方在完成前持有描述符,并通过关闭它释放锁。绑定缺失时拒绝获取锁,不授予没有保护的锁。 + +[Session 写租约决策](../feature/2026-08-31-cross-process-session-write-lease.zh.md) 继续负责获取时机、inode 校验、关闭所有权和崩溃语义。Windows 保留既有 koffi 信号量。浏览器 worker 仅替换 flock 子路径,使用未经修改的 `./landlock-run` JavaScript API。 + +源码构建在需要 addon 的仓库测试与构建之前显式编译当前宿主 addon。Native CI 构建完整平台产物,并让相同 addon 字节跨 Node 版本测试;Linux 还在 Alpine 中执行 musl 产物。平台 prepack 拒绝格式错误或不完整的二进制,离线 npm 安装演练检查安装字节与真实锁行为。Native [测试](../../../../native/system/test/flock.test.js) 覆盖描述符与进程竞争、关闭和崩溃释放、独立 errno 值及 worker 清理。 + +## Alternatives considered + +**保留 NAN,为每个 Node ABI 发布构建。** 这会为仅需稳定 Node-API 操作的绑定保留 Node 主版本构建矩阵。已评估的 `fs-ext-extra-prebuilt@2.2.14` 在 Node 26 ABI147 下选中 Node 25 ABI141 二进制;默认安装回退还会在 NAN 被提升安装时提前退出而不编译。 + +**将 fs-ext 打入父包 tarball。** npm 默认仍执行 bundled 依赖的安装钩子。仅打包既不能禁止编译,也不能让一个二进制跨操作系统、CPU、libc 实现或 Node ABI 通用。 + +**将 flock 换成 OFD/fcntl 锁。** 在普通 Linux 文件系统上,这些锁不一定排斥既有 flock 持有者。tmpfs 探针在 fs-ext 持有 flock 时仍取得 OFD 锁,因此这不是保持行为的替换。 + +**通过 koffi 执行 POSIX 调用。** 同步调用改变事件循环的阻塞行为;在异步回调后读取 errno 会读到错误线程的值。原生 async-work 适配器把系统调用结果和 errno 保存在一起,无须另加 FFI 协调层。 + +## Consequences + +包族维护小型 C 绑定、平台构建和安装产物验证,而不是整套文件系统扩展 API。Node-API 消除按 Node 主版本分发二进制的要求,但不消除操作系统、CPU 和 libc 要求。统一原生发布包含两项能力,但导入或使用其中一项不会加载另一项。Landlock 二进制语义、Windows 锁和已发布 Session 数据格式保持不变。 diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml index 5db7ce1ba0..f39eb5ce66 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml @@ -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: a6e5639e21ca7140cb0314c9918319e99b1495f6 -2026-07-06-sandbox.zh.md: 5144b3fa719465707d7fb2870d45094b8c07661a +2026-07-06-sandbox.md: 31d96836ad2f932f2abf7d1d76242a711f0de2f6 +2026-07-06-sandbox.zh.md: fdf5f4b1691b4a55fffbd206847c99307e12c9da diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md index a6e5639e21..31d96836ad 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -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 ` / `--rw ` 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 family live at `native/landlock-run`, next to the harness consumers and inside the root pnpm workspace. The [`native/` README](../../../../native/README.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. +The Landlock launcher source and package family live at `native/system`, next to the harness consumers and inside the root pnpm workspace. The [`native/` README](../../../../native/README.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. diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md index 5144b3fa71..fdf5f4b169 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md @@ -64,7 +64,7 @@ OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还 launcher 是一个约 300 行的 C 程序(纯 C11,直接使用 Landlock UAPI——除静态链接的 musl 外无其他库,因此审计面仅为该文件加内核的稳定 syscall 约定):`--ro ` / `--rw ` 授权,`--`,被包装的 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 消费方同仓,并属于根 pnpm workspace。[`native/` README](../../../../native/README.zh.md)负责共享锁文件、原生构建、打包演练和 npm 发布边界。平台二进制由 npm 选择,入口包拥有路径解析、探测、CLI(命令行界面)参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。 +Landlock launcher 源码和包家族位于 `native/system`,与 harness 消费方同仓,并属于根 pnpm workspace。[`native/` README](../../../../native/README.zh.md)负责共享锁文件、原生构建、打包演练和 npm 发布边界。平台二进制由 npm 选择,入口包拥有路径解析、探测、CLI(命令行界面)参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。 后端 profile 共享模式约定但在必要的主机授权上有所不同。Landlock 和 Seatbelt 在 read-only 模式下仅允许 `/dev/null`;workspace-write 还允许各自所需的主机临时目录根。每次包装携带后端特定的拒绝签名。Landlock 在较旧的 ABI 无法管控所有操作时报告 partial enforcement,而成功的 bwrap 和 Seatbelt profile 报告 full enforcement。 diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml index 21322238d5..d95f30747e 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml @@ -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-08-04-claude-code-and-codex-subagent-backends.md -2026-08-04-claude-code-and-codex-subagent-backends.md: 52665a44b654a50b8dc28f4bbd530a0606f8e2d9 -2026-08-04-claude-code-and-codex-subagent-backends.zh.md: f16d2412826db5d0fd3dcaad1281ccd92acdd26e +2026-08-04-claude-code-and-codex-subagent-backends.md: d11b200e95cbb0769ad30ae88cb976c7921c80f8 +2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 5899a27bb8ad592d9b83b7d19e681bef375525fb diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md index 52665a44b6..d11b200e95 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md @@ -34,21 +34,21 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro ## Codex provider -`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.149.1` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision. +`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.153.4` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision. Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the optional model and resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no model, mode, or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session. `turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns Codex action categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic. -For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.149.1 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply. +For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; a request without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply. An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects `start()` with its fixed operation stage. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Independent cleanup failure reports `teardown`; when startup and rollback both fail, the aggregate's top message retains both safe stage lines while the underlying causes remain internal. -Codex 0.149.1 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively. +Codex 0.153.4 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively. ## Claude Code provider -`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.241`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.241 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires. +`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.263`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.263 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires. The public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged through `Options.model`; omission leaves that field absent so native settings choose the model. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own. @@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret. -The Codex evidence pins `@openai/codex@0.149.1`, `codex-cli 0.149.1`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence. +The Codex evidence pins `@openai/codex@0.153.4`, `codex-cli 0.153.4`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence. The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit. -The Claude Code evidence pins Agent SDK 0.3.241, Claude Code 2.1.241, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product. +The Claude Code evidence pins Agent SDK 0.3.263, Claude Code 2.1.263, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product. The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test. diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md index f16d241282..5899a27bb8 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md @@ -34,21 +34,21 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro ## Codex 提供方 -`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.149.1` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。 +`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.153.4` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。 发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把可选模型与已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模型、模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。 `turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责 Codex 行动类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。 -对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.149.1 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。 +对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;若请求没有决策选项列表,则回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。 若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树、等待其退出、移除 stderr observer,然后用固定操作阶段拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,等待整棵进程树退出,并移除 observer。独立清理失败会报告 `teardown`;启动与回滚同时失败时,聚合的顶层消息会保留两条安全阶段说明,而底层 cause 仍只在内部可见。 -Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。 +Codex 0.153.4 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。 ## Claude Code 提供方 -`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.241`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.241 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。 +`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.263`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.263 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。 公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传入 `Options.model`;省略时不设置该字段,由原生设置选择模型。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。 @@ -62,11 +62,11 @@ Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端 每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Codex Loader fixture 会公开两个命名 Codex 实例与工具;Claude Code Loader fixture 会公开默认 Codex 工具以及两个命名 Claude Code 实例与工具。两个 fixture 都包含通用 Job 控制工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。 -Codex 证据会锁定 `@openai/codex@0.149.1`、`codex-cli 0.149.1` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败、进程结果与最终呈现证据。 +Codex 证据会锁定 `@openai/codex@0.153.4`、`codex-cli 0.153.4` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败、进程结果与最终呈现证据。 带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。 -Claude Code 证据会锁定 Agent SDK 0.3.241、Claude Code 2.1.241 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测省略模型继承、两个显式实例模型、确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。 +Claude Code 证据会锁定 Agent SDK 0.3.263、Claude Code 2.1.263 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测省略模型继承、两个显式实例模型、确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。 带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。 diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml index 4c04945294..7e9849194b 100644 --- a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml @@ -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-08-15-product-subagent-noninteractive-permissions.md -2026-08-15-product-subagent-noninteractive-permissions.md: 1bdca6214bf730386e27e922361bb45a3c120ecf -2026-08-15-product-subagent-noninteractive-permissions.zh.md: 9377da8d5f3622f5faafce631ed2c6095d22560c +2026-08-15-product-subagent-noninteractive-permissions.md: 1ec45f9adafa2364f9d0f37f3914d97054942226 +2026-08-15-product-subagent-noninteractive-permissions.zh.md: 80e99d9ee47da859456faf66560d42e73c88cf3d diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md index 1bdca6214b..1ec45f9ada 100644 --- a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md +++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md @@ -32,7 +32,7 @@ Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny ins ### Codex -Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.149.1. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client: +Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.153.4. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client: | Value | `thread/start` fields | Native behavior | | --- | --- | --- | @@ -63,7 +63,7 @@ The foreground consumer presents the stop-reason headline, then the optional dia ## Verification -Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.241 and Claude Code 2.1.241 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.149.1 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter. +Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.263 and Claude Code 2.1.263 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.153.4 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md index 9377da8d5f..80e99d9ee4 100644 --- a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md +++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md @@ -32,7 +32,7 @@ Claude Code 默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支 ### Codex -Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程: +Codex 默认使用 `never`,并接受 Codex 0.153.4 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程: | 值 | `thread/start` 字段 | 原生行为 | | --- | --- | --- | @@ -63,7 +63,7 @@ Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交 ## Verification -包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.149.1 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。 +包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.263 与 Claude Code 2.1.263 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.153.4 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。 ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.i18n.yaml b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.i18n.yaml index 908d48ae05..71a7ed9371 100644 --- a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.i18n.yaml @@ -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-08-31-cross-process-session-write-lease.md -2026-08-31-cross-process-session-write-lease.md: 174c5152ea62e01e30ade9a68b6786638acb8ada -2026-08-31-cross-process-session-write-lease.zh.md: e4246f12f7ed8d8b304ca7f7514117f03f32267b +2026-08-31-cross-process-session-write-lease.md: ef8ebe2de6b231075dee31a9184bcc0a5b323011 +2026-08-31-cross-process-session-write-lease.zh.md: 8a79b6d5327c7bd5d0ca12ac435140bc0951e993 diff --git a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md index 174c5152ea..ef8ebe2de6 100644 --- a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md +++ b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md @@ -10,7 +10,7 @@ The JSONL backend's write-handle claim excluded a second writer only inside one ## Decision -`SessionWriteLease` (packages/session/session-persistence-jsonl/src/lease.ts) holds a kernel lock on `session.lock` beside the log for the whole life of a write handle: POSIX takes a non-blocking `flock(2)` through the pinned native dependency `fs-ext`, and Windows holds a named kernel semaphore (count 1) derived from the canonical lock path (`CreateSemaphoreW` in src/win32.ts beside the existing koffi bindings) — a kernel object with no filesystem footprint, destroyed with its last handle. Contention maps to `SessionAlreadyOwnedError`; the kernel releases the lock when the holder's descriptor or handle closes, including on any process death, so a crashed holder never blocks a successor and no expiry bookkeeping exists. A live but wedged holder keeps the lock until its process exits: expropriating a stalled writer was rejected because its resumed appends would tear the log, and on POSIX removing the lock file remains the explicit forfeit for that case. Because a POSIX lock names an inode rather than a path, acquisition verifies the locked inode is still the file at the lock path and retries otherwise. The lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write — an unmaterialized session leaves no filesystem footprint, and a handle that acquired the lock keeps it through close even when materialization fails; release never removes the lock file, preserving the stable inode later lockers verify against. The browser worker deployment stubs fs-ext to immediate success: it is single-process, so the in-process write claim already excludes every writer. +`SessionWriteLease` (packages/session/session-persistence-jsonl/src/lease.ts) holds a kernel lock on `session.lock` beside the log for the whole life of a write handle: POSIX takes a non-blocking `flock(2)` through the prebuilt `@deepseek-ai/node-addon-system/flock` binding, and Windows holds a named kernel semaphore (count 1) derived from the canonical lock path (`CreateSemaphoreW` in src/win32.ts beside the existing koffi bindings) — a kernel object with no filesystem footprint, destroyed with its last handle. Contention maps to `SessionAlreadyOwnedError`; the kernel releases the lock when the holder's descriptor or handle closes, including on any process death, so a crashed holder never blocks a successor and no expiry bookkeeping exists. A live but wedged holder keeps the lock until its process exits: expropriating a stalled writer was rejected because its resumed appends would tear the log, and on POSIX removing the lock file remains the explicit forfeit for that case. Because a POSIX lock names an inode rather than a path, acquisition verifies the locked inode is still the file at the lock path and retries otherwise. The lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write — an unmaterialized session leaves no filesystem footprint, and a handle that acquired the lock keeps it through close even when materialization fails; release never removes the lock file, preserving the stable inode later lockers verify against. The browser worker deployment stubs the flock entry to immediate success: it is single-process, so the in-process write claim already excludes every writer. ## Alternatives considered @@ -22,8 +22,8 @@ The JSONL backend's write-handle claim excluded a second writer only inside one **Windows exclusive-open sharing mode (`CreateFileW` denying `FILE_SHARE_WRITE`)** — leaves readers untouched but pins the lock file's name and directory while held: CI showed dozens of suites failing their temp-root cleanup with EBUSY because a still-open handle blocks recursive removal, and users deleting a session directory would hit the same wall. The named semaphore keeps kernel arbitration with zero filesystem footprint. -**Hand-rolled ffi for POSIX too (`flock(2)` via koffi)** — avoids the node-gyp install-time build, but means owning both platform lock implementations plus their error mapping; `fs-ext` ships the POSIX code maintained and pinned, and the Windows side reuses the koffi bindings `win32.ts` already owns. +**Hand-rolled ffi for POSIX too (`flock(2)` via koffi)** — binding selection and asynchronous errno handling are governed by the [prebuilt system primitives decision](../architecture/2026-09-07-prebuilt-system-primitives.md). The Windows side retains the koffi bindings `win32.ts` already owns. ## Consequences -Cross-process exclusion costs a node-gyp-compiled native dependency (`fs-ext`, allow-listed in `pnpm-workspace.yaml` `allowBuilds`), one lock file per materialized session that release deliberately leaves in place, and the wedged-holder rule: a stuck process blocks that session's writers until it exits. It buys immediate crash recovery (no waiting period), no renewal traffic, and the removal of every takeover race the TTL design managed rather than prevented. Advisory `flock` is unreliable on some network filesystems (NFSv3); a root on such a mount degrades toward in-process-only exclusion. Deleting a live session's lock file forfeits exclusion on POSIX by design — the harness never does so; the agent-loop resume test uses it deliberately to simulate a wedged first lifecycle, and skips on Windows, where the lock is a kernel object no file operation can forfeit. +Cross-process exclusion requires the platform's prebuilt system binding, one lock file per materialized session that release deliberately leaves in place, and the wedged-holder rule: a stuck process blocks that session's writers until it exits. It buys immediate crash recovery (no waiting period), no renewal traffic, and the removal of every takeover race the TTL design managed rather than prevented. Advisory `flock` is unreliable on some network filesystems (NFSv3); a root on such a mount degrades toward in-process-only exclusion. Deleting a live session's lock file forfeits exclusion on POSIX by design — the harness never does so; the agent-loop resume test uses it deliberately to simulate a wedged first lifecycle, and skips on Windows, where the lock is a kernel object no file operation can forfeit. diff --git a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md index e4246f12f7..8a79b6d532 100644 --- a/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md +++ b/.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md @@ -10,7 +10,7 @@ JSONL 后端的写句柄认领只在单个后端实例内部排除第二个写 ## Decision -`SessionWriteLease`(packages/session/session-persistence-jsonl/src/lease.ts)在日志旁的 `session.lock` 上持有内核锁,贯穿写句柄的整个生命期:POSIX 经由固定版本的原生依赖 `fs-ext` 以非阻塞 `flock(2)` 加锁,Windows 持有由规范锁路径派生的命名内核信号量(计数 1,`CreateSemaphoreW`,实现在 src/win32.ts 既有 koffi 绑定旁)——零文件系统足迹的内核对象,随最后一个句柄关闭而销毁。竞争映射为 `SessionAlreadyOwnedError`;持有者的描述符或句柄关闭时内核释放锁,包括任何形式的进程死亡,因此崩溃的持有者从不阻塞后继者,也不存在任何过期簿记。活着但卡死的持有者保有锁直到其进程退出:剥夺停顿写入方的所有权被否决,因为其复活后的追加会撕坏日志;POSIX 上删除锁文件仍是该场景的显式放弃手段。由于 POSIX 锁指向 inode 而非路径,获取后会校验所锁 inode 仍是锁路径上的文件,否则重试。锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取——未物化的会话不留任何文件系统足迹,已取得锁的句柄即使物化失败也保有锁直到关闭;释放从不删除锁文件,保住后续加锁者用于校验的稳定 inode。浏览器 worker 部署将 fs-ext 存根为立即成功:它是单进程部署,进程内写认领已排除所有写入方。 +`SessionWriteLease`(packages/session/session-persistence-jsonl/src/lease.ts)在日志旁的 `session.lock` 上持有内核锁,贯穿写句柄的整个生命期:POSIX 经由预编译 `@deepseek-ai/node-addon-system/flock` 绑定 以非阻塞 `flock(2)` 加锁,Windows 持有由规范锁路径派生的命名内核信号量(计数 1,`CreateSemaphoreW`,实现在 src/win32.ts 既有 koffi 绑定旁)——零文件系统足迹的内核对象,随最后一个句柄关闭而销毁。竞争映射为 `SessionAlreadyOwnedError`;持有者的描述符或句柄关闭时内核释放锁,包括任何形式的进程死亡,因此崩溃的持有者从不阻塞后继者,也不存在任何过期簿记。活着但卡死的持有者保有锁直到其进程退出:剥夺停顿写入方的所有权被否决,因为其复活后的追加会撕坏日志;POSIX 上删除锁文件仍是该场景的显式放弃手段。由于 POSIX 锁指向 inode 而非路径,获取后会校验所锁 inode 仍是锁路径上的文件,否则重试。锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取——未物化的会话不留任何文件系统足迹,已取得锁的句柄即使物化失败也保有锁直到关闭;释放从不删除锁文件,保住后续加锁者用于校验的稳定 inode。浏览器 worker 部署将 flock 入口存根为立即成功:它是单进程部署,进程内写认领已排除所有写入方。 ## Alternatives considered @@ -22,8 +22,8 @@ JSONL 后端的写句柄认领只在单个后端实例内部排除第二个写 **Windows 共享模式独占打开(`CreateFileW` 拒绝 `FILE_SHARE_WRITE`)** —— 读者不受影响,但持有期间钉住锁文件的名字与目录:CI 显示数十个套件的临时根清理因仍打开的句柄阻塞递归删除而报 EBUSY,用户删除会话目录也会撞上同一堵墙。命名信号量保住内核仲裁,且文件系统足迹为零。 -**POSIX 也手写 ffi(经 koffi 调 `flock(2)`)** —— 免去 node-gyp 安装期编译,但意味着自有两个平台的锁实现及其错误映射;`fs-ext` 交付了有维护、可固定版本的 POSIX 侧,Windows 侧复用 `win32.ts` 已自有的 koffi 绑定。 +**POSIX 也手写 ffi(经 koffi 调 `flock(2)`)** —— 绑定选择与异步 errno 处理由[预编译系统原语决策](../architecture/2026-09-07-prebuilt-system-primitives.zh.md)规定。Windows 侧保留 `win32.ts` 已有的 koffi 绑定。 ## Consequences -跨进程排他的代价是一个 node-gyp 编译的原生依赖(`fs-ext`,已在 `pnpm-workspace.yaml` 的 `allowBuilds` 列入允许)、每个物化会话一个由释放刻意留下的锁文件,以及卡死持有者规则:卡住的进程阻塞该会话的写入方直到其退出。它换来的是即时崩溃恢复(无等待期)、零续约流量,以及删除了 TTL 设计只能"管理"而非"消除"的全部接管竞态。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠;位于此类挂载上的根目录会退化为仅进程内排他。POSIX 上删除活跃会话的锁文件按设计即放弃排他——harness 自身从不这样做;agent-loop 的 resume 测试刻意用它模拟卡死的第一个生命周期,并在 Windows 上跳过:那里的锁是任何文件操作都无法放弃的内核对象。 +跨进程排他需要对应平台的预编译系统绑定、每个物化会话一个由释放刻意留下的锁文件,以及卡死持有者规则:卡住的进程阻塞该会话的写入方直到其退出。它换来的是即时崩溃恢复(无等待期)、零续约流量,以及删除了 TTL 设计只能"管理"而非"消除"的全部接管竞态。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠;位于此类挂载上的根目录会退化为仅进程内排他。POSIX 上删除活跃会话的锁文件按设计即放弃排他——harness 自身从不这样做;agent-loop 的 resume 测试刻意用它模拟卡死的第一个生命周期,并在 Windows 上跳过:那里的锁是任何文件操作都无法放弃的内核对象。 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index ff4e50c508..56858d05b2 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -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/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: ba63af5a8f1d32035e116b3900eb9d5905f326d0 -2026-08-08-native-windows-pull-request-ci.zh.md: 3a0c8f510f2f8881833633f69d8ac5d7330d5195 +2026-08-08-native-windows-pull-request-ci.md: 690f8e6f9b13fa7e72240a42ff482bd83f9088b0 +2026-08-08-native-windows-pull-request-ci.zh.md: 9efa3cbcf33b6c12e4eed253b6a0546c79b768fe diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index ba63af5a8f..690f8e6f9b 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -32,7 +32,7 @@ Windows durable JSONL paths keep drive roots in native spelling and apply the ex Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write. -Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.149.1 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host. +Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.153.4 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index 3a0c8f510f..9efa3cbcf3 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -32,7 +32,7 @@ Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并 启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。 -Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.149.1 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。 +Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.153.4 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml index 96e9ba396f..5edb76739c 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml @@ -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/process/2026-08-10-npm-release-sequences.md -2026-08-10-npm-release-sequences.md: 6729b506a9cfe7f6d4410dbf0750621901e07293 -2026-08-10-npm-release-sequences.zh.md: 18a30d6dd57c17b8e13cc3a1f71bc91242a391ff +2026-08-10-npm-release-sequences.md: c403964d8de1c158e5949b5e112a038832709874 +2026-08-10-npm-release-sequences.zh.md: d03da4c4485c4807497dfb28b61ab342bb4c8d12 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md index 6729b506a9..c403964d8d 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md @@ -8,7 +8,7 @@ English | [中文](2026-08-10-npm-release-sequences.zh.md) This repository held three unrelated groups of publishable packages and no channel that sent any of them to a registry. -`packages/*/*` and `apps/*` form the runtime surface of `@deepseek-ai/dsh`; `vendor/*` holds nine rescoped Cordis framework packages, each carrying its upstream version; `native/landlock-run/packages/*` holds Linux platform packages with their own workflow. The three differ in version baseline, change rate, and build requirements: dsh moves with the product, vendor moves only when upstream is re-synced or a local modification changes, and native needs a musl toolchain and one build per architecture. Forcing them through one pipeline means every product release republishes the framework and the native binaries. +`packages/*/*` and `apps/*` form the runtime surface of `@deepseek-ai/dsh`; `vendor/*` holds nine rescoped Cordis framework packages, each carrying its upstream version; `native/system/packages/*` holds Linux platform packages with their own workflow. The three differ in version baseline, change rate, and build requirements: dsh moves with the product, vendor moves only when upstream is re-synced or a local modification changes, and native needs a musl toolchain and one build per architecture. Forcing them through one pipeline means every product release republishes the framework and the native binaries. Two hard blockers sat in the way. All 217 workspace manifests set `private: true`, which npm refuses to publish. The subtler one was 933 hand-written `peerDependencies: "^0.0.1"` entries between sibling dsh packages: `pnpm pack` substitutes the `workspace:` protocol but leaves semver ranges alone, and `^0.0.1` means `>=0.0.1 <0.0.2` — it excludes `0.0.2`, and semver excludes prereleases from a range without a prerelease of its own, so it excluded `0.0.1-rc.1` too. Those entries never failed only because the version never left `0.0.1`. @@ -24,7 +24,7 @@ Two hard blockers sat in the way. All 217 workspace manifests set `private: true |---|---|---|---|---| | dsh | Publish set: non-experimental `packages/*/*` + `apps/*`; private experimental packages join only the shared version bump | one version for the publish set, private dsh packages, and workspace root, `0.0.x` | `dsh-v` | `release.yml` (pack) / `release-publish.yml` (publish) | | vendored framework | the nine `vendor/*` packages | each package on its own version line | `vendor--v` (one per package) | `release-vendor.yml` (pack) / `release-vendor-publish.yml` (publish) | -| native | `native/landlock-run/packages/*` | its own `0.0.x` | `landlock-run-v` | `landlock-run-release.yml` | +| native | `native/system/packages/*` | its own `0.0.x` | `node-addon-system-v` | `node-addon-system-release.yml` | All three publish to the `@deepseek-ai` scope on npmjs.com, and access is per sequence rather than per scope: the vendored framework and the native packages are `public`, and the dsh family has been `public` since its own sequence went public on 2026-08-13 ([rationale](../../archived/process/2026-08-13-public-vendor-and-native-sequences.md)). No publish path passes `--access`, because one flag cannot serve sequences that disagree and would override the manifest that owns the level. diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md index 18a30d6dd5..d03da4c448 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md @@ -8,7 +8,7 @@ Status: implemented 这个仓库有三组互不相干的可发布包,却没有任何发布通道把它们送上 registry。 -`packages/*/*` 与 `apps/*` 组成 `@deepseek-ai/dsh` 的运行面;`vendor/*` 是九个 rescope 过的 Cordis 框架包,各自带着上游的版本号;`native/landlock-run/packages/*` 是 Linux 平台包,有自己的 workflow。三组的版本基线、变更节奏和构建要求都不同:dsh 随产品迭代,vendor 只在同步上游或改动本地修改时才动,native 需要 musl 工具链和逐架构构建。把它们塞进一条发布流水线,等于每次产品发版都要重发框架和原生二进制。 +`packages/*/*` 与 `apps/*` 组成 `@deepseek-ai/dsh` 的运行面;`vendor/*` 是九个 rescope 过的 Cordis 框架包,各自带着上游的版本号;`native/system/packages/*` 是 Linux 平台包,有自己的 workflow。三组的版本基线、变更节奏和构建要求都不同:dsh 随产品迭代,vendor 只在同步上游或改动本地修改时才动,native 需要 musl 工具链和逐架构构建。把它们塞进一条发布流水线,等于每次产品发版都要重发框架和原生二进制。 挡路的还有两处硬门。全部 217 个 workspace manifest 都是 `private: true`,`npm publish` 直接拒绝。更隐蔽的是 933 条 dsh 兄弟包之间硬写的 `peerDependencies: "^0.0.1"`:`pnpm pack` 只替换 `workspace:` 协议,不动语义范围,而 `^0.0.1` 等于 `>=0.0.1 <0.0.2`——发 `0.0.2` 落不进去,发 `0.0.1-rc.1` 也落不进去(semver 规定不带预发布段的范围排除预发布版本)。这些条目至今没出事,只因为版本一直停在 `0.0.1`。 @@ -24,7 +24,7 @@ Status: implemented |---|---|---|---|---| | dsh | 发布集:非 experimental 的 `packages/*/*` + `apps/*`;私有实验性包仅加入共享版本 bump | 发布集、私有 dsh 包与 workspace 根共用一个 `0.0.x` | `dsh-v<版本>` | `release.yml`(pack)/ `release-publish.yml`(发布) | | vendored framework | `vendor/*` 九个包 | 每包各自一条版本线 | `vendor-<包名>-v<版本>`(每包一个) | `release-vendor.yml`(pack)/ `release-vendor-publish.yml`(发布) | -| native | `native/landlock-run/packages/*` | 自己的 `0.0.x` | `landlock-run-v<版本>` | `landlock-run-release.yml` | +| native | `native/system/packages/*` | 自己的 `0.0.x` | `node-addon-system-v<版本>` | `node-addon-system-release.yml` | 三组一律发到 npmjs.com 的 `@deepseek-ai` scope,且 access 按序列而非按 scope 区分:vendored 框架与 native 包是 `public`,dsh 族自 2026-08-13 其自身序列公开发布起即为 `public`([理由](../../archived/process/2026-08-13-public-vendor-and-native-sequences.md))。没有任何发布路径传 `--access`——一个选项无法服务级别互不相同的序列,且会覆盖真正拥有该级别的 manifest。 diff --git a/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.i18n.yaml b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.i18n.yaml new file mode 100644 index 0000000000..de9078ed58 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.i18n.yaml @@ -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/testing/2026-09-08-ci-readiness-and-completion.md +2026-09-08-ci-readiness-and-completion.md: 8ea0a5892d78eda16e657334dba2af58b6648d09 +2026-09-08-ci-readiness-and-completion.zh.md: 62a4c64081369a20a576805fb8a465bffff2922d diff --git a/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md new file mode 100644 index 0000000000..8ea0a5892d --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md @@ -0,0 +1,33 @@ +# Agent Note: CI assertions wait for owned completion + +Status: implemented + +English | [中文](2026-09-08-ci-readiness-and-completion.zh.md) + +## Problem + +The [empty master PR run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049) fails while waiting one second for webhook Session creation and five seconds for PowerShell output. Neither test measures a startup latency guarantee. A [separate run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34207864157) shows the same short-budget problem in a desktop worker readiness test and captures a feedback acknowledgement while the composer still holds the submitted command. + +Another [Windows coverage run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34224004885/job/102053583437) reports a null publint child status and an LSP initialization-marker timeout. Their helpers impose five- and three-second limits inside the lane's 90-second test budget. These cases verify publication contents and cancellation behavior rather than cold-start latency. + +## Decision + +The [webhook browser test](../../../../apps/web/tests/github-ready-review.e2e.ts) observes the model request caused by delivery before checking Session registration. The [feedback test](../../../../apps/web/tests/feedback-command.e2e.ts) waits for the empty composer and enabled attachment control before comparing ARIA output. Matching consecutive snapshots cannot prove that the command RPC has settled: its event stream can publish the acknowledgement first. + +The [desktop transaction test](../../../../apps/desktop/tests/project-manager.spec.ts) gives the worker readiness marker the active test's execution budget. Its independent `afterEach` releases and awaits workers before deleting private roots, including when the runner abandons a timed-out test body. The poll observes runner cancellation, and teardown reports transaction failures independently from assertion failures. The [PowerShell tests](../../../../packages/shell/pwsh-local/tests/executor.spec.ts) register each helper-created Context before plugin initialization and dispose those Contexts before deleting temporary directories. The background-input case awaits process completion before checking complete output, completed status, and exit code. Consuming reads remain covered by their separate streaming tests. + +The [publint runner tests](../../../../scripts/publint-all.spec.ts) pass the active test budget to their child and check launch errors and termination signals before interpreting its exit code. The [LSP instance test](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts) uses the same budget for its fixture marker, observes the actual pending `didOpen` write before aborting, and captures the query's rejection before waiting for readiness. Its [server fixture](../../../../packages/lsp/lsp-stdio/tests/fixture-server.ts) publishes the marker after pausing stdin. Teardown captures the instance list, Context, and directory before its first await. + +The [subagent teardown decision](2026-09-07-subagent-teardown-test-budgets.md) owns lifecycle cleanup budgets. The [persistent PowerShell decision](2026-09-07-pwsh-ci-observable-completion.md) owns exact versus inferred terminal readiness; a one-shot process's completion promise has different semantics. + +## Alternatives considered + +**Larger independent waits.** Rejected where a completion promise already exists. A separate polling deadline continues to compete with the execution lane's budget. + +**Refresh the feedback golden.** Rejected: the populated composer and disabled attachment control describe an in-flight submission. The settled expected UI remains the intended behavior. + +**Serialize CI or retry these tests.** Rejected: neither establishes the missing completion condition or releases a blocked child after assertion failure. + +## Consequences + +Readiness and output assertions preserve their original content and ownership checks. Controlled desktop readiness, webhook preflight, command-response, PowerShell output, publint startup, and LSP initialization delays reproduce the original failures and pass with the completion waits. A stalled desktop-worker control still reports a test timeout while proving that teardown drains the child before removing its directory. The execution lane bounds test bodies and cleanup hooks separately; native Windows execution remains necessary to verify PowerShell and process cleanup there. diff --git a/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.zh.md b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.zh.md new file mode 100644 index 0000000000..62a4c64081 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.zh.md @@ -0,0 +1,33 @@ +# Agent Note: CI 断言等待所属操作完成 + +Status: implemented + +[English](2026-09-08-ci-readiness-and-completion.md) | 中文 + +## 问题 + +[master 空 PR 的运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049)在等待 Webhook Session 创建一秒、等待 PowerShell 输出五秒时失败。两个测试都不衡量启动延迟保证。[另一次运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34207864157)在 Desktop worker 就绪测试中暴露了相同的局部短时限问题,并在输入框仍保留已提交命令时截取了反馈确认。 + +另一次 [Windows coverage 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34224004885/job/102053583437)报告了 publint 子进程退出状态为 null,以及 LSP 初始化标记等待超时。对应 helper 在通道的 90 秒测试预算内另设五秒和三秒限制。这些用例验证发布内容与取消行为,不衡量冷启动延迟。 + +## 决策 + +[Webhook 浏览器测试](../../../../apps/web/tests/github-ready-review.e2e.ts)观察投递触发的模型请求后再检查 Session 注册。[反馈测试](../../../../apps/web/tests/feedback-command.e2e.ts)在比较 ARIA 输出前等待输入框清空且附件按钮启用。连续两次快照相同不能证明命令 RPC 已完成:事件流可能先发布确认消息。 + +[Desktop 事务测试](../../../../apps/desktop/tests/project-manager.spec.ts)为 worker 就绪标记使用当前测试的执行预算。独立的 `afterEach` 在删除私有目录前释放并等待 worker,包括运行器放弃超时测试体的情况。轮询观察运行器的取消信号,teardown 独立报告事务失败,不覆盖断言失败。[PowerShell 测试](../../../../packages/shell/pwsh-local/tests/executor.spec.ts)在初始化插件前登记每个 helper 创建的 Context,并在删除临时目录前处置这些 Context。后台输入用例等待进程完成后检查完整输出、完成状态与退出码。消费式读取仍由独立的流式测试覆盖。 + +[publint runner 测试](../../../../scripts/publint-all.spec.ts)将当前测试预算传给子进程,并在解释退出码前检查启动错误和终止信号。[LSP 实例测试](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts)用同一预算等待 fixture 标记,在取消前观察实际尚未完成的 `didOpen` 写入,并在等待就绪前接住查询的 rejection。[服务器 fixture](../../../../packages/lsp/lsp-stdio/tests/fixture-server.ts)在暂停 stdin 后发布标记。Teardown 在首次 await 前捕获实例列表、Context 和目录。 + +[子 Agent 拆卸决策](2026-09-07-subagent-teardown-test-budgets.zh.md)负责生命周期清理预算。[持久 PowerShell 决策](2026-09-07-pwsh-ci-observable-completion.zh.md)负责精确与推断的终端就绪状态;一次性进程的完成 Promise 具有不同语义。 + +## 考虑过的替代方案 + +**增大独立等待时限。** 已有完成 Promise 时不采用。独立轮询期限仍会与执行通道的预算竞争。 + +**刷新反馈 golden。** 不采用:保留内容的输入框与禁用的附件按钮描述了尚未完成的提交。已稳定的预期 UI 仍是目标行为。 + +**串行化 CI 或重试这些测试。** 不采用:两者都不能建立缺失的完成条件,也不能在断言失败后释放阻塞的子进程。 + +## 后果 + +就绪与输出断言保留原有的内容和所有权检查。受控的 Desktop 就绪、Webhook 预检、命令响应、PowerShell 输出、publint 启动及 LSP 初始化延迟可复现原始失败,并在采用完成等待后通过。阻塞 Desktop worker 的控制用例仍报告测试超时,同时证明 teardown 在删除目录前等待子进程退出。执行通道分别限制测试体和清理 hook;PowerShell 和进程清理仍需在原生 Windows 上验证。 diff --git a/.github/workflows/landlock-run.yml b/.github/workflows/landlock-run.yml deleted file mode 100644 index 6379c8cdc1..0000000000 --- a/.github/workflows/landlock-run.yml +++ /dev/null @@ -1,144 +0,0 @@ -# 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: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -env: - # CI runs must never report to the production telemetry endpoint baked - # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). - DSH_TELEMETRY_DISABLED: '1' - -defaults: - run: - working-directory: native/landlock-run - -jobs: - matrix: - name: Matrix - runs-on: ubuntu-24.04 - outputs: - ci: ${{ steps.matrix.outputs.ci }} - steps: - - uses: actions/checkout@v4 - - - id: matrix - run: echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT" - - native: - name: ${{ matrix.platform }} - needs: matrix - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.matrix.outputs.ci) }} - 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 @deepseek-ai/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 TypeScript - run: pnpm build:ts - - - name: Typecheck - run: pnpm typecheck - - - name: Build native binaries (this architecture is the builder of record) - run: pnpm build:native - - - name: Entry tests (keyless) - run: node ./test/entry.test.js - - # NALR_REQUIRE_LANDLOCK: a self-skip on the very platform that exists to - # prove enforcement would be a false green, so an unenforcing kernel - # fails the leg instead of skipping. - - name: Launcher tests (real kernel enforcement) - run: node ./test/launcher.test.js - env: - NALR_REQUIRE_LANDLOCK: 1 - - - name: Pack rehearsal (pack → install → confine, this platform only) - run: | - node ./scripts/pack-release.mjs .release/npm --current-platform-only - node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only - env: - NALR_REQUIRE_LANDLOCK: 1 - - darwin: - name: darwin (no platform package — degradation proof) - runs-on: macos-latest - 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 @deepseek-ai/node-addon-landlock-run-workspace... --frozen-lockfile - - - name: Build TypeScript - run: pnpm build:ts - - - name: Typecheck - run: pnpm typecheck - - - name: Entry tests (keyless) - run: node ./test/entry.test.js - - - name: Launcher tests (must self-skip cleanly) - run: node ./test/launcher.test.js - - - name: Pack rehearsal (entry only — fallback resolution + unusable probe) - run: | - node ./scripts/pack-release.mjs .release/npm --current-platform-only - node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only diff --git a/.github/workflows/landlock-run-release.yml b/.github/workflows/node-addon-system-release.yml similarity index 85% rename from .github/workflows/landlock-run-release.yml rename to .github/workflows/node-addon-system-release.yml index 76d08e9e4c..6c5596232b 100644 --- a/.github/workflows/landlock-run-release.yml +++ b/.github/workflows/node-addon-system-release.yml @@ -1,13 +1,13 @@ -# Build and publish the @deepseek-ai/node-addon-landlock-run package family from the +# Build and publish the @deepseek-ai/node-addon-system 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 +name: Node Addon System Release on: workflow_dispatch: inputs: publish: - description: Publish packed tarballs to npm. Must run from a landlock-run-v* tag. + description: Publish packed tarballs to npm. Must run from a node-addon-system-v* tag. required: true type: boolean default: false @@ -23,7 +23,7 @@ concurrency: defaults: run: - working-directory: native/landlock-run + working-directory: native/system jobs: matrix: @@ -58,9 +58,10 @@ jobs: cache-dependency-path: pnpm-lock.yaml - name: Install dependencies - run: pnpm install --filter @deepseek-ai/node-addon-landlock-run-workspace... --frozen-lockfile + run: pnpm install --filter @deepseek-ai/node-addon-system-workspace... --frozen-lockfile - name: Install musl toolchain + if: runner.os == 'Linux' run: | sudo apt-get update -q sudo apt-get install -yq musl-tools @@ -71,11 +72,18 @@ jobs: - name: Verify binary metadata run: node ./scripts/verify-launcher-binary.mjs ${{ matrix.dir }} + - name: Verify the built entry and flock + run: | + pnpm build:ts + pnpm build:test-oracle + pnpm test:flock + pnpm test:packaging + - name: Upload prebuild artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact }} - path: native/landlock-run/${{ matrix.dir }}/bin/* + path: native/system/${{ matrix.dir }}/bin/** if-no-files-found: error retention-days: 7 @@ -97,7 +105,7 @@ jobs: cache-dependency-path: pnpm-lock.yaml - name: Install dependencies - run: pnpm install --filter @deepseek-ai/node-addon-landlock-run-workspace... --frozen-lockfile + run: pnpm install --filter @deepseek-ai/node-addon-system-workspace... --frozen-lockfile - name: Build TypeScript run: pnpm build:ts @@ -111,7 +119,7 @@ jobs: uses: actions/download-artifact@v4 with: pattern: prebuild-* - path: native/landlock-run/.release/prebuild-artifacts + path: native/system/.release/prebuild-artifacts - name: Assemble and verify prebuilds run: node ./scripts/assemble-prebuilds.mjs .release/prebuild-artifacts @@ -133,7 +141,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: npm-tarballs - path: native/landlock-run/dist/npm/* + path: native/system/dist/npm/* if-no-files-found: error retention-days: 7 @@ -162,7 +170,7 @@ jobs: uses: actions/download-artifact@v4 with: name: npm-tarballs - path: native/landlock-run/dist/npm + path: native/system/dist/npm - name: Configure npm token fallback env: diff --git a/.github/workflows/node-addon-system.yml b/.github/workflows/node-addon-system.yml new file mode 100644 index 0000000000..1097042f7d --- /dev/null +++ b/.github/workflows/node-addon-system.yml @@ -0,0 +1,179 @@ +# CI for the node-addon-system packages under native/system. 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 +# Node Addon System Release workflow. +name: Node Addon System + +on: + pull_request: + paths: + - '.github/workflows/node-addon-system.yml' + - '.github/workflows/node-addon-system-release.yml' + - 'native/system/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + push: + branches: [master] + paths: + - '.github/workflows/node-addon-system.yml' + - '.github/workflows/node-addon-system-release.yml' + - 'native/system/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # CI runs must never report to the production telemetry endpoint baked + # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). + DSH_TELEMETRY_DISABLED: '1' + +defaults: + run: + working-directory: native/system + +jobs: + matrix: + name: Matrix + runs-on: ubuntu-24.04 + outputs: + ci: ${{ steps.matrix.outputs.ci }} + compatibility: ${{ steps.matrix.outputs.compatibility }} + steps: + - uses: actions/checkout@v4 + + - id: matrix + run: | + echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT" + echo "compatibility=$(node ./scripts/github-matrix.mjs compatibility)" >> "$GITHUB_OUTPUT" + + native: + name: ${{ matrix.platform }} + needs: matrix + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.ci) }} + 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 @deepseek-ai/node-addon-system-workspace... --frozen-lockfile + + - name: Install musl toolchain + if: runner.os == 'Linux' + run: | + sudo apt-get update -q + sudo apt-get install -yq musl-tools + + - name: Build TypeScript + run: pnpm build:ts + + - name: Typecheck + run: pnpm typecheck + + - name: Build native binaries (this architecture is the builder of record) + run: pnpm build:native + + - name: Entry tests (keyless) + run: node ./test/entry.test.js + + # NALR_REQUIRE_LANDLOCK: a self-skip on the very platform that exists to + # prove enforcement would be a false green, so an unenforcing kernel + # fails the leg instead of skipping. + - name: Launcher tests (real kernel enforcement) + if: runner.os == 'Linux' + run: node ./test/launcher.test.js + env: + NALR_REQUIRE_LANDLOCK: 1 + + - name: Pack rehearsal (pack → install → confine, this platform only) + run: | + node ./scripts/pack-release.mjs .release/npm --current-platform-only + node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only + env: + NALR_REQUIRE_LANDLOCK: ${{ runner.os == 'Linux' && '1' || '0' }} + + - name: Verify platform payload rules + run: pnpm test:packaging + + - name: Flock behavior (built addon) + run: | + pnpm build:test-oracle + pnpm test:flock + + - name: Upload this platform's built addon and entry + uses: actions/upload-artifact@v4 + with: + name: system-compat-${{ matrix.platform }} + path: | + native/system/packages/*/bin/** + native/system/packages/entry/lib/** + if-no-files-found: error + + - name: Upload independent syscall test oracle + uses: actions/upload-artifact@v4 + with: + name: system-oracle-${{ matrix.platform }} + path: native/system/test/bin/** + if-no-files-found: error + + compatibility: + name: ${{ matrix.platform }} / Node ${{ matrix.node }} (same binary) + needs: [matrix, native] + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.matrix.outputs.compatibility) }} + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Download the original platform build + uses: actions/download-artifact@v4 + with: + name: system-compat-${{ matrix.platform }} + path: native/system/packages + + - name: Download independent syscall test oracle + uses: actions/download-artifact@v4 + with: + name: system-oracle-${{ matrix.platform }} + path: native/system/test/bin + + - name: Restore oracle executable permissions + run: find ./test/bin -type f -name flock-oracle -exec chmod +x {} + + + - name: Test without rebuilding or installing dependencies + run: | + node ./test/link-platform.mjs + node --test ./test/flock.test.js ./test/package-matrix.test.js + + - name: Test the same musl addon without a compiler + if: runner.os == 'Linux' + run: >- + docker run --rm -v "$PWD:$PWD" -w "$PWD" + node:${{ matrix.node }}-alpine + node --test ./test/flock.test.js ./test/package-matrix.test.js diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 143df14caa..cdee5ce196 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -76,8 +76,8 @@ jobs: # are optional, and building them needs a musl toolchain per architecture. - name: Pack the Landlock entry for verification run: | - pnpm --dir native/landlock-run run build:ts - pnpm --dir native/landlock-run/packages/entry pack --pack-destination "$PWD/dist/npm-landlock" + pnpm --dir native/system run build:ts + pnpm --dir native/system/packages/entry pack --pack-destination "$PWD/dist/npm-landlock" - name: Verify packed install run: pnpm run release:verify-packed-install --family dsh --from dist/npm --from dist/npm-vendor --from dist/npm-landlock diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e6662149e..1415d1f9e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -166,8 +166,8 @@ jobs: # are optional, and building them needs a musl toolchain per architecture. - name: Pack the Landlock entry for verification run: | - pnpm --dir native/landlock-run run build:ts - pnpm --dir native/landlock-run/packages/entry pack --pack-destination "$PWD/dist/npm-landlock" + pnpm --dir native/system run build:ts + pnpm --dir native/system/packages/entry pack --pack-destination "$PWD/dist/npm-landlock" - name: Verify packed install run: pnpm run release:verify-packed-install --family dsh --from dist/npm --from dist/npm-vendor --from dist/npm-landlock diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml index d13e4f7c50..97b7e298d7 100644 --- a/.github/workflows/sandbox.yml +++ b/.github/workflows/sandbox.yml @@ -3,7 +3,7 @@ # .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 is built from native/landlock-run on each Landlock leg and installed +# launcher is built from native/system on each Landlock leg and installed # from the same tarballs the main-repository release workflow publishes. name: Sandbox @@ -84,8 +84,8 @@ jobs: 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 + pnpm --dir native/system run build:ts + pnpm --dir native/system 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 diff --git a/.gitignore b/.gitignore index a954fe5d85..fb999f93fb 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ apps/web/dist/ worktrees/ .agents/worktrees/ .typert-*/ +native/system/test/bin/ diff --git a/AGENTS.md b/AGENTS.md index e44772c1b2..a479e76625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// support/ dev/test infrastructure util/ zero-dependency utilities python/ Python SDK/runtime (see python/README.md) -native/ @deepseek-ai/node-addon-landlock-run source of record (see native/README.md) +native/ @deepseek-ai/node-addon-system source of record (see native/README.md) benchmarks/ performance gates .agents/ Agent workflows and Agent Notes (`notes/`) docs/ architecture, generated catalogs, postmortems, cookbook (see docs/AGENTS.md) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 49c2d8ba69..43c4263136 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -71,7 +71,6 @@ External packages that a workspace package resolves at runtime. The tier covers | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | | [`fflate`](https://github.com/101arrowz/fflate) | MIT | -| [`fs-ext`](https://github.com/baudehlo/node-fs-ext) | MIT | | [`immer`](https://github.com/immerjs/immer) | MIT | | [`ipaddr.js`](https://github.com/whitequark/ipaddr.js) | MIT | | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT | @@ -123,18 +122,18 @@ pnpm applies local patches to the following packages at install time, so shipped The project owner authorizes distribution of every version of the official `@anthropic-ai/claude-agent-sdk` package and the official Claude Code CLI/platform payloads that each version declares through `optionalDependencies`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review. -The installed SDK 0.3.241 declares the following optional platform packages. Each carries the official Claude Code 2.1.241 executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host. +The installed SDK 0.3.263 declares the following optional platform packages. Each carries the official Claude Code 2.1.263 executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host. | Optional platform package | Version | Declared license | | --- | --- | --- | -| [`@anthropic-ai/claude-agent-sdk-darwin-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-darwin-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-arm64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64-musl) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-x64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64-musl) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-win32-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-darwin-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-arm64) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-darwin-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-x64) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-arm64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64-musl) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-x64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64-musl) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-win32-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-arm64) | 0.3.263 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.263 | SEE LICENSE IN LICENSE.md | ## Development-only npm dependencies @@ -154,7 +153,6 @@ External packages **directly declared** only by repository tooling, test infrast | [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT | | [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/compression`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | -| [`@types/fs-ext`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/mime-types`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | @@ -224,4 +222,4 @@ Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the developm ## First-party native packages -`@deepseek-ai/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. +`@deepseek-ai/node-addon-system` (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. diff --git a/apps/desktop/scripts/package-target.ts b/apps/desktop/scripts/package-target.ts index 1f32123ec0..64855151d4 100644 --- a/apps/desktop/scripts/package-target.ts +++ b/apps/desktop/scripts/package-target.ts @@ -266,10 +266,10 @@ async function main(): Promise { await runPnpm(['run', 'release:pack', '--family', 'vendor', '--out', buildPaths.packedVendor], buildEnv, REPOSITORY_ROOT) rmSync(buildPaths.packedLandlock, { recursive: true, force: true }) mkdirSync(buildPaths.packedLandlock, { recursive: true }) - await runPnpm(['--dir', 'native/landlock-run', 'run', 'build:ts'], buildEnv, REPOSITORY_ROOT) + await runPnpm(['--dir', 'native/system', 'run', 'build:ts'], buildEnv, REPOSITORY_ROOT) await runPnpm([ '--dir', - 'native/landlock-run/packages/entry', + 'native/system/packages/entry', 'pack', '--pack-destination', buildPaths.packedLandlock, diff --git a/apps/desktop/tests/project-manager.spec.ts b/apps/desktop/tests/project-manager.spec.ts index f5320b47bd..27ac24fce4 100644 --- a/apps/desktop/tests/project-manager.spec.ts +++ b/apps/desktop/tests/project-manager.spec.ts @@ -18,6 +18,7 @@ import type { DesktopRelease } from '../src/release.ts' import { archivePnpmStore } from '../src/seed-store.ts' const roots: string[] = [] +const releaseWorkers: Array<() => Promise> = [] function temporaryRoot(): string { const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-test-')) @@ -162,8 +163,13 @@ function release(version = '1.0.0'): DesktopRelease { } } -afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +afterEach(async () => { + const cleanups = releaseWorkers.splice(0) + const directories = roots.splice(0) + const results = await Promise.allSettled(cleanups.map(cleanup => cleanup())) + for (const root of directories) rmSync(root, { recursive: true, force: true }) + const failures: unknown[] = results.flatMap((result): unknown[] => result.status === 'rejected' ? [result.reason] : []) + if (failures.length > 0) throw new AggregateError(failures, 'desktop worker cleanup failed') }) describe('desktop package policy', () => { @@ -293,7 +299,7 @@ describe('desktop project transactions', () => { expect(existsSync(paths.pending)).toBe(false) }) - it('records the live pnpm worker as transaction owner until it exits', async () => { + it('records the live pnpm worker as transaction owner until it exits', async ({ task, signal }) => { const root = temporaryRoot() const seed = join(root, 'seed') const ready = join(root, 'pnpm-ready') @@ -306,7 +312,19 @@ describe('desktop project transactions', () => { const runtime = { node: process.execPath, pnpm: writeBlockingFakePnpm(root, ready, releaseWorker) } const manager = new DesktopProjectManager(paths, runtime) const installing = manager.applyRelease(seed, '1.0.0', hooks()) - await expect.poll(() => existsSync(ready)).toBe(true) + // Teardown observes failures even if the runner has abandoned the test body. + const completed = installing.then(value => ({ value }), (error: unknown) => ({ error })) + releaseWorkers.push(async () => { + writeFileSync(releaseWorker, 'continue') + const outcome = await completed + if ('error' in outcome) throw outcome.error + }) + // Child startup shares the test budget; an aborted poll must not resume ownership assertions. + await expect.poll(() => { + signal.throwIfAborted() + return existsSync(ready) + }, { timeout: task.timeout }).toBe(true) + signal.throwIfAborted() const workerPid = Number.parseInt(readFileSync(ready, 'utf8'), 10) expect(readFileSync(paths.lock, 'utf8')).toBe(`${String(workerPid)}\n`) const competing = new DesktopProjectManager(paths, runtime) diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 6f8d2aab97..90dc202cf0 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -415,7 +415,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { await page.getByText(SHOWCASE_OLDEST, { exact: true }).waitFor({ timeout: 15_000 }) expect(pageErrors.map(error => error.message)).toEqual([]) expect(consoleErrors.filter(line => - /watchFile|failed to watch|node-addon-landlock-run\.probe|sandbox backend is usable|SANDBOX_UNAVAILABLE/i.test(line))).toEqual([]) + /watchFile|failed to watch|node-addon-system\.probe|sandbox backend is usable|SANDBOX_UNAVAILABLE/i.test(line))).toEqual([]) } catch (error) { await saveFailureShot(page, 'preview-boot') throw pageErrors.length === 0 diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index f3be2fcd39..e617c9aead 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -10,7 +10,7 @@ // open llm seam. import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' -import type { Browser, Page } from 'playwright' +import type { Browser, Locator, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { join } from 'node:path' @@ -190,13 +190,33 @@ describe('web e2e: settings modal and General preferences', () => { expect(tripwire.pageErrors).toEqual([]) }, 60_000) + async function selectTheme(cube: Locator, preference: 'light' | 'dark' | 'system'): Promise { + // Optimistic UI and a file value from an earlier gesture do not prove this write finished. + const [response] = await Promise.all([ + page.waitForResponse((candidate) => { + if (candidate.request().method() !== 'POST' + || new URL(candidate.url()).pathname !== '/api/settings/mutate') return false + const { payload: { args } } = candidate.request().postDataJSON() as { + payload: { args: { ns: string; ops: { op: string; path: string[]; value?: unknown }[] } } + } + return args.ns === 'ui-theme' && args.ops.some(op => op.op === 'set' + && op.path.length === 1 && op.path[0] === 'preference' && op.value === preference) + }, { timeout: 5_000 }), + cube.click(), + ]) + expect(response.ok()).toBe(true) + expect(await response.json()).toMatchObject({ + result: { ok: true, value: { ns: 'ui-theme', value: { preference } } }, + }) + } + it('uses the persisted dark preference while plugins are still loading', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme')) await page.emulateMedia({ colorScheme: 'light' }) await page.getByRole('button', { name: '设置', exact: true }).click() const initialDialog = page.getByRole('dialog', { name: '设置' }) const darkCube = initialDialog.getByRole('button', { name: '深色' }) - await darkCube.click() + await selectTheme(darkCube, 'dark') await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true') await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 }) .toMatch(/ui-theme:\n\s+preference: dark/) @@ -242,7 +262,7 @@ describe('web e2e: settings modal and General preferences', () => { await page.getByRole('button', { name: '设置', exact: true }).click() const restoredDialog = page.getByRole('dialog', { name: '设置' }) const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' }) - await systemCube.click() + await selectTheme(systemCube, 'system') await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true') await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme')), { timeout: 5_000, @@ -291,7 +311,7 @@ describe('web e2e: settings modal and General preferences', () => { await dialog.waitFor({ timeout: 10_000 }) const darkCube = dialog.getByRole('button', { name: '深色' }) expect(await darkCube.getAttribute('aria-pressed')).toBe('false') - await darkCube.click() + await selectTheme(darkCube, 'dark') // The full cascade: pressed state, Host-backed preference, body attribute, // alias token flip — all from one real user gesture. await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true') @@ -340,7 +360,7 @@ describe('web e2e: settings modal and General preferences', () => { // `system` follows the emulated OS scheme (dark stays dark, light clears). await page.getByRole('button', { name: '设置', exact: true }).click() const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' }) - await systemCube.click() + await selectTheme(systemCube, 'system') await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true') await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(false) expectThemeColorSynchronized(await readState()) @@ -349,7 +369,7 @@ describe('web e2e: settings modal and General preferences', () => { expectThemeColorSynchronized(await readState()) // Restore for the specs that follow: light preference beats the emulated // dark OS scheme, leaving the shared page in the light default. - await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '浅色' }).click() + await selectTheme(page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '浅色' }), 'light') await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(false) expectThemeColorSynchronized(await readState()) await page.keyboard.press('Escape') diff --git a/apps/web/tests/workflow-run.e2e.ts b/apps/web/tests/workflow-run.e2e.ts index 0da8f851e0..d665afddb6 100644 --- a/apps/web/tests/workflow-run.e2e.ts +++ b/apps/web/tests/workflow-run.e2e.ts @@ -114,11 +114,13 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () = await runDisclosure.press('Space') expect(await disclosures.count()).toBe(2) expect(await phaseDisclosure.getAttribute('aria-expanded')).toBe('true') - await member.focus() - const lightColor = await member.locator('[data-member-label]').evaluate(element => getComputedStyle(element).color) await page.setViewportSize({ width: 560, height: 800 }) await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') }) + // Exercise keyboard focus after the responsive layout has changed. + await phaseDisclosure.focus() + await phaseDisclosure.press('Tab') + await expect.poll(() => member.evaluate(element => element.matches(':focus-visible'))).toBe(true) const darkNarrow = await page.locator('[data-workflow-run]').evaluate((element) => { const panel = element as HTMLElement panel.style.width = '356px' diff --git a/native/README.i18n.yaml b/native/README.i18n.yaml index 0d7eebd170..74db687acb 100644 --- a/native/README.i18n.yaml +++ b/native/README.i18n.yaml @@ -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: 51c8da7b57df15e65b8e431ee18ce6cebb89d54b -README.zh.md: 6d07c6afb43e2763795a71639707454b9cfbe280 +README.md: 100e25dbfa9f472cb6a5a93a719eb7e1be02d10e +README.zh.md: b1a66ac678334fd535951a7a0e4a921622b8ba51 diff --git a/native/README.md b/native/README.md index 51c8da7b57..100e25dbfa 100644 --- a/native/README.md +++ b/native/README.md @@ -2,10 +2,10 @@ English | [中文](README.zh.md) -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). +Native source and public packages maintained with DeepSeek Harness. The [`system/` workspace](system/README.md) owns the Landlock launcher and POSIX flock binding, their platform packages, and the [release procedure](system/docs/release.md). ## Workspace and release boundary -`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. +`system/` 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 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. +The main repository's `Node Addon System` workflow builds and tests each supported architecture. `Node Addon System Release` assembles those native artifacts, packs and verifies the npm tarballs, then optionally publishes them under one native 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. diff --git a/native/README.zh.md b/native/README.zh.md index 6d07c6afb4..b1a66ac678 100644 --- a/native/README.zh.md +++ b/native/README.zh.md @@ -2,10 +2,10 @@ [English](README.md) | 中文 -与 DeepSeek Harness 一同维护的原生源码和公开包。[`landlock-run/` workspace](landlock-run/README.zh.md) 负责 harness 使用的 Landlock 自限后执行启动器,包括其架构、由三个包组成的 npm 包家族、平台支持、开发工作流和[发布流程](landlock-run/docs/release.md)。 +与 DeepSeek Harness 一同维护的原生源码和公开包。[`system/` workspace](system/README.zh.md) 负责 Landlock 启动器、POSIX flock 绑定、平台包和[发布流程](system/docs/release.md)。 ## Workspace 与发布边界 -`landlock-run/` 及其包属于仓库根 pnpm workspace,并共用根锁文件。开发和 CI 中的 harness 消费方直接使用当前 workspace 的入口包,因此启动器约定变更与消费方更新可以在同一个改动中落地并一起测试。 +`system/` 及其包属于仓库根 pnpm workspace,并共用根锁文件。开发和 CI 中的 harness 消费方直接使用当前 workspace 的入口包,因此启动器约定变更与消费方更新可以在同一个改动中落地并一起测试。 -主仓库的 `Landlock Run` 工作流为每个受支持架构构建并测试。`Landlock Run Release` 汇集这些原生产物,打包并验证三个 npm tarball,随后可选择以同一个启动器版本发布。入口包继续将平台包声明为 npm 可选依赖,因此 npm 仍然只会安装与用户操作系统和 CPU 匹配的包。 +主仓库的 `Node Addon System` 工作流为每个受支持架构构建并测试。`Node Addon System Release` 汇集这些原生产物,打包并验证 npm tarball,随后可选择以同一个原生包版本发布。入口包继续将平台包声明为 npm 可选依赖,因此 npm 仍然只会安装与用户操作系统和 CPU 匹配的包。 diff --git a/native/landlock-run/AGENTS.md b/native/landlock-run/AGENTS.md deleted file mode 100644 index 2b03ddcec7..0000000000 --- a/native/landlock-run/AGENTS.md +++ /dev/null @@ -1,50 +0,0 @@ -# AGENTS.md - -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 implements 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 - -The project is pre-1.0. Prefer the correct public API over compatibility shims: if a package name, exported field, layout, or contract detail is wrong, rename it and update all references in the same change. Do not add deprecated aliases unless a stable release already needs them. - -## Runtime safety rules - -- Every tool must fail closed. If a ruleset cannot be created or the kernel does not enforce it, exit non-zero WITHOUT exec'ing the wrapped command. Never run unconfined as a fallback. -- Runtime binaries and the entry packages take NO environment-variable overrides: which binary confines a process must never be decidable by the ambient environment. Test injection is by function parameter; the `NALR_*` prefix is for build/test orchestration only. -- Kernel UAPI is self-defined in the C source (verbatim from the kernel headers), keeping builds independent of toolchain header vintage and making the definitions part of the audit record. -- No libraries beyond libc, linked statically against musl. The audit surface of a tool is its C source plus the kernel's stable syscall contract. -- The CLI contract of each tool ([docs/cli-contract.md](docs/cli-contract.md)) is the cross-repo compatibility contract: argv grammar, exit codes, and report lines change only with a version bump and a changelog entry, and consumers parse them only through the entry package. -- There is deliberately NO install-time build fallback: a host without a matching platform package gets a nonexistent launcher path, the consumer's probe fails, and the consumer falls closed — that degradation is part of the design, not a gap to fill with node-gyp. - -## Repository layout - -```text -packages/entry/ Published entry package: JavaScript API (resolve/probe/grants) + the C source. -packages/linux-*/ Published per-platform packages: one prebuilt static binary, no JavaScript. -scripts/ Build, matrix derivation, prepack gates, and release orchestration. -test/ Plain-node behavioral tests (entry API + real-kernel launcher proofs). -docs/ Architecture, packaging, CLI contract, release, support matrix, naming. -``` - -## Commands - -```sh -pnpm install -pnpm build:ts # entry packages → lib/ -pnpm build:native # this Linux architecture's binaries (needs musl-tools); fails fast elsewhere -pnpm typecheck -pnpm test # entry tests everywhere; launcher tests need linux + built binary -``` - -## Packaging invariants - -- The package matrix is explicit, checked-in metadata: `packages//package.json` (`os`, `cpu`), `packages//prebuilds.json` (the binaries that may exist there), and [docs/support-matrix.md](docs/support-matrix.md) stay synchronized when the matrix changes. `scripts/github-matrix.mjs` derives CI and release matrices from it; nothing else enumerates platforms. -- Platform package names contain platform only (`-linux-x64`), never tool variants — those stay inside `prebuilds.json`. Static musl linking is why there is no libc suffix: one binary serves glibc and musl distros. -- Platform packages ship no JavaScript; the entry package resolves them to file paths. Backends prove themselves at runtime through the functional probe, never through metadata trust. -- Builds are native-only: each architecture compiles its own binary on its own runner (CI is the builder of record); no cross toolchain enters the repo. -- Every tarball is gated at pack time: platform packages refuse to pack without their declared binaries present, executable, and in the right ELF architecture (`verify-launcher-binary.mjs`), entry packages without built `lib/` (`verify-entry-lib.mjs`), and the release pipeline byte-pins installed binaries against the workspace builds (`verify-packed-install.mjs`). -- Platform tarballs are packed with `npm pack`, never `pnpm pack`: pnpm's pack path strips the executable bit (observed on 11.7.0), shipping a launcher no consumer can spawn. `pack-release.mjs` encodes the split; the rehearsal asserts executability of the installed copy so a regression fails loudly instead of masquerading as a non-enforcing kernel. -- Generated artifacts stay out of git: `packages/*/bin/`, `packages/*/lib/`, `dist/`, `.release/`, `*.tsbuildinfo`. Ignore rules live in the ROOT `.gitignore` only — a package-nested ignore file can silently drop payload from tarballs. - -## Documentation - -User-facing docs are English. Keep the README focused on install, usage, and support status; durable design decisions belong in docs/ alongside the code, and the current implementation belongs in [docs/architecture.md](docs/architecture.md). diff --git a/native/landlock-run/README.md b/native/landlock-run/README.md deleted file mode 100644 index bf9b163d42..0000000000 --- a/native/landlock-run/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run - -English | [中文](README.zh.md) - -A [Landlock](https://landlock.io/) self-restrict-then-exec launcher for confining subprocesses on Linux, distributed as prebuilt per-platform npm packages plus a thin JS entry package that resolves the binary and speaks its CLI contract. Built for agent harnesses and other hosts that need to run untrusted commands under a filesystem allow-list without confining themselves. - -The tool is **`landlock-run`** — a self-restrict-then-exec [Landlock](https://landlock.io/) launcher (~300 lines of C11 over the raw kernel UAPI, statically linked against musl). It installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the command and every process it spawns run confined while the invoking process stays unrestricted. Fail-closed: if the kernel cannot enforce, it exits without running the command. - -## Install - -```sh -npm install @deepseek-ai/node-addon-landlock-run -``` - -Published packages use an entry package plus platform optional packages: - -```text -@deepseek-ai/node-addon-landlock-run -@deepseek-ai/node-addon-landlock-run-linux-x64 -@deepseek-ai/node-addon-landlock-run-linux-arm64 -``` - -npm's `os`/`cpu` fields make installers fetch only the matching platform package. There is no install-time build fallback on purpose: on a host without a platform package the resolved path never exists, the probe reports `unusable`, and the consumer falls closed. - -## Usage - -```js -import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run'; - -const launcher = launcherPath(); -if (probe(launcher) !== 'unusable') { - const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; - // spawn argv with your process runner of choice -} -``` - -The public API is intentionally small: - -- `launcherPath()`: absolute path of this host's launcher (existence deliberately unchecked — the probe is the availability signal). -- `probe(launcher?, { timeoutMs? })`: functional enforcement probe — `'full' | 'partial' | 'unusable'`. -- `grantArgs({ readOnly?, readWrite? })`: the launcher's grant argv; everything not granted is denied. -- `LAUNCHER_BIN` and `LAUNCHER_FAILURE_EXIT` (125): contract constants. A successfully exec'd child may also return 125, so consumers need the fatal diagnostic as well as the status to attribute launcher failure. - -The full binary contract (argv grammar, exit codes, report lines) is pinned in [docs/cli-contract.md](docs/cli-contract.md). - -## Support - -linux-x64 and linux-arm64, kernel with Landlock enabled (5.13+; ABI level determines `full` vs `partial` enforcement — see [docs/support-matrix.md](docs/support-matrix.md)). Other platforms deliberately have no package: consumers run different confinement backends there. - -## Development - -```sh -corepack enable -pnpm install -pnpm build:ts # entry packages → lib/ -pnpm build:native # this Linux architecture's binaries (apt-get install musl-tools) -pnpm test -``` - -Binaries are git-ignored and built natively per architecture — locally for your own machine, by CI's per-arch runners as the builders of record. Release flow: [docs/release.md](docs/release.md). diff --git a/native/landlock-run/README.zh.md b/native/landlock-run/README.zh.md deleted file mode 100644 index 7bd4765a73..0000000000 --- a/native/landlock-run/README.zh.md +++ /dev/null @@ -1,60 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run - -[English](README.md) | 中文 - -一个 [Landlock](https://landlock.io/)「先限制自身、再执行」启动器,用于在 Linux 上限制子进程。它以按平台预构建的 npm 包以及一个轻量 JS 入口包的形式发布;入口包负责解析二进制文件并遵循其 CLI(命令行界面)约定。该启动器面向需要让不可信命令在文件系统允许清单约束下运行、同时保持自身不受限制的 agent harness(智能体框架)和其他宿主。 - -该工具是 **`landlock-run`**:一个「先限制自身、再执行」的 [Landlock](https://landlock.io/) 启动器(基于原始内核 UAPI 编写,约 300 行 C11,并与 musl 静态链接)。它在自身上安装 Landlock 规则集,再 `exec` 被包装的命令;该规则集会跨 `execve` 继承,因此命令及其产生的每个进程都在限制下运行,调用进程仍不受限制。它采用失败闭合:如果内核无法强制执行,则不运行命令并直接退出。 - -## 安装 - -```sh -npm install @deepseek-ai/node-addon-landlock-run -``` - -已发布包由一个入口包和可选平台包组成: - -```text -@deepseek-ai/node-addon-landlock-run -@deepseek-ai/node-addon-landlock-run-linux-x64 -@deepseek-ai/node-addon-landlock-run-linux-arm64 -``` - -npm 的 `os`/`cpu` 字段使安装器只拉取匹配的平台包。系统有意不提供安装时构建回退:在没有对应平台包的宿主上,解析后的路径绝不存在,探测会报告 `unusable`,消费方以失败闭合方式处理。 - -## 用法 - -```js -import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run'; - -const launcher = launcherPath(); -if (probe(launcher) !== 'unusable') { - const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; - // spawn argv with your process runner of choice -} -``` - -公开 API 有意保持精简: - -- `launcherPath()`:当前宿主启动器的绝对路径(有意不检查是否存在;探测结果才是可用性信号)。 -- `probe(launcher?, { timeoutMs? })`:功能性强制执行探测,返回 `'full' | 'partial' | 'unusable'`。 -- `grantArgs({ readOnly?, readWrite? })`:启动器的授权 argv;未授予的一切都被拒绝。 -- `LAUNCHER_BIN` 和 `LAUNCHER_FAILURE_EXIT`(125):约定常量。成功完成 exec 的子进程也可能返回 125,因此消费方必须同时看到致命诊断和该状态,才能将结果归因为启动器失败。 - -完整的二进制约定(argv 语法、退出码、报告行)锁定在 [docs/cli-contract.md](docs/cli-contract.md) 中。 - -## 支持范围 - -支持 linux-x64 和 linux-arm64,且内核已启用 Landlock(5.13+;ABI 级别决定强制执行为 `full` 还是 `partial`,详见 [docs/support-matrix.md](docs/support-matrix.md))。其他平台有意不提供对应包:消费方会在这些平台上运行其他限制后端。 - -## 开发 - -```sh -corepack enable -pnpm install -pnpm build:ts # entry packages → lib/ -pnpm build:native # this Linux architecture's binaries (apt-get install musl-tools) -pnpm test -``` - -二进制文件被 git 忽略,并且按架构原生构建:本地只构建当前机器的版本,CI 各架构 runner 产出的构建则作为正式发布依据。发布流程详见 [docs/release.md](docs/release.md)。 diff --git a/native/landlock-run/docs/architecture.md b/native/landlock-run/docs/architecture.md deleted file mode 100644 index 72d2f85cff..0000000000 --- a/native/landlock-run/docs/architecture.md +++ /dev/null @@ -1,34 +0,0 @@ -# Architecture - -This repository owns confinement *mechanism*, not policy: consumers (agent harnesses and sandbox capabilities) decide which paths a run may read or write; this package family provides the launcher that enforces those grants and the JavaScript API that resolves and speaks to it. The packaging follows the per-platform-package model of [`node-addon-require-builtin`](https://www.npmjs.com/package/@esplus/node-addon-require-builtin) (and esbuild), adapted from Node addons to standalone static executables. - -## Two-layer package family - -The family is one entry package plus per-platform binary packages: - -- **Entry package** (`@deepseek-ai/node-addon-landlock-run`): ESM JavaScript. Owns the tool's CLI contract — path resolution (`launcherPath`), the functional probe (`probe`), grant-argv construction (`grantArgs`), and the contract constants. Ships the C source in its tarball for auditability. Lists every platform package as an `optionalDependency`. -- **Platform packages** (`@deepseek-ai/node-addon-landlock-run-linux-{x64,arm64}`): one prebuilt static binary under `bin/`, a `prebuilds.json` declaring it, and no JavaScript at all. npm's `os`/`cpu` fields select the matching one at install time; the entry package resolves it to a file path — there is nothing to import. - -Because the CLI parser and binary are versioned together in one package family, the parser cannot fall behind that binary version. Preventing that mismatch is why the package split exists. - -There is no shared loader package: platform packages have nothing to load. If a second tool ever needs shared JS, extract it then, not preemptively. - -## Resolution and availability - -`launcherPath()` resolves `@deepseek-ai/node-addon-landlock-run--` and returns `/bin/landlock-run`. When the package is not resolvable it returns a deterministic fallback path inside the entry package's own `node_modules` that simply never exists. Existence is deliberately unchecked either way: `probe()` is the single availability signal, and a missing binary probes `unusable` exactly like an unenforcing kernel. Consumers get one degradation path, not two. - -The probe is functional — the launcher builds and enforces a real maximal ruleset in a short-lived child — because version checks would miss a kernel that has the syscalls but refuses enforcement. - -## Fail-closed everywhere - -The launcher exits `125` without exec'ing the command on any launcher-level failure: usage error, unenforcing kernel, unopenable grant root, failed exec. Partial enforcement (an older Landlock ABI governing only a subset of accesses) is accepted, reported on stderr, and surfaced by the probe as `partial` — the consumer decides what its mode vocabulary promises at each level. Neither the binary nor the entry package reads environment variables: which binary confines a process is never decidable by the ambient environment. - -## Build and release model - -Builds are native-only. `scripts/build.ts` compiles the running architecture's binaries with the distro `musl-gcc` (static: no loader or libc expectations on consumers, one binary for glibc and musl distros); CI's per-architecture runners are the builders of record, and no cross toolchain exists in the repo. Review covers the C source and the CI job that built each binary, enforced by three gates: platform prepack refuses missing/wrong-ELF binaries, entry prepack refuses unbuilt `lib/`, and the release pipeline byte-pins installed binaries against the workspace builds they were packed from. - -The package matrix is checked-in metadata (`prebuilds.json` + `os`/`cpu` fields); `scripts/github-matrix.mjs` derives the CI and Release matrices from it, so adding a platform extends automation without editing workflows. - -## Adding a platform - -A new platform adds one `packages//` package (`package.json` with `os`/`cpu`, `prebuilds.json`, README, LICENSE), a runner entry in `scripts/github-matrix.mjs`, and a row in [support-matrix.md](support-matrix.md) — added only together with a native GitHub runner that builds and proves it (the no-cross-toolchain rule). Sibling launchers for other confinement mechanisms belong in their own repositories on this same template, not as second tools here. diff --git a/native/landlock-run/docs/packaging.md b/native/landlock-run/docs/packaging.md deleted file mode 100644 index 16ee74de97..0000000000 --- a/native/landlock-run/docs/packaging.md +++ /dev/null @@ -1,45 +0,0 @@ -# Packaging - -The package family uses the same layout as native packages such as esbuild: one JS entry package plus platform optional packages. Unlike Node addons there is no ABI or backend division — each platform package carries exactly the static executables its `prebuilds.json` declares. - -## Published packages - -```text -@deepseek-ai/node-addon-landlock-run -@deepseek-ai/node-addon-landlock-run-linux-x64 -@deepseek-ai/node-addon-landlock-run-linux-arm64 -``` - -Unsupported platforms are intentionally absent from `optionalDependencies` — see [support-matrix.md](support-matrix.md). - -## Package matrix - -The matrix is explicit in checked-in metadata: - -- `packages/entry/package.json` lists the platform packages as `optionalDependencies`. -- `packages//package.json` declares `os` and `cpu`. There is no `libc` field on purpose: the binaries are statically linked against musl and run on glibc and musl distros alike. -- `packages//prebuilds.json` declares the binaries that may exist in that package (`tool`, `kind`, `path`). -- [support-matrix.md](support-matrix.md) explains why unsupported platform packages are not published. - -`scripts/github-matrix.mjs` derives the CI and Release matrices from these files. `scripts/build.ts` builds only the current host's targets, into `packages//bin/`; it is not a matrix generator. When changing the matrix, update package metadata, `prebuilds.json`, the lockfile, and the support/release docs in the same change. - -## Runtime selection - -1. npm's `os`/`cpu` fields make installers fetch only the matching platform package. -2. The entry package's `launcherPath()` resolves it to `/bin/landlock-run`; unresolvable packages yield a deterministic, never-existing fallback path. -3. `probe()` is the single availability signal: missing binary and unenforcing kernel are deliberately indistinguishable (`unusable`), so consumers have one fail-closed path. - -## No install fallback - -The entry package has NO install script and never compiles on the consumer host. A compile fallback would require a musl toolchain everywhere and turn a clean fail-closed degradation into an environment-dependent maybe. The packed-manifest check in `verify-packed-install.mjs` enforces the absence of install lifecycle scripts. - -## Pack gates - -Platform tarballs are produced by `npm pack`, entry tarballs by `pnpm pack` — deliberately split: `pnpm pack` (observed on 11.7.0) normalizes file modes and strips the executable bit, which would ship a launcher no consumer can spawn, while platform packages have no dependencies and so need none of pnpm's workspace-protocol conversion; entry packages need that conversion and carry no executables. `scripts/pack-release.mjs` encodes the split — never hand-pack a platform package with pnpm. - -Both pack paths produce the exact publish bytes behind a `prepack` gate: - -- Platform packages: `scripts/verify-launcher-binary.mjs` — every declared binary present, executable, ELF `e_machine` matching the declared `cpu`, nothing undeclared in `bin/`. -- Entry packages: `scripts/verify-entry-lib.mjs` — built `lib/` present. - -`scripts/verify-packed-install.mjs` then rehearses the consumer path from the packed tarballs: payload checks, a throwaway install, a byte-pin of the installed binary against the workspace build, an executability check on the installed copy, and a real confinement world-proof through the installed launcher. A non-executable or missing binary fails loudly here instead of masquerading as a non-enforcing kernel. diff --git a/native/landlock-run/docs/release.md b/native/landlock-run/docs/release.md deleted file mode 100644 index d5eec50b6e..0000000000 --- a/native/landlock-run/docs/release.md +++ /dev/null @@ -1,60 +0,0 @@ -# Release - -Pre-1.0: treat this as a release checklist, not a stability policy. - -## Versioning - -The launcher workspace root and its three public packages share one version. Run the bump helper from the repository root: - -```sh -pnpm --dir native/landlock-run release:bump patch # or minor / major / x.y.z -``` - -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 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 --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 --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 --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 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 `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). A current-platform rehearsal can still query npm for metadata about an incompatible optional platform package; that package cannot supply the host launcher, which comes from the matching local tarball. Publishing every platform package before the entry ensures a public entry version never points ahead of its platform packages. The workflow supports npm trusted publishing through GitHub OIDC; without it, provide an `NPM_TOKEN` secret in the `npm-publish` environment. Packages publish with `--access public`. - -The three scoped package names must be bootstrapped with an `@deepseek-ai` organization token through the `NPM_TOKEN` fallback: npm [requires a package to exist before a trusted publisher can be configured](https://docs.npmjs.com/cli/v11/commands/npm-trust/). After the first release creates all three packages, configure each package to trust `landlock-run-release.yml` in this repository with the `npm-publish` environment, then remove the fallback token when organization policy permits it. - -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 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. diff --git a/native/landlock-run/docs/support-matrix.md b/native/landlock-run/docs/support-matrix.md deleted file mode 100644 index e60ad201c1..0000000000 --- a/native/landlock-run/docs/support-matrix.md +++ /dev/null @@ -1,18 +0,0 @@ -# Support matrix - -## Supported - -| Platform package | GitHub runner (builder of record) | Notes | -|---|---|---| -| `@deepseek-ai/node-addon-landlock-run-linux-x64` | `ubuntu-24.04` | static musl — glibc and musl distros alike | -| `@deepseek-ai/node-addon-landlock-run-linux-arm64` | `ubuntu-24.04-arm` | static musl — glibc and musl distros alike | - -Enforcement additionally requires a kernel with Landlock enabled (5.13+). The negotiated ABI level decides the probe verdict: every access this build knows governed → `full`; an older ABI governing a subset → `partial` (still confined for everything it supports); Landlock absent or disabled → `unusable`, and the launcher refuses to run commands at all. The probe — not the kernel version — is the authority: a kernel built without Landlock, or with the LSM disabled, probes `unusable` regardless of its version. - -## Deliberately unsupported - -- **darwin**: macOS consumers typically confine through `sandbox-exec`/Seatbelt, which ships with the OS — there is no binary to distribute. -- **win32**: a Windows confinement launcher would be a different mechanism in its own repository, not a port of this one. -- **Other Linux architectures** (riscv64, s390x, …): no native CI builder of record yet. The no-cross-toolchain rule means a platform package is added only together with a native runner that builds and proves it. - -A consumer on an unsupported platform resolves a nonexistent launcher path, probes `unusable`, and falls closed — the documented degradation, exercised by CI's darwin leg. diff --git a/native/landlock-run/packages/entry/README.md b/native/landlock-run/packages/entry/README.md deleted file mode 100644 index fff722428c..0000000000 --- a/native/landlock-run/packages/entry/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run - -English | [中文](README.zh.md) - -Landlock self-restrict-then-exec launcher for confining subprocesses on Linux: this entry package resolves the per-platform prebuilt binary, runs its functional enforcement probe, and builds its grant argv — consumers never spell launcher flags or parse launcher output themselves. - -```js -import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run'; - -const launcher = launcherPath(); -if (probe(launcher) !== 'unusable') { - const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; -} -``` - -The launcher installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the whole process tree runs confined. Everything not granted is denied, and launcher failures exit `125` without running the command — fail-closed, never fail-open. The binary contract is pinned in the repo's `docs/cli-contract.md`; the C source rides this tarball (`src/main.c`) for audit. - -Platform packages (`os`/`cpu`-selected optional dependencies, no JavaScript inside): `@deepseek-ai/node-addon-landlock-run-linux-x64`, `@deepseek-ai/node-addon-landlock-run-linux-arm64`. On hosts without one, `launcherPath()` returns a deterministic nonexistent path and `probe()` reports `'unusable'` — there is deliberately no install-time compile fallback. diff --git a/native/landlock-run/packages/entry/README.zh.md b/native/landlock-run/packages/entry/README.zh.md deleted file mode 100644 index cbf867d9dd..0000000000 --- a/native/landlock-run/packages/entry/README.zh.md +++ /dev/null @@ -1,18 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run - -[English](README.md) | 中文 - -用于在 Linux 上限制子进程的 Landlock「先限制自身、再执行」启动器:此入口包定位对应平台的预构建二进制文件,运行功能性强制执行探测,并构建其授权 argv。消费方无需自行拼写启动器标志或解析启动器输出。 - -```js -import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run'; - -const launcher = launcherPath(); -if (probe(launcher) !== 'unusable') { - const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; -} -``` - -启动器在自身上安装 Landlock 规则集,再 `exec` 被包装的命令;该规则集会跨 `execve` 继承,因此整个进程树都在限制下运行。未授予的一切都被拒绝;启动器失败时以 `125` 退出且不运行命令:采用失败闭合策略,绝不在失败时放行。二进制约定锁定在仓库的 `docs/cli-contract.md` 中;C 源码作为 `src/main.c` 随该 tarball 分发,便于审计。 - -平台包(由 `os`/`cpu` 选择的可选依赖,内部不含 JavaScript):`@deepseek-ai/node-addon-landlock-run-linux-x64`、`@deepseek-ai/node-addon-landlock-run-linux-arm64`。在缺少对应包的宿主上,`launcherPath()` 返回一个固定但不存在的路径,`probe()` 报告 `'unusable'`;系统有意不提供安装时编译回退。 diff --git a/native/landlock-run/packages/entry/package.json b/native/landlock-run/packages/entry/package.json deleted file mode 100644 index 5e34cf96f0..0000000000 --- a/native/landlock-run/packages/entry/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@deepseek-ai/node-addon-landlock-run", - "version": "0.1.1", - "type": "module", - "description": "Landlock self-restrict-then-exec launcher for sandboxing subprocesses on Linux: per-platform prebuilt static binaries plus the JS seam that resolves, probes, and speaks their CLI contract", - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", - "directory": "native/landlock-run/packages/entry" - }, - "main": "lib/index.js", - "types": "lib/index.d.ts", - "exports": { - ".": { - "types": "./lib/index.d.ts", - "default": "./lib/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "README.md", - "lib/", - "!lib/*.tsbuildinfo", - "src/main.c" - ], - "scripts": { - "build:js": "tsc -b", - "prepack": "node ../../scripts/verify-entry-lib.mjs" - }, - "engines": { - "node": ">=20" - }, - "license": "BSD-3-Clause", - "publishConfig": { - "access": "public" - }, - "optionalDependencies": { - "@deepseek-ai/node-addon-landlock-run-linux-arm64": "workspace:*", - "@deepseek-ai/node-addon-landlock-run-linux-x64": "workspace:*" - } -} diff --git a/native/landlock-run/packages/linux-arm64/README.md b/native/landlock-run/packages/linux-arm64/README.md deleted file mode 100644 index dfcc9e97dc..0000000000 --- a/native/landlock-run/packages/linux-arm64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run-linux-arm64 - -English | [中文](README.zh.md) - -Prebuilt `bin/landlock-run` Landlock launcher for linux-arm64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported. - -The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name. - -Sibling: `@deepseek-ai/node-addon-landlock-run-linux-x64`. diff --git a/native/landlock-run/packages/linux-arm64/README.zh.md b/native/landlock-run/packages/linux-arm64/README.zh.md deleted file mode 100644 index 350044e92f..0000000000 --- a/native/landlock-run/packages/linux-arm64/README.zh.md +++ /dev/null @@ -1,9 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run-linux-arm64 - -[English](README.md) | 中文 - -面向 linux-arm64 的预构建 `bin/landlock-run` Landlock 启动器:一个由 [`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run) 包所附的 C 源码原生编译而成的静态 musl 二进制文件(不使用交叉工具链)。npm 的 `os`/`cpu` 字段在安装时选择此包;入口包将其定位到文件路径。该包不包含 JavaScript,也绝不会被导入。 - -该二进制文件被 git 忽略,并通过 `files` 列表进入 npm tarball;如果文件缺失或 ELF 架构错误,`prepack` 门禁会拒绝打包,发布流水线则会按字节核验打包的二进制文件与其来源 CI 构建产物一致。静态 musl 链接使同一个二进制文件同时适用于 glibc 和 musl 发行版,因此名称中没有 libc 后缀。 - -同级包:`@deepseek-ai/node-addon-landlock-run-linux-x64`。 diff --git a/native/landlock-run/packages/linux-arm64/prebuilds.json b/native/landlock-run/packages/linux-arm64/prebuilds.json deleted file mode 100644 index 81e6b429f7..0000000000 --- a/native/landlock-run/packages/linux-arm64/prebuilds.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "platform": "linux-arm64", - "binaries": [ - { - "tool": "landlock-run", - "kind": "static-musl", - "path": "bin/landlock-run" - } - ] -} diff --git a/native/landlock-run/packages/linux-x64/README.md b/native/landlock-run/packages/linux-x64/README.md deleted file mode 100644 index d08cc0c4ab..0000000000 --- a/native/landlock-run/packages/linux-x64/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run-linux-x64 - -English | [中文](README.zh.md) - -Prebuilt `bin/landlock-run` Landlock launcher for linux-x64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported. - -The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name. - -Sibling: `@deepseek-ai/node-addon-landlock-run-linux-arm64`. diff --git a/native/landlock-run/packages/linux-x64/README.zh.md b/native/landlock-run/packages/linux-x64/README.zh.md deleted file mode 100644 index ed6839aa62..0000000000 --- a/native/landlock-run/packages/linux-x64/README.zh.md +++ /dev/null @@ -1,9 +0,0 @@ -# @deepseek-ai/node-addon-landlock-run-linux-x64 - -[English](README.md) | 中文 - -面向 linux-x64 的预构建 `bin/landlock-run` Landlock 启动器:一个由 [`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run) 包所附的 C 源码原生编译而成的静态 musl 二进制文件(不使用交叉工具链)。npm 的 `os`/`cpu` 字段在安装时选择此包;入口包将其定位到文件路径。该包不包含 JavaScript,也绝不会被导入。 - -该二进制文件被 git 忽略,并通过 `files` 列表进入 npm tarball;如果文件缺失或 ELF 架构错误,`prepack` 门禁会拒绝打包,发布流水线则会按字节核验打包的二进制文件与其来源 CI 构建产物一致。静态 musl 链接使同一个二进制文件同时适用于 glibc 和 musl 发行版,因此名称中没有 libc 后缀。 - -同级包:`@deepseek-ai/node-addon-landlock-run-linux-arm64`。 diff --git a/native/landlock-run/packages/linux-x64/prebuilds.json b/native/landlock-run/packages/linux-x64/prebuilds.json deleted file mode 100644 index 27b0de360c..0000000000 --- a/native/landlock-run/packages/linux-x64/prebuilds.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "platform": "linux-x64", - "binaries": [ - { - "tool": "landlock-run", - "kind": "static-musl", - "path": "bin/landlock-run" - } - ] -} diff --git a/native/landlock-run/scripts/build.ts b/native/landlock-run/scripts/build.ts deleted file mode 100644 index b8e2d34f12..0000000000 --- a/native/landlock-run/scripts/build.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Build every native tool this host can build, into its per-platform - * package. - * - * Targets are derived from the checked-in matrix: each - * `packages//prebuilds.json` whose `platform` matches this host names - * the binaries to produce; the TOOLS table below maps each `tool` to its C - * source. Builds are NATIVE-ONLY — each Linux architecture compiles its own - * binary with the distro's `musl-gcc` (static musl: runs on glibc and musl - * distros alike, no loader or libc expectations on the consumer host), and - * CI's per-arch runners are the builders of record. No cross toolchain - * exists here on purpose: native runners replace it, and the audit surface - * is the reviewed C source plus the CI job that built the binary. - * - * Binaries land in `packages//bin/` — git-ignored (root - * `.gitignore`), packed into the platform package's npm tarball behind its - * `prepack` gate (`scripts/verify-launcher-binary.mjs`). - * - * Run: `pnpm run build:native` (Linux with musl-gcc on PATH: - * `apt-get install musl-tools`). Non-Linux hosts fail fast — no platform - * package exists for them to build. - */ -import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs' -import { basename, dirname, join, resolve } from 'node:path' - -/** Each native tool's C source, keyed by the `tool` field in prebuilds.json. */ -const TOOLS: Record = { - 'landlock-run': { source: 'packages/entry/src/main.c' }, -} - -const repoRoot = resolve(import.meta.dirname, '..') - -if (process.platform !== 'linux') { - console.error(`build: native tools are built natively per Linux architecture (no cross toolchain) — nothing to build on ${process.platform}. CI's per-arch runners build and rehearse every platform package.`) - process.exit(1) -} -const hostPlatform = `linux-${process.arch}` - -/** This host's platform packages, from the checked-in matrix. */ -const targets: { packageDir: string; tool: string; binaryPath: string; kind: string }[] = [] -const packagesRoot = join(repoRoot, 'packages') -for (const name of readdirSync(packagesRoot).sort()) { - const prebuildsFile = join(packagesRoot, name, 'prebuilds.json') - if (!existsSync(prebuildsFile)) continue - const prebuilds = JSON.parse(readFileSync(prebuildsFile, 'utf8')) as { - platform: string - binaries: { tool: string; kind: string; path: string }[] - } - if (prebuilds.platform !== hostPlatform) continue - for (const binary of prebuilds.binaries) { - targets.push({ packageDir: join(packagesRoot, name), tool: binary.tool, binaryPath: binary.path, kind: binary.kind }) - } -} -if (targets.length === 0) { - console.error(`build: no platform package declares binaries for ${hostPlatform} — supported platforms are the packages/*/prebuilds.json "platform" values.`) - process.exit(1) -} - -for (const target of targets) { - const tool = TOOLS[target.tool] - if (tool === undefined) { - console.error(`build: prebuilds.json names unknown tool "${target.tool}" — add it to the TOOLS table in scripts/build.ts.`) - process.exit(1) - } - if (target.kind !== 'static-musl') { - console.error(`build: unknown binary kind "${target.kind}" — the only toolchain here is static musl.`) - process.exit(1) - } - const binary = join(target.packageDir, target.binaryPath) - mkdirSync(dirname(binary), { recursive: true }) - - // -static against musl: self-contained, no loader/libc expectations on the - // consumer host. -Werror is safe to keep hard: CI pins the builder images, - // and a new warning on a toolchain bump deserves a look, not a pass. - const result = spawnSync('musl-gcc', [ - '-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s', - '-o', binary, join(repoRoot, tool.source), - ], { stdio: ['ignore', 'inherit', 'inherit'] }) - if (result.error !== undefined || result.status !== 0) { - console.error('build: musl-gcc failed' + - (result.error ? ` (${result.error.message} — is musl-tools installed?)` : '')) - process.exit(1) - } - console.log(`build: built ${basename(target.packageDir)}/${target.binaryPath}`) -} diff --git a/native/landlock-run/scripts/repo.mjs b/native/landlock-run/scripts/repo.mjs deleted file mode 100644 index db5ffaf28d..0000000000 --- a/native/landlock-run/scripts/repo.mjs +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env node -/** - * Shared helpers for the repo scripts: package discovery, the checked-in - * prebuild matrix, and binary verification. The package matrix is explicit - * metadata — `packages//prebuilds.json` marks a platform package and - * declares its binaries; everything else under `packages/` is an entry - * package. Scripts derive from these files and never guess. - */ - -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -export const root = fileURLToPath(new URL('..', import.meta.url)); -const packagesRoot = path.join(root, 'packages'); - -/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */ -const E_MACHINE = { x64: 62, arm64: 183 }; - -export function readJson(file) { - return JSON.parse(fs.readFileSync(file, 'utf8')); -} - -/** Platform packages: every `packages/` carrying a `prebuilds.json`. */ -export function platformDirs() { - return fs.readdirSync(packagesRoot) - .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) - .sort() - .map((name) => path.join('packages', name)); -} - -/** Entry packages: every other `packages/` with a `package.json`. */ -export function entryDirs() { - return fs.readdirSync(packagesRoot) - .filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) - .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json'))) - .sort() - .map((name) => path.join('packages', name)); -} - -/** All published packages in publish order: platform packages before the entries that optionally depend on them. */ -export function packageDirs() { - return [...platformDirs(), ...entryDirs()]; -} - -/** - * Verify one platform package's binaries against its `prebuilds.json`: - * every declared binary exists, nothing undeclared sits in `bin/`, and each - * file's ELF `e_machine` matches the package's declared `cpu`. Throws with - * a remediation message on the first mismatch. - */ -export function verifyPlatformBinaries(packageDir) { - const manifest = readJson(path.join(packageDir, 'package.json')); - const prebuilds = readJson(path.join(packageDir, 'prebuilds.json')); - const cpu = manifest.cpu?.[0]; - if (cpu === undefined || !(cpu in E_MACHINE)) { - throw new Error(`${manifest.name}: unsupported or missing "cpu" in package.json (expected one of: ${Object.keys(E_MACHINE).join(', ')})`); - } - - for (const binary of prebuilds.binaries) { - const file = path.join(packageDir, binary.path); - if (!fs.existsSync(file)) { - throw new Error(`${manifest.name}: missing ${binary.path} — run \`pnpm build:native\` on a ${prebuilds.platform} host (or assemble release artifacts) before packing.`); - } - try { - fs.accessSync(file, fs.constants.X_OK); - } catch { - // Only reachable when the mode was mangled somewhere between build and - // here (e.g. an archive step that normalized permissions) — the build - // itself always produces 755. - throw new Error(`${manifest.name}: ${binary.path} is not executable — a pack/extract step stripped the mode bit.`); - } - const machine = fs.readFileSync(file).readUInt16LE(18); - if (machine !== E_MACHINE[cpu]) { - throw new Error(`${manifest.name}: ${binary.path} has ELF e_machine ${machine}, expected ${E_MACHINE[cpu]} for ${cpu} — the binary was built for a different architecture.`); - } - } - - const declared = prebuilds.binaries.map((binary) => path.basename(binary.path)).sort(); - const binDir = path.join(packageDir, 'bin'); - const actual = fs.existsSync(binDir) ? fs.readdirSync(binDir).sort() : []; - const extra = actual.filter((name) => !declared.includes(name)); - if (extra.length) { - throw new Error(`${manifest.name}: bin/ contains files not declared in prebuilds.json: ${extra.join(', ')}`); - } - - return { name: manifest.name, count: prebuilds.binaries.length }; -} diff --git a/native/landlock-run/.gitignore b/native/system/.gitignore similarity index 100% rename from native/landlock-run/.gitignore rename to native/system/.gitignore diff --git a/native/system/AGENTS.md b/native/system/AGENTS.md new file mode 100644 index 0000000000..8234f941fd --- /dev/null +++ b/native/system/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md + +This workspace owns `@deepseek-ai/node-addon-system`: the Linux `landlock-run` confinement executable and the POSIX `system.node` binding. It shares the root pnpm workspace and lockfile; native packages have one independent version and release workflow. + +## Runtime rules + +- Landlock's argv, exit codes, diagnostics, and fail-closed confinement are defined in [docs/cli-contract.md](docs/cli-contract.md). Do not change them when extending another system capability. +- The launcher uses only libc, statically linked against musl. Its kernel UAPI definitions remain in the reviewed C source. +- Node bindings use stable Node-API v8, never NAN, V8 C++ APIs, or experimental Node interfaces. Linux glibc and musl addons are distinct binaries; macOS has its own Mach-O bundle. +- The flock binding attempts only `LOCK_EX | LOCK_NB` in asynchronous work and captures errno on that worker. The caller owns the fd through completion and releases its lock by closing it. +- `./landlock-run` and `./flock` are independent capability exports; the package has no root export. Neither import loads the addon. `./flock` loads it only when called; Windows retains the Harness's existing semaphore implementation. +- Runtime binary selection has no environment-variable overrides. `NALR_REQUIRE_LANDLOCK` is a test-only enforcement requirement. +- There is no install-time compile fallback. Missing Landlock binaries probe unusable; missing flock bindings reject acquisition, never silently grant a lock. + +## Layout and commands + +`packages/entry/` owns JavaScript, types, and auditable C sources. Platform packages hold only binaries and metadata. `scripts/` owns native builds, packing, validation, and release; `test/` owns real process and lock behavior. + +Run `pnpm build:ts`, `pnpm build:native`, `pnpm build:test-oracle`, `pnpm typecheck`, and `pnpm test` in this directory. Linux full builds require musl-gcc; macOS uses cc. Repository tests build only their host addon through the root `build:native-system` script. The independent syscall fixture is test-only and never enters a published platform package. + +## Packaging and verification + +- `os`/`cpu` and `prebuilds.json` are the checked-in package matrix. CI derives runners from that matrix, builds natively on each architecture, and tests identical addon bytes under several Node releases. +- Linux packages contain `bin/landlock-run`, `bin/glibc/system.node`, and `bin/musl/system.node`. macOS packages contain `bin/system.node`. No Windows platform package is needed by these capabilities. +- Platform prepack rejects missing, undeclared, wrong-format, wrong-architecture, and non-Node-API addon payloads. Launcher executability is checked separately. +- Platform tarballs use npm pack to preserve executable permissions. The entry uses pnpm pack for workspace version conversion. +- Packed-install verification checks manifests, installs local tarballs without a registry, byte-pins payloads, and exercises both the installed flock binding and Landlock's functional probe. +- Build outputs stay ignored. Source/consumer changes and their behavior tests land together; preserve bilingual READMEs and independent native publication. diff --git a/native/landlock-run/LICENSE b/native/system/LICENSE similarity index 100% rename from native/landlock-run/LICENSE rename to native/system/LICENSE diff --git a/native/landlock-run/README.i18n.yaml b/native/system/README.i18n.yaml similarity index 56% rename from native/landlock-run/README.i18n.yaml rename to native/system/README.i18n.yaml index 6eb3457bdb..1e1eb1cd92 100644 --- a/native/landlock-run/README.i18n.yaml +++ b/native/system/README.i18n.yaml @@ -1,6 +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 native/landlock-run/README.md -README.md: bf9b163d42a9b7402dbf4c045d47e7b8d36e15ab -README.zh.md: 7bd4765a73f0d5df2a4710ce06ff319f6c902db6 +# pnpm run verify-translation-pairing --write native/system/README.md +README.md: 5a01754cb96fabbdfd2dbf4f068f19addd7a2347 +README.zh.md: dabd00cecd05ded13a50d2efd000985afed713d4 diff --git a/native/system/README.md b/native/system/README.md new file mode 100644 index 0000000000..5a01754cb9 --- /dev/null +++ b/native/system/README.md @@ -0,0 +1,41 @@ +--- +description: "Prebuilt system primitives for Linux confinement and POSIX Session write locks." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system + +English | [中文](README.zh.md) + +## Summary + +Use the Linux `landlock-run` executable to confine subprocesses, or the `./flock` entry to acquire a POSIX write lock. Platform packages contain the precompiled binaries; consumer installation never builds native code. Landlock policy and Session lifecycle remain with callers. + +## Table of Contents + +- [Use](#use) +- [Support](#support) +- [Development](#development) + +## Use + +`@deepseek-ai/node-addon-system/landlock-run` exports `launcherPath`, `probe`, and `grantArgs` for Landlock. Its executable name, flags, and failure semantics are defined by the [CLI contract](docs/cli-contract.md). + +The [flock behavior contract](docs/flock-contract.md) maps descriptor, process, and advisory-lock semantics to independent native tests. + +`@deepseek-ai/node-addon-system/flock` exports `tryLockExclusive(fd): Promise`. Keep the descriptor open until completion. Acquisition uses nonblocking exclusive flock; contention rejects with `EAGAIN` or `EWOULDBLOCK`, and closing the final descriptor for the open file description releases the lock. See the [entry README](packages/entry/README.md). + +Importing either entry does not load an addon. A missing Landlock executable probes unusable; a missing flock binding rejects acquisition. Neither path compiles or silently grants unsupported behavior. + +## Support + +Linux x64/arm64 packages contain the static Landlock executable and separate glibc/musl `system.node` files. macOS x64/arm64 packages contain `system.node` only. Landlock additionally needs an enforcing Linux kernel; Windows uses the Harness's existing locking implementation. The [support matrix](docs/support-matrix.md) names builders and verification owners. + +## Development + +From this directory, `pnpm build:ts` builds the entry, `pnpm build:native` builds the host's declared native payload, and `pnpm build:test-oracle` builds an independent flock syscall fixture. Then `pnpm test` exercises entry, lock, packaging, and available kernel behavior. Linux requires musl-gcc for a complete build; macOS uses cc. The root `pnpm run build:native-system` builds only the current host addon for source tests. + +The [architecture](docs/architecture.md), [packaging](docs/packaging.md), and [release procedure](docs/release.md) own implementation and publication details. + +### Dev Note + +None. diff --git a/native/system/README.zh.md b/native/system/README.zh.md new file mode 100644 index 0000000000..dabd00cecd --- /dev/null +++ b/native/system/README.zh.md @@ -0,0 +1,41 @@ +--- +description: "为 Linux 进程隔离与 POSIX Session 写锁提供预编译系统原语。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system + +[English](README.md) | 中文 + +## Summary + +使用 Linux `landlock-run` 可执行文件限制子进程,或通过 `./flock` 入口获取 POSIX 写锁。平台包包含预编译二进制;用户安装时不会构建原生代码。Landlock 策略与 Session 生命周期仍由调用方负责。 + +## Table of Contents + +- [使用](#use) +- [支持范围](#support) +- [开发](#development) + +## Use + +`@deepseek-ai/node-addon-system/landlock-run` 为 Landlock 导出 `launcherPath`、`probe` 和 `grantArgs`。其可执行文件名、参数和失败语义由 [CLI 约定](docs/cli-contract.md) 定义。 + +[flock 行为约定](docs/flock-contract.md) 将描述符、进程和咨询式锁语义对应到独立原生测试。 + +`@deepseek-ai/node-addon-system/flock` 导出 `tryLockExclusive(fd): Promise`。在调用完成前保持描述符打开。获取操作使用非阻塞独占 flock;竞争以 `EAGAIN` 或 `EWOULDBLOCK` 拒绝,关闭该打开文件描述的最后一个描述符即释放锁。参见[入口 README](packages/entry/README.zh.md)。 + +导入任一入口都不会加载 addon。Landlock 可执行文件缺失时探测为不可用;flock 绑定缺失时拒绝获取。两条路径都不会编译或静默授予不受支持的行为。 + +## Support + +Linux x64/arm64 包包含静态 Landlock 可执行文件,以及分别用于 glibc/musl 的 `system.node` 文件。macOS x64/arm64 包仅包含 `system.node`。Landlock 还需要支持强制执行的 Linux 内核;Windows 使用 Harness 既有锁实现。[支持矩阵](docs/support-matrix.md) 指定构建者与验证负责人。 + +## Development + +在本目录运行 `pnpm build:ts` 构建入口、`pnpm build:native` 构建当前宿主声明的原生产物、`pnpm build:test-oracle` 构建独立的 flock 系统调用 fixture。随后用 `pnpm test` 验证入口、锁、打包及可用的内核行为。Linux 完整构建需要 musl-gcc;macOS 使用 cc。根目录 `pnpm run build:native-system` 只构建源码测试所需的当前宿主 addon。 + +[架构](docs/architecture.md)、[打包](docs/packaging.md)和[发布流程](docs/release.md)分别负责实现与发布细节。 + +### Dev Note + +无。 diff --git a/native/system/docs/architecture.md b/native/system/docs/architecture.md new file mode 100644 index 0000000000..7c258752a3 --- /dev/null +++ b/native/system/docs/architecture.md @@ -0,0 +1,25 @@ +# Architecture + +The system package family supplies native mechanisms to Node callers: a Linux confinement executable and a POSIX file-lock binding. Consumers own sandbox policy and Session lifecycle. + +## Package family + +The ESM package `@deepseek-ai/node-addon-system` and its optional platform packages share one version. Platform metadata chooses the operating system and CPU; each package's `prebuilds.json` declares the files it must contain. + +The `./landlock-run` entry owns Landlock path resolution, grant argv, and the functional probe. It does not load native addons. The `./flock` entry lazily loads `system.node` only when `tryLockExclusive(fd)` is called. Importing either JavaScript entry therefore works without a matching native payload. The package exposes these capability subpaths and its manifest, without a root export. + +## Separate mechanisms + +`landlock-run` remains a static musl executable with the [existing CLI contract](cli-contract.md). It installs confinement on itself before exec, and refuses to exec if enforcement is unavailable. A missing launcher or unsupported kernel produces an unusable probe. + +`system.node` uses stable Node-API v8. Its flock operation follows [fs-ext's asynchronous callback model](https://github.com/baudehlo/node-fs-ext/blob/v2.1.1/fs-ext.cc): it runs `flock(fd, LOCK_EX | LOCK_NB)` in asynchronous work and records errno on that worker. The native callback receives zero or positive errno; JavaScript owns the promise and syscall error construction. Setup errors throw into that promise. Callback exceptions are reported through Node's uncaught-exception handler; unexpected Node-API failures terminate the process. A terminating environment may suppress JavaScript completion, but its cleanup waits for queued or running native work before freeing storage. + +The descriptor belongs to the caller and must stay open through completion. The binding neither opens nor closes it; closing the final descriptor for its open file description releases the lock. + +The JSONL backend retains its inode check, materialization timing, and close lifecycle. Windows uses its existing koffi semaphore and never calls this POSIX binding. The browser worker supplies a single-process replacement for the flock entry, while running the Landlock JavaScript API unchanged. + +## Builds and release + +Repository builds and `build:bench` explicitly build the host addon before running consumers. Each platform builds natively on its CI runner. Landlock is static-musl; Linux addons are separately built for glibc and musl, and macOS uses a Mach-O bundle. Stable Node-API removes the Node-major build dimension, not OS, CPU, or libc differences. CI exercises identical addon bytes under Node 20, 22, 24, and 26; Linux also runs the musl addon in Alpine containers. + +Platform prepack validates file formats, architecture, payload completeness, and Node-API exports. The packed-install rehearsal installs local tarballs, checks their bytes, and exercises the installed mechanisms. Missing capabilities fail explicitly; no consumer install runs a compiler. [Packaging](packaging.md) and [release](release.md) own the operational details. diff --git a/native/landlock-run/docs/cli-contract.md b/native/system/docs/cli-contract.md similarity index 100% rename from native/landlock-run/docs/cli-contract.md rename to native/system/docs/cli-contract.md diff --git a/native/system/docs/flock-contract.md b/native/system/docs/flock-contract.md new file mode 100644 index 0000000000..dafbdbb3a3 --- /dev/null +++ b/native/system/docs/flock-contract.md @@ -0,0 +1,36 @@ +# POSIX flock behavior + +`tryLockExclusive(fd)` returns a promise for one `flock(fd, LOCK_EX | LOCK_NB)` attempt. The syscall runs off the JavaScript thread. The caller keeps the descriptor open through completion; the binding does not open, duplicate, or close it. It exposes neither a blocking-wait API nor a shared-lock API. + +## Behavior tests + +The [native tests](../test/flock.test.js) exercise real descriptors and independent processes. The [C oracle](../test/fixtures/flock-oracle.c) calls the operating system directly, independently of `system.node`. + +| Condition | Required observation | +|---|---| +| No conflicting lock | Acquisition resolves to void | +| Same open file description acquires again | Acquisition succeeds without a second ownership record | +| Separate opens of the same file | Exactly one exclusive holder; the contender rejects with EAGAIN/EWOULDBLOCK | +| Different files | Both can be locked | +| Independent C flock holder | The addon cannot acquire, and the C oracle cannot acquire while the addon holds the lock | +| A shared flock holder | The addon's exclusive attempt conflicts | +| Holder remains live | A nonblocking attempt reports contention before the holder unlocks | +| Ordinary read/write by another process | Access is allowed: flock is advisory, not an I/O permission mechanism | +| One unrelated descriptor closes | The actual holder keeps its lock | +| A descriptor inherited by a child remains open | Closing the parent's descriptor does not release the shared open file description's lock | +| Last owning descriptor closes | An already-open contender can acquire | +| Holder process exits or is killed | Acquisition succeeds after process exit, without a stale-lock timeout | +| Invalid descriptor | The promise rejects with EBADF and positive errno | +| Native argument validation fails | The JavaScript entry returns a rejected promise without throwing synchronously | +| Native completion callback | It receives zero or the request's positive errno asynchronously | +| Native completion callback throws | The exception reaches Node's uncaught-exception handler | +| Concurrent success/failure calls | Each completion receives its own syscall errno | +| Worker environment terminates before or during its callback | Native work and cleanup reach completion without taking ownership of the caller's descriptor | + +Tests synchronize through IPC or flushed line protocols and await process exit before asserting crash recovery. They do not use fixed sleeps or a millisecond performance threshold to prove nonblocking behavior. The syscall oracle is built only for tests and never included in a published platform package. + +## Limits + +Locks belong to open file descriptions and follow the host filesystem's flock semantics. Removing or replacing a pathname does not transfer a lock to the replacement inode; the JSONL backend separately checks inode identity. Network filesystems can have different or unsupported lock semantics. Windows does not use this API and retains its existing semaphore implementation. + +Node-API compatibility tests reuse the same platform addon under different Node versions. They complement these syscall tests; loading a binary alone does not prove correct locking behavior. diff --git a/native/landlock-run/docs/naming.md b/native/system/docs/naming.md similarity index 60% rename from native/landlock-run/docs/naming.md rename to native/system/docs/naming.md index a1f2665654..bc82176b51 100644 --- a/native/landlock-run/docs/naming.md +++ b/native/system/docs/naming.md @@ -2,18 +2,18 @@ ## npm packages -The public package family belongs to the `@deepseek-ai` scope and uses the `node-addon-landlock-run` package prefix; platform packages append platform information only: +The public package family belongs to the `@deepseek-ai` scope and uses the `node-addon-system` package prefix; platform packages append platform information only: ```text -@deepseek-ai/node-addon-landlock-run -@deepseek-ai/node-addon-landlock-run- +@deepseek-ai/node-addon-system +@deepseek-ai/node-addon-system- ``` -Platform suffixes carry no libc component (binaries are static musl) and no variant component — variants stay inside `prebuilds.json` and binary filenames. +Platform suffixes carry OS and CPU. Linux libc variants live inside the same platform package and are declared in `prebuilds.json`. ## Binaries -The launcher executable is `landlock-run`, shipped at `bin/landlock-run` inside each platform package. +The Linux launcher remains `bin/landlock-run`. The Node-API addon is `system.node`: `bin/glibc/system.node` and `bin/musl/system.node` on Linux, `bin/system.node` on macOS. ## Environment variables diff --git a/native/system/docs/packaging.md b/native/system/docs/packaging.md new file mode 100644 index 0000000000..5ca2320612 --- /dev/null +++ b/native/system/docs/packaging.md @@ -0,0 +1,24 @@ +# Packaging + +The family publishes one ESM entry package plus OS/CPU-selected optional platform packages. All share one version; platform packages publish before the entry. + +## Payloads + +The entry package exports the Landlock API at `./landlock-run` and the asynchronous lock API at `./flock`, with C sources included for auditability. There is no root export. Platform packages contain no JavaScript. + +- Linux: `bin/landlock-run`, `bin/glibc/system.node`, and `bin/musl/system.node`. +- macOS: `bin/system.node`. + +`package.json` supplies OS/CPU metadata; `prebuilds.json` supplies tool, binary kind, path, and addon Node-API/libc metadata. CI matrices and release assembly derive from those files. Nested paths remain intact in uploaded artifacts and tarballs. + +## Installation and use + +Neither entry nor platform packages have installation lifecycle scripts. The entry resolves its matching optional package when a native operation needs it. Optional means that the package manager selects a platform, not that a requested lock can succeed without its binding. + +The `./landlock-run` API stays importable without native payloads and reports unavailable enforcement through its probe. The flock entry is also lazy at import; acquisition reports a missing or unloadable addon instead of compiling or granting an unprotected lock. + +## Pack verification + +Platform tarballs use npm pack to preserve the launcher's executable bit. The entry uses pnpm pack to convert workspace dependency versions. Prepack rejects missing or undeclared payloads, invalid ELF/Mach-O architecture or type, addons without Node-API exports, and launchers without executable permission. + +The installed-artifact rehearsal verifies concrete dependency versions and absence of installation hooks, performs an offline npm install from local tarballs, and compares installed bytes with build outputs. It then proves flock contention/close release and probes the installed Landlock launcher; real confinement remains required on enforcing CI kernels. diff --git a/native/system/docs/release.md b/native/system/docs/release.md new file mode 100644 index 0000000000..650156576e --- /dev/null +++ b/native/system/docs/release.md @@ -0,0 +1,63 @@ +# Release + +Pre-1.0: treat this as a release checklist, not a stability policy. + +## Versioning + +The native workspace root and every platform/entry package share one version. Run the bump helper from the repository root: + +```sh +pnpm --dir native/system release:bump patch # or minor / major / x.y.z +``` + +It updates `native/system/package.json` and every `native/system/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/system 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 launcher manifests and root lockfile, merge it, then create the matching `node-addon-system-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 --dir native/system release:commit patch # bump + stage + commit in one command +git tag node-addon-system-v0.0.2 +``` + +## Preflight + +```sh +pnpm install --frozen-lockfile +pnpm --dir native/system build:ts +pnpm --dir native/system typecheck +pnpm --dir native/system test:entry +``` + +On a supported Linux or macOS host, also rehearse the pack path locally: + +```sh +pnpm --dir native/system build:native +pnpm --dir native/system build:test-oracle +pnpm --dir native/system test:launcher +pnpm --dir native/system test:flock +pnpm --dir native/system test:packaging +node native/system/scripts/pack-release.mjs native/system/.release/npm --current-platform-only +node native/system/scripts/verify-packed-install.mjs native/system/.release/npm --current-platform-only +``` + +## Publish + +Use the main repository's `Node Addon System 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 `node-addon-system-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). The current-platform rehearsal uses offline npm installation; the current entry and platform package come from local tarballs. Publishing every platform package before the entry ensures a public entry version never points ahead of its platform packages. The workflow supports npm trusted publishing through GitHub OIDC; without it, provide an `NPM_TOKEN` secret in the `npm-publish` environment. Packages publish with `--access public`. + +New scoped package names must be bootstrapped with an `@deepseek-ai` organization token through the `NPM_TOKEN` fallback: npm [requires a package to exist before a trusted publisher can be configured](https://docs.npmjs.com/cli/v11/commands/npm-trust/). After the first release creates the packages, configure each package to trust `node-addon-system-release.yml` in this repository with the `npm-publish` environment, then remove the fallback token when organization policy permits it. + +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 native/system/scripts/pack-release.mjs native/system/dist/npm --current-platform-only +node native/system/scripts/verify-packed-install.mjs native/system/dist/npm --current-platform-only +while IFS= read -r tarball; do npm publish "native/system/dist/npm/${tarball}" --access public; done < native/system/dist/npm/publish-order.txt +``` + +Do not commit `.npmrc` files with tokens or registry overrides. diff --git a/native/system/docs/support-matrix.md b/native/system/docs/support-matrix.md new file mode 100644 index 0000000000..c499a73b8b --- /dev/null +++ b/native/system/docs/support-matrix.md @@ -0,0 +1,14 @@ +# Support matrix + +| Platform package suffix | Builder | Payload | +|---|---|---| +| linux-x64 | ubuntu-24.04 | static Landlock executable; glibc and musl system.node | +| linux-arm64 | ubuntu-24.04-arm | static Landlock executable; glibc and musl system.node | +| darwin-x64 | macos-15-intel | system.node | +| darwin-arm64 | macos-latest | system.node | + +The stable Node-API v8 addon is built once per platform/libc and exercised by CI under Node 20, 22, 24, and 26. macOS builds target 11.0 or later. Linux binding selection uses the running Node process's libc; the static launcher serves both libc variants. + +Landlock additionally requires an enforcing Linux kernel. The functional probe determines full, partial, or unusable enforcement; kernel version alone is not an availability guarantee. + +Windows has neither a Landlock launcher nor this POSIX addon. The Harness retains its existing Windows semaphore implementation. Other CPU/OS combinations have no published platform package: Landlock probes unusable, and flock acquisition rejects. New platform support requires a native builder and installed-artifact verification. diff --git a/native/landlock-run/package.json b/native/system/package.json similarity index 74% rename from native/landlock-run/package.json rename to native/system/package.json index be0ca9732d..5b9373de72 100644 --- a/native/landlock-run/package.json +++ b/native/system/package.json @@ -1,5 +1,5 @@ { - "name": "@deepseek-ai/node-addon-landlock-run-workspace", + "name": "@deepseek-ai/node-addon-system-workspace", "version": "0.1.1", "private": true, "type": "module", @@ -9,10 +9,13 @@ "build": "pnpm build:ts", "build:ts": "tsc -b", "build:native": "tsx ./scripts/build.ts", + "build:test-oracle": "node ./scripts/build-test-oracle.mjs", "typecheck": "tsc --noEmit && tsc -b --dry", - "test": "node ./test/entry.test.js && node ./test/launcher.test.js", + "test": "node ./test/entry.test.js && node ./test/launcher.test.js && node --test ./test/flock.test.js ./test/package-matrix.test.js", "test:entry": "node ./test/entry.test.js", "test:launcher": "node ./test/launcher.test.js", + "test:flock": "node --test ./test/flock.test.js", + "test:packaging": "node --test ./test/package-matrix.test.js", "gha:matrix": "node ./scripts/github-matrix.mjs", "release:bump": "node ./scripts/bump-release.mjs", "release:commit": "node ./scripts/commit-release.mjs", @@ -23,7 +26,7 @@ "release:verify-packed-install": "node ./scripts/verify-packed-install.mjs" }, "devDependencies": { - "@deepseek-ai/node-addon-landlock-run": "workspace:*", + "@deepseek-ai/node-addon-system": "workspace:*", "@types/node": "^26.0.1", "tsx": "^4.20.6", "typescript": "^6.0.3" diff --git a/native/landlock-run/packages/linux-arm64/LICENSE b/native/system/packages/darwin-arm64/LICENSE similarity index 100% rename from native/landlock-run/packages/linux-arm64/LICENSE rename to native/system/packages/darwin-arm64/LICENSE diff --git a/native/landlock-run/packages/linux-x64/README.i18n.yaml b/native/system/packages/darwin-arm64/README.i18n.yaml similarity index 54% rename from native/landlock-run/packages/linux-x64/README.i18n.yaml rename to native/system/packages/darwin-arm64/README.i18n.yaml index cb0022b138..cce14d5da1 100644 --- a/native/landlock-run/packages/linux-x64/README.i18n.yaml +++ b/native/system/packages/darwin-arm64/README.i18n.yaml @@ -1,6 +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 native/landlock-run/packages/linux-x64/README.md -README.md: d08cc0c4abbc74f64c5d1075dea796427211bd8f -README.zh.md: ed6839aa6230b16b82c67a716fc0a4128e5a977c +# pnpm run verify-translation-pairing --write native/system/packages/darwin-arm64/README.md +README.md: 71cfaacc9bc442341b3a9d57389c2cb1904fbb70 +README.zh.md: 5e54c8155009b98e3336ea612535ce08e56817ae diff --git a/native/system/packages/darwin-arm64/README.md b/native/system/packages/darwin-arm64/README.md new file mode 100644 index 0000000000..71cfaacc9b --- /dev/null +++ b/native/system/packages/darwin-arm64/README.md @@ -0,0 +1,9 @@ +--- +description: "Prebuilt system.node for macOS arm64 POSIX locks." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-darwin-arm64 + +English | [中文](README.zh.md) + +This platform package supplies `bin/system.node`, a stable Node-API v8 addon used by `@deepseek-ai/node-addon-system/flock`. It contains no Landlock executable, JavaScript loader, or installation build script. The native workflow builds it on macOS arm64 and owns its installed-artifact validation. diff --git a/native/system/packages/darwin-arm64/README.zh.md b/native/system/packages/darwin-arm64/README.zh.md new file mode 100644 index 0000000000..5e54c81550 --- /dev/null +++ b/native/system/packages/darwin-arm64/README.zh.md @@ -0,0 +1,9 @@ +--- +description: "为 macOS arm64 POSIX 锁提供预编译 system.node。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-darwin-arm64 + +[English](README.md) | 中文 + +此平台包提供 `bin/system.node`,供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS arm64 上构建它,并负责验证安装后的产物。 diff --git a/native/system/packages/darwin-arm64/package.json b/native/system/packages/darwin-arm64/package.json new file mode 100644 index 0000000000..c89c246c45 --- /dev/null +++ b/native/system/packages/darwin-arm64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@deepseek-ai/node-addon-system-darwin-arm64", + "version": "0.1.1", + "description": "Prebuilt POSIX flock Node-API binding for macOS arm64", + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", + "directory": "native/system/packages/darwin-arm64" + }, + "os": ["darwin"], + "cpu": ["arm64"], + "files": ["README.md", "bin/", "prebuilds.json"], + "scripts": { + "prepack": "node ../../scripts/verify-launcher-binary.mjs" + }, + "engines": { "node": ">=20" }, + "license": "BSD-3-Clause", + "publishConfig": { "access": "public" } +} diff --git a/native/system/packages/darwin-arm64/prebuilds.json b/native/system/packages/darwin-arm64/prebuilds.json new file mode 100644 index 0000000000..8b3ba78d63 --- /dev/null +++ b/native/system/packages/darwin-arm64/prebuilds.json @@ -0,0 +1,6 @@ +{ + "platform": "darwin-arm64", + "binaries": [ + { "tool": "flock", "kind": "node-api", "napi": 8, "path": "bin/system.node" } + ] +} diff --git a/native/landlock-run/packages/linux-x64/LICENSE b/native/system/packages/darwin-x64/LICENSE similarity index 100% rename from native/landlock-run/packages/linux-x64/LICENSE rename to native/system/packages/darwin-x64/LICENSE diff --git a/native/landlock-run/packages/entry/README.i18n.yaml b/native/system/packages/darwin-x64/README.i18n.yaml similarity index 54% rename from native/landlock-run/packages/entry/README.i18n.yaml rename to native/system/packages/darwin-x64/README.i18n.yaml index 1a701c02bc..ff5479220b 100644 --- a/native/landlock-run/packages/entry/README.i18n.yaml +++ b/native/system/packages/darwin-x64/README.i18n.yaml @@ -1,6 +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 native/landlock-run/packages/entry/README.md -README.md: fff722428c5d213d9fcce0ee87a1d48cdc189884 -README.zh.md: cbf867d9dd3551a12b74b19a6ceaf2be3e1c53a6 +# pnpm run verify-translation-pairing --write native/system/packages/darwin-x64/README.md +README.md: 1740788eef943373591a895baeb7207635972a46 +README.zh.md: 644165743486a29e48bee7af33429ebf4f5d8fe2 diff --git a/native/system/packages/darwin-x64/README.md b/native/system/packages/darwin-x64/README.md new file mode 100644 index 0000000000..1740788eef --- /dev/null +++ b/native/system/packages/darwin-x64/README.md @@ -0,0 +1,9 @@ +--- +description: "Prebuilt system.node for macOS x64 POSIX locks." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-darwin-x64 + +English | [中文](README.zh.md) + +This platform package supplies `bin/system.node`, a stable Node-API v8 addon used by `@deepseek-ai/node-addon-system/flock`. It contains no Landlock executable, JavaScript loader, or installation build script. The native workflow builds it on macOS x64 and owns its installed-artifact validation. diff --git a/native/system/packages/darwin-x64/README.zh.md b/native/system/packages/darwin-x64/README.zh.md new file mode 100644 index 0000000000..6441657434 --- /dev/null +++ b/native/system/packages/darwin-x64/README.zh.md @@ -0,0 +1,9 @@ +--- +description: "为 macOS x64 POSIX 锁提供预编译 system.node。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-darwin-x64 + +[English](README.md) | 中文 + +此平台包提供 `bin/system.node`,供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS x64 上构建它,并负责验证安装后的产物。 diff --git a/native/system/packages/darwin-x64/package.json b/native/system/packages/darwin-x64/package.json new file mode 100644 index 0000000000..d613acda0d --- /dev/null +++ b/native/system/packages/darwin-x64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@deepseek-ai/node-addon-system-darwin-x64", + "version": "0.1.1", + "description": "Prebuilt POSIX flock Node-API binding for macOS x64", + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", + "directory": "native/system/packages/darwin-x64" + }, + "os": ["darwin"], + "cpu": ["x64"], + "files": ["README.md", "bin/", "prebuilds.json"], + "scripts": { + "prepack": "node ../../scripts/verify-launcher-binary.mjs" + }, + "engines": { "node": ">=20" }, + "license": "BSD-3-Clause", + "publishConfig": { "access": "public" } +} diff --git a/native/system/packages/darwin-x64/prebuilds.json b/native/system/packages/darwin-x64/prebuilds.json new file mode 100644 index 0000000000..f13bcc5d5d --- /dev/null +++ b/native/system/packages/darwin-x64/prebuilds.json @@ -0,0 +1,6 @@ +{ + "platform": "darwin-x64", + "binaries": [ + { "tool": "flock", "kind": "node-api", "napi": 8, "path": "bin/system.node" } + ] +} diff --git a/native/landlock-run/packages/linux-arm64/README.i18n.yaml b/native/system/packages/entry/README.i18n.yaml similarity index 54% rename from native/landlock-run/packages/linux-arm64/README.i18n.yaml rename to native/system/packages/entry/README.i18n.yaml index fc5c8f9b11..6c10cabb1c 100644 --- a/native/landlock-run/packages/linux-arm64/README.i18n.yaml +++ b/native/system/packages/entry/README.i18n.yaml @@ -1,6 +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 native/landlock-run/packages/linux-arm64/README.md -README.md: dfcc9e97dc1393a42ff4b89ac009cdfd31e1497b -README.zh.md: 350044e92f1d0247222cc16c82f03588ed0154c9 +# pnpm run verify-translation-pairing --write native/system/packages/entry/README.md +README.md: 8f6a4d341c8f9e4c104e5706e08d9a7d45c065e1 +README.zh.md: 152257916df0cf40511b9743d389ae753ab6c85c diff --git a/native/system/packages/entry/README.md b/native/system/packages/entry/README.md new file mode 100644 index 0000000000..8f6a4d341c --- /dev/null +++ b/native/system/packages/entry/README.md @@ -0,0 +1,15 @@ +--- +description: "JavaScript entry for the prebuilt Landlock launcher and asynchronous POSIX flock." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system + +English | [中文](README.zh.md) + +The `./landlock-run` entry exports the Landlock launcher path, enforcement probe, grant arguments, and protocol constants. The independent `./flock` entry exports `tryLockExclusive(fd): Promise`; importing either entry does not load `system.node`. The package has no root export. + +The lock operation attempts `LOCK_EX | LOCK_NB` asynchronously. Keep the caller-owned descriptor open until completion; contention rejects with `EAGAIN`/`EWOULDBLOCK`, other syscall failures also reject, and errors carry their code, positive errno, and `syscall: 'flock'`. Native setup errors reject the same promise. Closing the last descriptor for the open file description releases the lock. The binding does not open, duplicate, close, or explicitly unlock descriptors. + +Optional OS/CPU platform packages carry the binaries. Linux has `bin/landlock-run` and separate `bin/glibc/system.node` / `bin/musl/system.node`; macOS has `bin/system.node`. Missing or unloadable flock bindings reject acquisition, without installation-time compilation. Landlock remains a separate executable with its existing fail-closed protocol; unsupported kernels/platforms probe unusable. + +The two C sources ship for auditability. See the workspace [architecture](../../docs/architecture.md), [support matrix](../../docs/support-matrix.md), and [CLI contract](../../docs/cli-contract.md). diff --git a/native/system/packages/entry/README.zh.md b/native/system/packages/entry/README.zh.md new file mode 100644 index 0000000000..152257916d --- /dev/null +++ b/native/system/packages/entry/README.zh.md @@ -0,0 +1,15 @@ +--- +description: "预编译 Landlock 启动器与异步 POSIX flock 的 JavaScript 入口。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system + +[English](README.md) | 中文 + +`./landlock-run` 入口导出 Landlock 启动器路径、强制执行探测、授权参数和协议常量。独立的 `./flock` 入口导出 `tryLockExclusive(fd): Promise`;导入任一入口都不会加载 `system.node`。包不提供根导出。 + +锁操作异步尝试 `LOCK_EX | LOCK_NB`。在完成前保持调用方拥有的描述符打开;竞争以 `EAGAIN`/`EWOULDBLOCK` 拒绝,其他系统调用失败也会拒绝,错误携带 code、正 errno 和 `syscall: 'flock'`。原生调用准备阶段的错误也会拒绝同一个 promise。关闭该打开文件描述的最后一个描述符即释放锁。绑定不打开、复制、关闭或显式解锁描述符。 + +可选操作系统/CPU 平台包携带二进制。Linux 包含 `bin/landlock-run` 和分别用于两种 libc 的 `bin/glibc/system.node` / `bin/musl/system.node`;macOS 包含 `bin/system.node`。flock 绑定缺失或无法加载时拒绝获取,不在安装时编译。Landlock 仍是遵循既有失败关闭协议的独立可执行文件;不支持的内核或平台探测为不可用。 + +两个 C 源文件随包分发以供审计。参见工作区[架构](../../docs/architecture.md)、[支持矩阵](../../docs/support-matrix.md)和 [CLI 约定](../../docs/cli-contract.md)。 diff --git a/native/system/packages/entry/package.json b/native/system/packages/entry/package.json new file mode 100644 index 0000000000..9f46d53f01 --- /dev/null +++ b/native/system/packages/entry/package.json @@ -0,0 +1,46 @@ +{ + "name": "@deepseek-ai/node-addon-system", + "version": "0.1.1", + "type": "module", + "description": "Prebuilt system primitives: a Linux Landlock launcher and asynchronous POSIX flock through stable Node-API", + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", + "directory": "native/system/packages/entry" + }, + "exports": { + "./landlock-run": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./flock": { + "types": "./lib/flock.d.ts", + "default": "./lib/flock.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "README.md", + "lib/", + "!lib/*.tsbuildinfo", + "src/main.c", + "src/flock.c" + ], + "scripts": { + "build:js": "tsc -b", + "prepack": "node ../../scripts/verify-entry-lib.mjs" + }, + "engines": { + "node": ">=20" + }, + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "optionalDependencies": { + "@deepseek-ai/node-addon-system-darwin-arm64": "workspace:*", + "@deepseek-ai/node-addon-system-darwin-x64": "workspace:*", + "@deepseek-ai/node-addon-system-linux-arm64": "workspace:*", + "@deepseek-ai/node-addon-system-linux-x64": "workspace:*" + } +} diff --git a/native/system/packages/entry/src/flock.c b/native/system/packages/entry/src/flock.c new file mode 100644 index 0000000000..cd5d1f2c1d --- /dev/null +++ b/native/system/packages/entry/src/flock.c @@ -0,0 +1,164 @@ +/* + * Node-API v8 binding for asynchronous flock(LOCK_EX | LOCK_NB). + * The caller owns fd through completion; this module never opens, duplicates, + * closes, or explicitly unlocks it. The callback receives zero or a positive + * errno; JavaScript owns the promise and syscall error construction. + */ + +#include +#include +#include +#include +#include + +typedef struct { + napi_env env; + napi_ref callback; + napi_async_work work; + napi_async_cleanup_hook_handle cleanup; + int fd; + int error; + bool closing; +} lock_request; + +static void check_status(napi_status status, const char *message) { + if (status != napi_ok) { + napi_fatal_error("flock", NAPI_AUTO_LENGTH, message, NAPI_AUTO_LENGTH); + } +} + +static void release_request(lock_request *request) { + if (request->callback != NULL) { + (void)napi_delete_reference(request->env, request->callback); + } + if (request->work != NULL) { + (void)napi_delete_async_work(request->env, request->work); + } + if (request->cleanup != NULL) { + (void)napi_remove_async_cleanup_hook(request->cleanup); + } + free(request); +} + +static napi_value throw_setup_error(napi_env env, napi_status status, + const char *message) { + if (status != napi_pending_exception) { + status = napi_throw_error(env, "ERR_FLOCK_ASYNC_WORK", message); + bool pending; + /* Error construction can fail with both generic_failure and a JS exception. */ + check_status(napi_is_exception_pending(env, &pending), "Cannot inspect flock setup exception"); + if (!pending && status != napi_pending_exception) check_status(status, message); + } + return NULL; +} + +static void execute_lock(napi_env env, void *data) { + (void)env; + lock_request *request = data; + request->error = flock(request->fd, LOCK_EX | LOCK_NB) == 0 ? 0 : errno; +} + +static void complete_lock(napi_env env, napi_status status, void *data) { + lock_request *request = data; + if (env != NULL && !request->closing) { + napi_value callback; + napi_value receiver; + napi_value result; + check_status(status, "flock async work did not complete"); + check_status(napi_get_reference_value(env, request->callback, &callback), + "Cannot retrieve flock callback"); + check_status(napi_get_undefined(env, &receiver), "Cannot create flock receiver"); + check_status(napi_create_int32(env, request->error, &result), + "Cannot create flock result"); + status = napi_call_function(env, receiver, callback, 1, &result, NULL); + } else { + status = napi_ok; + } + release_request(request); + /* Node's async-work dispatcher reports callback exceptions and handles termination. */ + if (status != napi_pending_exception) { + check_status(status, "Cannot invoke flock callback"); + } +} + +static void cleanup_lock(napi_async_cleanup_hook_handle handle, void *data) { + (void)handle; + lock_request *request = data; + request->closing = true; + /* + * Running work cannot be cancelled. The hook keeps the environment alive + * until completion releases both the work and this hook; it never frees + * memory that the execution thread can still access. + */ + (void)napi_cancel_async_work(request->env, request->work); +} + +static napi_value try_lock(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + double fd; + napi_status status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + if (status != napi_ok) { + return throw_setup_error(env, status, "Cannot read flock arguments"); + } + if (argc < 1 || napi_get_value_double(env, argv[0], &fd) != napi_ok) { + (void)napi_throw_type_error(env, NULL, "fd must be a number"); + return NULL; + } + if (!(fd >= INT_MIN && fd <= INT_MAX) || fd != (int)fd) { + (void)napi_throw_range_error(env, NULL, "fd must be a signed C int"); + return NULL; + } + napi_valuetype callback_type; + if (argc < 2 || napi_typeof(env, argv[1], &callback_type) != napi_ok || + callback_type != napi_function) { + (void)napi_throw_type_error(env, NULL, "callback must be a function"); + return NULL; + } + + lock_request *request = calloc(1, sizeof(*request)); + if (request == NULL) { + (void)napi_throw_error(env, "ENOMEM", "Cannot allocate flock async work"); + return NULL; + } + request->env = env; + request->fd = (int)fd; + + status = napi_create_reference(env, argv[1], 1, &request->callback); + if (status != napi_ok) { + release_request(request); + return throw_setup_error(env, status, "Cannot retain flock callback"); + } + napi_value name; + status = napi_create_string_utf8(env, "flock", NAPI_AUTO_LENGTH, &name); + if (status == napi_ok) { + status = napi_create_async_work(env, NULL, name, execute_lock, complete_lock, + request, &request->work); + } + if (status != napi_ok) { + release_request(request); + return throw_setup_error(env, status, "Cannot create flock async work"); + } + status = napi_add_async_cleanup_hook(env, cleanup_lock, request, &request->cleanup); + if (status != napi_ok) { + release_request(request); + return throw_setup_error(env, status, "Cannot register flock environment cleanup"); + } + status = napi_queue_async_work(env, request->work); + if (status != napi_ok) { + /* Queue failure schedules no completion callback. */ + release_request(request); + return throw_setup_error(env, status, "Cannot queue flock async work"); + } + return NULL; +} + +NAPI_MODULE_INIT() { + napi_value function; + if (napi_create_function(env, "tryLock", NAPI_AUTO_LENGTH, try_lock, + NULL, &function) != napi_ok || + napi_set_named_property(env, exports, "tryLock", function) != napi_ok) { + return NULL; + } + return exports; +} diff --git a/native/system/packages/entry/src/flock.ts b/native/system/packages/entry/src/flock.ts new file mode 100644 index 0000000000..fcbf8a9e3c --- /dev/null +++ b/native/system/packages/entry/src/flock.ts @@ -0,0 +1,57 @@ +/** Lazy POSIX flock entry; importing it does not load a native addon. */ +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { getSystemErrorName } from 'node:util' + +interface FlockBinding { + tryLock(fd: number, callback: (errno: number) => void): void +} + +let binding: FlockBinding | undefined + +function loadBinding(): FlockBinding { + if (binding) return binding + const { platform, arch } = process + if (platform !== 'linux' && platform !== 'darwin') { + throw Object.assign(new Error(`flock is not supported on ${platform}-${arch}`), { + code: 'ERR_FLOCK_UNSUPPORTED_PLATFORM', + syscall: 'flock', + }) + } + + let filename = 'system.node' + if (platform === 'linux') { + // Node's report types omit the libc field supplied by Linux reports. + const report = process.report.getReport() as { header: { glibcVersionRuntime?: string } } + filename = join(report.header.glibcVersionRuntime ? 'glibc' : 'musl', filename) + } + const require = createRequire(import.meta.url) + const manifest = require.resolve(`@deepseek-ai/node-addon-system-${platform}-${arch}/package.json`) + binding = require(join(dirname(manifest), 'bin', filename)) as FlockBinding + return binding +} + +/** + * Attempt an exclusive, nonblocking POSIX flock on the caller's descriptor. + * The syscall runs in asynchronous work, so acquisition can occur after this + * call returns. Keep fd open until the promise settles; the binding never + * opens, duplicates, or closes it. Closing the locked descriptor releases the + * lock once all descriptors for its open file description are closed. + * @param fd - Open file descriptor to lock; ownership remains with the caller. + * @returns A promise resolving to void on acquisition. Contention rejects with + * EAGAIN/EWOULDBLOCK; other syscall failures also reject. Syscall errors carry + * code, positive errno, and syscall='flock'. Native setup errors, unsupported + * platforms, and addon loading failures reject; importing alone does not load it. + */ +export async function tryLockExclusive(fd: number): Promise { + const errno = await new Promise((resolve) => { + loadBinding().tryLock(fd, resolve) + }) + if (errno === 0) return + const code = getSystemErrorName(-errno) + throw Object.assign(new Error(`${code}: flock failed`), { + code, + errno, + syscall: 'flock', + }) +} diff --git a/native/landlock-run/packages/entry/src/index.ts b/native/system/packages/entry/src/index.ts similarity index 97% rename from native/landlock-run/packages/entry/src/index.ts rename to native/system/packages/entry/src/index.ts index 2bb0699381..5a6c5f955f 100644 --- a/native/landlock-run/packages/entry/src/index.ts +++ b/native/system/packages/entry/src/index.ts @@ -53,7 +53,7 @@ export interface LauncherGrants { /** * Path of the launcher binary for this host: resolved from the per-platform - * npm package `@deepseek-ai/node-addon-landlock-run--` (npm's + * npm package `@deepseek-ai/node-addon-system--` (npm's * `os`/`cpu` fields make installers fetch only the matching one). When the * package is not resolvable — a platform without one, or an install that * skipped the optional dependency — the returned fallback path points inside @@ -69,7 +69,7 @@ export interface LauncherGrants { export function launcherPath( resolvePackageJson: (specifier: string) => string = createRequire(import.meta.url).resolve, ): string { - const platformPackage = `@deepseek-ai/node-addon-landlock-run-${process.platform}-${process.arch}` + const platformPackage = `@deepseek-ai/node-addon-system-${process.platform}-${process.arch}` try { return join(dirname(resolvePackageJson(`${platformPackage}/package.json`)), 'bin', LAUNCHER_BIN) } catch { diff --git a/native/landlock-run/packages/entry/src/main.c b/native/system/packages/entry/src/main.c similarity index 99% rename from native/landlock-run/packages/entry/src/main.c rename to native/system/packages/entry/src/main.c index af0cc2a988..7857030291 100644 --- a/native/landlock-run/packages/entry/src/main.c +++ b/native/system/packages/entry/src/main.c @@ -31,7 +31,7 @@ * linked statically), so the whole audit surface is this file plus the * kernel's stable syscall contract. Built natively per architecture by * `scripts/build.ts` into the per-platform npm packages - * (`@deepseek-ai/node-addon-landlock-run-linux-{x64,arm64}`); the argv grammar, + * (`@deepseek-ai/node-addon-system-linux-{x64,arm64}`); the argv grammar, * exit codes, and report lines are pinned in `docs/cli-contract.md`. */ diff --git a/native/landlock-run/packages/entry/tsconfig.json b/native/system/packages/entry/tsconfig.json similarity index 100% rename from native/landlock-run/packages/entry/tsconfig.json rename to native/system/packages/entry/tsconfig.json diff --git a/native/system/packages/linux-arm64/LICENSE b/native/system/packages/linux-arm64/LICENSE new file mode 100644 index 0000000000..8187059c9a --- /dev/null +++ b/native/system/packages/linux-arm64/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, node-addon-landlock-run contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/system/packages/linux-arm64/README.i18n.yaml b/native/system/packages/linux-arm64/README.i18n.yaml new file mode 100644 index 0000000000..881c7ebaf3 --- /dev/null +++ b/native/system/packages/linux-arm64/README.i18n.yaml @@ -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 native/system/packages/linux-arm64/README.md +README.md: b1e97481a3f59b5b7cf85e6565cd3d078e42cd7f +README.zh.md: 79f3b75e16570c4dff8d7249bcd75481f11cc85c diff --git a/native/system/packages/linux-arm64/README.md b/native/system/packages/linux-arm64/README.md new file mode 100644 index 0000000000..b1e97481a3 --- /dev/null +++ b/native/system/packages/linux-arm64/README.md @@ -0,0 +1,11 @@ +--- +description: "Prebuilt Landlock launcher and POSIX flock addons for Linux arm64." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-linux-arm64 + +English | [中文](README.zh.md) + +This platform package contains the static musl executable `bin/landlock-run` and Node-API v8 addons `bin/glibc/system.node` and `bin/musl/system.node`. The entry chooses the addon matching the running Node process's libc; the Landlock executable serves both libc systems. + +The package contains no JavaScript or installation build script. Platform prepack checks complete payloads, ELF architecture, Node-API exports, and launcher executability; the installed-artifact rehearsal checks bytes and executes native behavior. See the workspace [support matrix](../../docs/support-matrix.md). diff --git a/native/system/packages/linux-arm64/README.zh.md b/native/system/packages/linux-arm64/README.zh.md new file mode 100644 index 0000000000..79f3b75e16 --- /dev/null +++ b/native/system/packages/linux-arm64/README.zh.md @@ -0,0 +1,11 @@ +--- +description: "为 Linux arm64 提供预编译 Landlock 启动器和 POSIX flock addon。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-linux-arm64 + +[English](README.md) | 中文 + +此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。 + +包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。 diff --git a/native/landlock-run/packages/linux-arm64/package.json b/native/system/packages/linux-arm64/package.json similarity index 60% rename from native/landlock-run/packages/linux-arm64/package.json rename to native/system/packages/linux-arm64/package.json index 8e6affec6a..03bd6c216e 100644 --- a/native/landlock-run/packages/linux-arm64/package.json +++ b/native/system/packages/linux-arm64/package.json @@ -1,11 +1,11 @@ { - "name": "@deepseek-ai/node-addon-landlock-run-linux-arm64", + "name": "@deepseek-ai/node-addon-system-linux-arm64", "version": "0.1.1", - "description": "Prebuilt landlock-run Landlock launcher binary for linux-arm64 (static musl) — resolved as a file path by @deepseek-ai/node-addon-landlock-run, never imported", + "description": "Linux arm64 system binaries: static Landlock launcher and glibc/musl Node-API flock addons", "repository": { "type": "git", "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", - "directory": "native/landlock-run/packages/linux-arm64" + "directory": "native/system/packages/linux-arm64" }, "os": [ "linux" diff --git a/native/system/packages/linux-arm64/prebuilds.json b/native/system/packages/linux-arm64/prebuilds.json new file mode 100644 index 0000000000..aef7abf975 --- /dev/null +++ b/native/system/packages/linux-arm64/prebuilds.json @@ -0,0 +1,24 @@ +{ + "platform": "linux-arm64", + "binaries": [ + { + "tool": "landlock-run", + "kind": "static-musl", + "path": "bin/landlock-run" + }, + { + "tool": "flock", + "kind": "node-api", + "napi": 8, + "libc": "glibc", + "path": "bin/glibc/system.node" + }, + { + "tool": "flock", + "kind": "node-api", + "napi": 8, + "libc": "musl", + "path": "bin/musl/system.node" + } + ] +} diff --git a/native/system/packages/linux-x64/LICENSE b/native/system/packages/linux-x64/LICENSE new file mode 100644 index 0000000000..8187059c9a --- /dev/null +++ b/native/system/packages/linux-x64/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, node-addon-landlock-run contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/system/packages/linux-x64/README.i18n.yaml b/native/system/packages/linux-x64/README.i18n.yaml new file mode 100644 index 0000000000..631328590a --- /dev/null +++ b/native/system/packages/linux-x64/README.i18n.yaml @@ -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 native/system/packages/linux-x64/README.md +README.md: caebb5d8a888a69a952fc89afb3edfb8ece489fc +README.zh.md: 6ad1dcf6fe26007f76cdeb98936a94262ece4d7c diff --git a/native/system/packages/linux-x64/README.md b/native/system/packages/linux-x64/README.md new file mode 100644 index 0000000000..caebb5d8a8 --- /dev/null +++ b/native/system/packages/linux-x64/README.md @@ -0,0 +1,11 @@ +--- +description: "Prebuilt Landlock launcher and POSIX flock addons for Linux x64." +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-linux-x64 + +English | [中文](README.zh.md) + +This platform package contains the static musl executable `bin/landlock-run` and Node-API v8 addons `bin/glibc/system.node` and `bin/musl/system.node`. The entry chooses the addon matching the running Node process's libc; the Landlock executable serves both libc systems. + +The package contains no JavaScript or installation build script. Platform prepack checks complete payloads, ELF architecture, Node-API exports, and launcher executability; the installed-artifact rehearsal checks bytes and executes native behavior. See the workspace [support matrix](../../docs/support-matrix.md). diff --git a/native/system/packages/linux-x64/README.zh.md b/native/system/packages/linux-x64/README.zh.md new file mode 100644 index 0000000000..6ad1dcf6fe --- /dev/null +++ b/native/system/packages/linux-x64/README.zh.md @@ -0,0 +1,11 @@ +--- +description: "为 Linux x64 提供预编译 Landlock 启动器和 POSIX flock addon。" +kind: "package-library" +--- +# @deepseek-ai/node-addon-system-linux-x64 + +[English](README.md) | 中文 + +此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。 + +包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。 diff --git a/native/landlock-run/packages/linux-x64/package.json b/native/system/packages/linux-x64/package.json similarity index 60% rename from native/landlock-run/packages/linux-x64/package.json rename to native/system/packages/linux-x64/package.json index 7e9cab7a1a..3f7159e75c 100644 --- a/native/landlock-run/packages/linux-x64/package.json +++ b/native/system/packages/linux-x64/package.json @@ -1,11 +1,11 @@ { - "name": "@deepseek-ai/node-addon-landlock-run-linux-x64", + "name": "@deepseek-ai/node-addon-system-linux-x64", "version": "0.1.1", - "description": "Prebuilt landlock-run Landlock launcher binary for linux-x64 (static musl) — resolved as a file path by @deepseek-ai/node-addon-landlock-run, never imported", + "description": "Linux x64 system binaries: static Landlock launcher and glibc/musl Node-API flock addons", "repository": { "type": "git", "url": "git+https://github.com/deepseek-harness/deepseek-harness.git", - "directory": "native/landlock-run/packages/linux-x64" + "directory": "native/system/packages/linux-x64" }, "os": [ "linux" diff --git a/native/system/packages/linux-x64/prebuilds.json b/native/system/packages/linux-x64/prebuilds.json new file mode 100644 index 0000000000..9408c7d430 --- /dev/null +++ b/native/system/packages/linux-x64/prebuilds.json @@ -0,0 +1,24 @@ +{ + "platform": "linux-x64", + "binaries": [ + { + "tool": "landlock-run", + "kind": "static-musl", + "path": "bin/landlock-run" + }, + { + "tool": "flock", + "kind": "node-api", + "napi": 8, + "libc": "glibc", + "path": "bin/glibc/system.node" + }, + { + "tool": "flock", + "kind": "node-api", + "napi": 8, + "libc": "musl", + "path": "bin/musl/system.node" + } + ] +} diff --git a/native/landlock-run/scripts/assemble-prebuilds.mjs b/native/system/scripts/assemble-prebuilds.mjs similarity index 82% rename from native/landlock-run/scripts/assemble-prebuilds.mjs rename to native/system/scripts/assemble-prebuilds.mjs index 4dcdb23bed..1be3be74d2 100644 --- a/native/landlock-run/scripts/assemble-prebuilds.mjs +++ b/native/system/scripts/assemble-prebuilds.mjs @@ -4,7 +4,7 @@ * verify the result. The Release workflow's build legs upload one * `prebuild-` artifact per platform package (its `bin/` payload); * this script copies each into `packages//bin/` and then checks - * every declared binary for presence and ELF architecture. + * every declared binary for presence and native architecture. * * Usage: `node scripts/assemble-prebuilds.mjs `. */ @@ -39,13 +39,16 @@ for (const artifactName of fs.readdirSync(artifactRoot)) { for (const file of fs.readdirSync(artifactDir)) { const source = path.join(artifactDir, file); const destination = path.join(root, 'packages', name, 'bin', file); - fs.copyFileSync(source, destination); - fs.chmodSync(destination, 0o755); + fs.cpSync(source, destination, { recursive: true, preserveTimestamps: true }); console.log(`Copied ${path.relative(root, source)} -> ${path.relative(root, destination)}`); } } for (const dir of platformDirs()) { + const metadata = JSON.parse(fs.readFileSync(path.join(root, dir, 'prebuilds.json'), 'utf8')); + for (const binary of metadata.binaries) { + if (binary.kind === 'static-musl') fs.chmodSync(path.join(root, dir, binary.path), 0o755); + } const { name, count } = verifyPlatformBinaries(path.join(root, dir)); console.log(`Verified ${name}: ${count} binaries`); } diff --git a/native/system/scripts/build-test-oracle.mjs b/native/system/scripts/build-test-oracle.mjs new file mode 100644 index 0000000000..22f6bf9f4b --- /dev/null +++ b/native/system/scripts/build-test-oracle.mjs @@ -0,0 +1,23 @@ +/** Build the independent POSIX flock oracle used by native behavior tests. */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +if (process.platform !== 'linux' && process.platform !== 'darwin') { + throw new Error('The flock oracle is a POSIX test fixture'); +} +const variants = process.platform === 'linux' ? ['glibc', 'musl'] : ['']; +for (const variant of variants) { + const compiler = variant === 'musl' ? 'musl-gcc' : 'cc'; + const output = path.join(root, 'test/bin', variant, 'flock-oracle'); + fs.mkdirSync(path.dirname(output), { recursive: true }); + const args = ['-std=c11', '-O2', '-Wall', '-Wextra', '-Werror']; + if (process.platform === 'darwin') args.push('-mmacosx-version-min=11.0'); + if (variant === 'musl') args.push('-static'); + const result = spawnSync(compiler, [...args, path.join(root, 'test/fixtures/flock-oracle.c'), '-o', output], { stdio: 'inherit' }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`${compiler} failed to build the flock oracle`); + console.log(`Built test oracle ${path.relative(root, output)}`); +} diff --git a/native/system/scripts/build.ts b/native/system/scripts/build.ts new file mode 100644 index 0000000000..ca995d9c03 --- /dev/null +++ b/native/system/scripts/build.ts @@ -0,0 +1,92 @@ +/** + * Build this host's declared system binaries. Landlock is a static musl + * executable; flock uses stable Node-API with separate Linux libc builds. + * Node headers come from the Node installation running this script. + */ +import { spawnSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs' +import { basename, dirname, join, resolve } from 'node:path' +import { parseArgs } from 'node:util' + +const root = resolve(import.meta.dirname, '..') +const { values } = parseArgs({ options: { 'host-addon-only': { type: 'boolean' } }, allowPositionals: false }) +const hostAddonOnly = values['host-addon-only'] === true +const sources: Record = { + 'landlock-run': 'packages/entry/src/main.c', + flock: 'packages/entry/src/flock.c', +} + +interface Binary { + tool: string + kind: string + path: string + napi?: number + libc?: string +} + +if (process.platform !== 'linux' && process.platform !== 'darwin') { + if (hostAddonOnly) process.exit(0) + throw new Error('build: system binaries are built on Linux or macOS; no native target for this host') +} +const host = `${process.platform}-${process.arch}` +const libc = process.platform === 'linux' + ? ((process.report.getReport() as { header: { glibcVersionRuntime?: string } }).header.glibcVersionRuntime ? 'glibc' : 'musl') + : undefined +const headers = resolve(dirname(process.execPath), '../include/node') +let built = 0 + +for (const name of readdirSync(join(root, 'packages')).sort()) { + const dir = join(root, 'packages', name) + const metadata = join(dir, 'prebuilds.json') + if (!existsSync(metadata)) continue + const spec = JSON.parse(readFileSync(metadata, 'utf8')) as { platform: string; binaries: Binary[] } + if (spec.platform !== host) continue + + for (const binary of spec.binaries) { + if (hostAddonOnly && (binary.kind !== 'node-api' || (binary.libc !== undefined && binary.libc !== libc))) continue + const source = sources[binary.tool] + if (source === undefined) throw new Error(`build: unknown tool ${binary.tool}`) + const output = join(dir, binary.path) + mkdirSync(dirname(output), { recursive: true }) + let compiler: string + let flags: string[] + + if (binary.kind === 'static-musl' && process.platform === 'linux' && binary.tool === 'landlock-run') { + compiler = 'musl-gcc' + flags = ['-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s'] + } else if (binary.kind === 'node-api' && binary.tool === 'flock' && binary.napi === 8) { + if (!existsSync(join(headers, 'node_api.h'))) { + throw new Error(`build: Node-API headers missing at ${headers}; use a Node installation with development headers`) + } + compiler = process.platform === 'linux' && binary.libc === 'musl' ? 'musl-gcc' : 'cc' + flags = ['-std=c11', '-O2', '-Wall', '-Wextra', '-Werror', '-fPIC', '-fvisibility=hidden', '-DNAPI_VERSION=8', '-I', headers] + if (process.platform === 'darwin') { + if (binary.libc !== undefined) throw new Error('build: macOS flock does not select a Linux libc') + flags.push('-bundle', '-undefined', 'dynamic_lookup', '-mmacosx-version-min=11.0') + } else { + if (binary.libc !== 'glibc' && binary.libc !== 'musl') { + throw new Error('build: Linux flock must select glibc or musl') + } + flags.push('-shared') + } + } else { + throw new Error(`build: unsupported ${binary.tool}/${binary.kind} target on ${host}`) + } + + mkdirSync(join(root, '.release'), { recursive: true }) + const temporary = mkdtempSync(join(root, '.release', 'native-build-')) + try { + const pending = join(temporary, basename(output)) + const result = spawnSync(compiler, [...flags, '-o', pending, join(root, source)], { stdio: 'inherit' }) + if (result.error) throw result.error + if (result.status !== 0) throw new Error(`build: ${compiler} failed for ${binary.path}`) + // Readers never see a truncated addon when source checks build concurrently. + renameSync(pending, output) + } finally { + rmSync(temporary, { recursive: true, force: true }) + } + console.log(`build: built ${basename(dir)}/${binary.path}`) + built++ + } +} +if (built === 0) throw new Error(`build: no declared binaries for ${host}`) diff --git a/native/landlock-run/scripts/bump-release.mjs b/native/system/scripts/bump-release.mjs similarity index 100% rename from native/landlock-run/scripts/bump-release.mjs rename to native/system/scripts/bump-release.mjs diff --git a/native/landlock-run/scripts/commit-release.mjs b/native/system/scripts/commit-release.mjs similarity index 90% rename from native/landlock-run/scripts/commit-release.mjs rename to native/system/scripts/commit-release.mjs index 1b1c78bce5..e2e2dac691 100644 --- a/native/landlock-run/scripts/commit-release.mjs +++ b/native/system/scripts/commit-release.mjs @@ -37,6 +37,6 @@ run('git', [ 'packages/*/package.json', '../../pnpm-lock.yaml', ]); -run('git', ['commit', '-m', `release(landlock-run): ${version}`]); +run('git', ['commit', '-m', `release(node-addon-system): ${version}`]); -console.log(`Committed release ${version}. Create the tag manually: git tag landlock-run-v${version}`); +console.log(`Committed release ${version}. Create the tag manually: git tag node-addon-system-v${version}`); diff --git a/native/landlock-run/scripts/github-matrix.mjs b/native/system/scripts/github-matrix.mjs similarity index 91% rename from native/landlock-run/scripts/github-matrix.mjs rename to native/system/scripts/github-matrix.mjs index 9566b89c8a..312d2fea93 100644 --- a/native/landlock-run/scripts/github-matrix.mjs +++ b/native/system/scripts/github-matrix.mjs @@ -15,6 +15,8 @@ import { platformDirs, readJson, root } from './repo.mjs'; const RUNNERS = { 'linux-x64': 'ubuntu-24.04', 'linux-arm64': 'ubuntu-24.04-arm', + 'darwin-x64': 'macos-15-intel', + 'darwin-arm64': 'macos-latest', }; function runnerFor(platform) { @@ -56,6 +58,7 @@ const target = process.argv[2]; const matrices = { ci: ciMatrix, 'release-prebuild': releasePrebuildMatrix, + compatibility: () => ciMatrix().include.flatMap((row) => [20, 22, 24, 26].map((node) => ({ ...row, node }))), }; if (!target || !matrices[target]) { diff --git a/native/landlock-run/scripts/pack-release.mjs b/native/system/scripts/pack-release.mjs similarity index 100% rename from native/landlock-run/scripts/pack-release.mjs rename to native/system/scripts/pack-release.mjs diff --git a/native/landlock-run/scripts/publish-release.mjs b/native/system/scripts/publish-release.mjs similarity index 100% rename from native/landlock-run/scripts/publish-release.mjs rename to native/system/scripts/publish-release.mjs diff --git a/native/system/scripts/repo.mjs b/native/system/scripts/repo.mjs new file mode 100644 index 0000000000..ceb1de6f1d --- /dev/null +++ b/native/system/scripts/repo.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/** + * Shared helpers for the repo scripts: package discovery, the checked-in + * prebuild matrix, and binary verification. The package matrix is explicit + * metadata — `packages//prebuilds.json` marks a platform package and + * declares its binaries; everything else under `packages/` is an entry + * package. Scripts derive from these files and never guess. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const root = fileURLToPath(new URL('..', import.meta.url)); +const packagesRoot = path.join(root, 'packages'); + +/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */ +const E_MACHINE = { x64: 62, arm64: 183 }; + +export function readJson(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +/** Platform packages: every `packages/` carrying a `prebuilds.json`. */ +export function platformDirs() { + return fs.readdirSync(packagesRoot) + .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) + .sort() + .map((name) => path.join('packages', name)); +} + +/** Entry packages: every other `packages/` with a `package.json`. */ +export function entryDirs() { + return fs.readdirSync(packagesRoot) + .filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) + .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json'))) + .sort() + .map((name) => path.join('packages', name)); +} + +/** All published packages in publish order: platform packages before the entries that optionally depend on them. */ +export function packageDirs() { + return [...platformDirs(), ...entryDirs()]; +} + +/** + * Verify platform metadata, complete bin/ payloads, executable permissions, + * and native file formats before packing. Node addons must export Node-API. + */ +export function verifyPlatformBinaries(packageDir) { + const manifest = readJson(path.join(packageDir, 'package.json')); + const prebuilds = readJson(path.join(packageDir, 'prebuilds.json')); + const cpu = manifest.cpu?.[0]; + const os = manifest.os?.[0]; + if (!(cpu in E_MACHINE) || !['linux', 'darwin'].includes(os)) { + throw new Error(`${manifest.name}: unsupported or missing os/cpu metadata`); + } + if (prebuilds.platform !== `${os}-${cpu}`) { + throw new Error(`${manifest.name}: prebuild platform disagrees with package os/cpu`); + } + + const declared = new Set(); + for (const binary of prebuilds.binaries) { + if (typeof binary.path !== 'string' || !/^bin\/(?:[a-z0-9-]+\/)?[a-z0-9._-]+$/.test(binary.path)) { + throw new Error(`${manifest.name}: binary path must name a file inside bin/`); + } + if (declared.has(binary.path)) throw new Error(`${manifest.name}: duplicate binary path ${binary.path}`); + declared.add(binary.path); + const executable = binary.kind === 'static-musl' && binary.tool === 'landlock-run' && os === 'linux'; + const addon = binary.kind === 'node-api' && binary.tool === 'flock' && binary.napi === 8; + if (!executable && !addon) throw new Error(`${manifest.name}: unsupported binary kind/tool/NAPI for ${binary.path}`); + if (addon && os === 'linux' && !['glibc', 'musl'].includes(binary.libc)) { + throw new Error(`${manifest.name}: Linux addon must declare glibc or musl`); + } + if (addon && os === 'darwin' && binary.libc !== undefined) { + throw new Error(`${manifest.name}: macOS addon must not declare a Linux libc`); + } + + const file = path.join(packageDir, binary.path); + if (!fs.existsSync(file)) throw new Error(`${manifest.name}: missing ${binary.path} — build this platform before packing`); + if (!fs.lstatSync(file).isFile()) throw new Error(`${manifest.name}: ${binary.path} is not a regular file`); + if (executable) { + try { fs.accessSync(file, fs.constants.X_OK); } + catch { throw new Error(`${manifest.name}: ${binary.path} is not executable`); } + } + const data = fs.readFileSync(file); + if (os === 'linux') { + if (data.length < 64 || data.readUInt32LE(0) !== 0x464c457f || data[4] !== 2 || data[5] !== 1) { + throw new Error(`${manifest.name}: ${binary.path} is not a little-endian ELF64 binary`); + } + if (data.readUInt16LE(18) !== E_MACHINE[cpu]) { + throw new Error(`${manifest.name}: ${binary.path} has the wrong ELF architecture`); + } + if (data.readUInt16LE(16) !== (executable ? 2 : 3)) { + throw new Error(`${manifest.name}: ${binary.path} has the wrong ELF file type`); + } + } else { + const expectedCpu = cpu === 'x64' ? 0x01000007 : 0x0100000c; + if (data.length < 32 || data.readUInt32LE(0) !== 0xfeedfacf) { + throw new Error(`${manifest.name}: ${binary.path} is not a Mach-O 64-bit bundle`); + } + if (data.readUInt32LE(4) !== expectedCpu || data.readUInt32LE(12) !== 8) { + throw new Error(`${manifest.name}: ${binary.path} has the wrong Mach-O architecture or file type`); + } + } + if (addon && (!data.includes(Buffer.from('napi_register_module_v1')) + || !data.includes(Buffer.from('node_api_module_get_api_version_v1')))) { + throw new Error(`${manifest.name}: ${binary.path} does not export the Node-API entry points`); + } + } + + function files(dir, prefix) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const name = prefix + '/' + entry.name; + return entry.isDirectory() ? files(path.join(dir, entry.name), name) : [name]; + }); + } + const extra = files(path.join(packageDir, 'bin'), 'bin').filter((name) => !declared.has(name)); + if (extra.length) throw new Error(`${manifest.name}: undeclared bin/ files: ${extra.join(', ')}`); + return { name: manifest.name, count: declared.size }; +} diff --git a/native/landlock-run/scripts/verify-entry-lib.mjs b/native/system/scripts/verify-entry-lib.mjs similarity index 78% rename from native/landlock-run/scripts/verify-entry-lib.mjs rename to native/system/scripts/verify-entry-lib.mjs index 214705e2bc..2fc21eae1e 100644 --- a/native/landlock-run/scripts/verify-entry-lib.mjs +++ b/native/system/scripts/verify-entry-lib.mjs @@ -16,7 +16,10 @@ import path from 'node:path'; const packageDir = process.cwd(); const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')); -for (const file of ['lib/index.js', 'lib/index.d.ts']) { +const exportedFiles = Object.values(manifest.exports) + .flatMap((entry) => typeof entry === 'string' ? [entry] : Object.values(entry)) + .filter((file) => typeof file === 'string' && file.startsWith('./lib/')); +for (const file of exportedFiles) { if (!fs.existsSync(path.join(packageDir, file))) { console.error(`verify-entry-lib: ${manifest.name} has no ${file} — run \`pnpm build:ts\` before packing.`); process.exit(1); diff --git a/native/landlock-run/scripts/verify-launcher-binary.mjs b/native/system/scripts/verify-launcher-binary.mjs similarity index 84% rename from native/landlock-run/scripts/verify-launcher-binary.mjs rename to native/system/scripts/verify-launcher-binary.mjs index 997b20ca0f..ae89102b16 100644 --- a/native/landlock-run/scripts/verify-launcher-binary.mjs +++ b/native/system/scripts/verify-launcher-binary.mjs @@ -7,8 +7,8 @@ * `pnpm run build:native` would ship an EMPTY platform package — the * binary's absence surfacing only at runtime as a failed probe on every * consumer — and a binary copied across packages would advertise an - * architecture it cannot execute. The check is presence + ELF `e_machine` - * against the package's declared `cpu`. `verify-packed-install.mjs` + * architecture it cannot execute. Checks cover ELF/Mach-O format, architecture, + * declared payloads, and Node-API exports. `verify-packed-install.mjs` * separately pins the installed tarball bytes to the workspace build. * * Runs from each platform package's `prepack` hook (pnpm sets the script @@ -23,7 +23,7 @@ const packageDir = process.argv[2] ? path.resolve(root, process.argv[2]) : proce try { const { name, count } = verifyPlatformBinaries(packageDir); - console.log(`verify-launcher-binary: ${name} — ${count} binaries present with the right ELF architecture.`); + console.log(`verify-launcher-binary: ${name} — ${count} binaries present with the right native format and architecture.`); } catch (error) { console.error(`verify-launcher-binary: ${error instanceof Error ? error.message : error}`); process.exit(1); diff --git a/native/landlock-run/scripts/verify-packed-install.mjs b/native/system/scripts/verify-packed-install.mjs similarity index 80% rename from native/landlock-run/scripts/verify-packed-install.mjs rename to native/system/scripts/verify-packed-install.mjs index 928fff50f7..ebd6df487d 100644 --- a/native/landlock-run/scripts/verify-packed-install.mjs +++ b/native/system/scripts/verify-packed-install.mjs @@ -4,15 +4,14 @@ * exactly what a consumer install needs. `pnpm pack` already produced the * bytes `pnpm publish` would upload; this script checks the payload * (coverage, concrete dependency versions, NO lifecycle install scripts — - * this family has no install fallback on purpose), unpacks the entry plus + * this family has no install fallback on purpose), installs the entry plus * THIS host's platform tarball into a throwaway consumer OUTSIDE the repo, * byte-pins the installed binary against the workspace build it was packed * from, and drives the INSTALLED entry under plain `node` — resolution, * probe, and a real confinement world-proof through the installed launcher. * - * On non-Linux hosts (no platform package exists) it instead proves the - * documented degradation: resolution falls back to a nonexistent path and - * the probe reports `unusable`. + * On non-Linux hosts it proves that Landlock remains unavailable, while + * supported POSIX hosts independently exercise the flock binding. * * Usage: `node scripts/verify-packed-install.mjs [tarball-dir] [--current-platform-only]`. * The flag skips the all-platforms tarball-presence check for @@ -31,7 +30,7 @@ import { entryDirs, packageDirs, platformDirs, readJson, root } from './repo.mjs const args = process.argv.slice(2); const currentPlatformOnly = args.includes('--current-platform-only'); const tarballDir = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm')); -const entryPackageName = '@deepseek-ai/node-addon-landlock-run'; +const entryPackageName = '@deepseek-ai/node-addon-system'; function tarballName(manifest) { if (manifest.name.startsWith('@')) { @@ -56,7 +55,7 @@ function run(command, commandArgs, options = {}) { }); if (result.error) throw result.error; if (result.status !== 0) { - process.exit(result.status ?? 1); + throw new Error(`${command} failed (status=${result.status}, signal=${result.signal})`); } } @@ -98,19 +97,6 @@ function packageInstallDir(packageName) { return path.join(tempRoot, 'node_modules', ...packageName.split('/')); } -function unpackTarball(manifest) { - const extractRoot = fs.mkdtempSync(path.join(tempRoot, 'extract-')); - run('tar', ['-xzf', tarballPath(manifest), '-C', extractRoot]); - - const source = path.join(extractRoot, 'package'); - const destination = packageInstallDir(manifest.name); - fs.rmSync(destination, { recursive: true, force: true }); - fs.mkdirSync(path.dirname(destination), { recursive: true }); - fs.renameSync(source, destination); - fs.rmSync(extractRoot, { recursive: true, force: true }); - console.log(`Unpacked ${manifest.name} -> ${path.relative(tempRoot, destination)}`); -} - const manifests = packageDirs().map((dir) => ({ dir, manifest: readJson(path.join(root, dir, 'package.json')) })); const entryManifest = manifests.find(({ manifest }) => manifest.name === entryPackageName)?.manifest; if (!entryManifest) throw new Error(`missing source manifest for ${entryPackageName}`); @@ -144,16 +130,16 @@ for (const { manifest } of expectedTarballs) { } // Throwaway ESM consumer, built from local tarballs only — no registry. -const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-packed-install-')); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'native-system-packed-')); +try { fs.writeFileSync( path.join(tempRoot, 'package.json'), - `${JSON.stringify({ name: 'nalr-packed-install-check', version: '0.0.0', private: true, type: 'module' }, null, 2)}\n`, + `${JSON.stringify({ name: 'native-system-packed-check', version: '0.0.0', private: true, type: 'module', dependencies: Object.fromEntries([entryManifest, ...(currentPlatformEntry ? [currentPlatformEntry.manifest] : [])].map((manifest) => [manifest.name, `file:${tarballPath(manifest)}`])) }, null, 2)}\n`, ); console.log(`Verifying packed install in ${tempRoot}`); -unpackTarball(entryManifest); +run('npm', ['install', '--offline', '--no-audit', '--no-fund', '--package-lock=false'], { cwd: tempRoot }); if (currentPlatformEntry) { - unpackTarball(currentPlatformEntry.manifest); // Byte-pin: the installed binary must be the workspace build it was packed // from — any divergence means the tarball did not carry the built bytes. @@ -180,10 +166,15 @@ import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run'; +import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-system/landlock-run'; +import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock'; + +await assert.rejects(import('@deepseek-ai/node-addon-system'), { + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', +}); const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1'; -const platformPackage = '@deepseek-ai/node-addon-landlock-run-' + process.platform + '-' + process.arch; +const platformPackage = '@deepseek-ai/node-addon-system-' + process.platform + '-' + process.arch; const resolved = launcherPath(); assert.ok(path.isAbsolute(resolved), 'launcherPath must be absolute'); assert.ok(resolved.includes(path.join(...platformPackage.split('/'))), 'launcherPath must point into the platform package: ' + resolved); @@ -213,11 +204,35 @@ if (process.platform === 'linux') { console.log('confinement world-proof passed through the installed launcher'); } } else { - assert.ok(!fs.existsSync(resolved), 'no platform package exists for this host — the fallback path must not exist'); + assert.ok(!fs.existsSync(resolved), 'Landlock has no executable for this host'); assert.equal(probe(resolved), 'unusable'); console.log('non-linux host: fallback resolution and unusable probe verified'); } + +if (process.platform === 'linux' || process.platform === 'darwin') { + const lockRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'native-system-flock-')); + const handles = []; + try { + const lock = path.join(lockRoot, 'lock'); + const a = fs.openSync(lock, 'wx+', 0o600); + handles.push(a); + const b = fs.openSync(lock, 'r+'); + handles.push(b); + await tryLockExclusive(a); + await assert.rejects(tryLockExclusive(b), { code: 'EAGAIN' }); + fs.closeSync(a); + handles.splice(handles.indexOf(a), 1); + await tryLockExclusive(b); + console.log('installed Node-API flock: exclusion and close release verified'); + } finally { + for (const fd of handles) fs.closeSync(fd); + fs.rmSync(lockRoot, { recursive: true, force: true }); + } +} `); run(process.execPath, [driver], { cwd: tempRoot }); console.log('Packed install verification passed.'); +} finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); +} diff --git a/native/landlock-run/scripts/verify-release.mjs b/native/system/scripts/verify-release.mjs similarity index 80% rename from native/landlock-run/scripts/verify-release.mjs rename to native/system/scripts/verify-release.mjs index ff3917b4b5..f09faa7bd9 100644 --- a/native/landlock-run/scripts/verify-release.mjs +++ b/native/system/scripts/verify-release.mjs @@ -2,15 +2,15 @@ /** * Release verification. Always: every published package carries one shared * 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 + * `node-addon-system-vX.Y.Z` tag matches it. With `--prebuilds`: every platform package's declared + * binaries exist with the right native format and architecture (run after * `assemble-prebuilds.mjs` or a local `build:native`). */ import path from 'node:path'; import { packageDirs, platformDirs, readJson, root, verifyPlatformBinaries } from './repo.mjs'; -const TAG_PREFIX = 'refs/tags/landlock-run-v'; +const TAG_PREFIX = 'refs/tags/node-addon-system-v'; function verifyVersions() { const packages = packageDirs().map((dir) => ({ @@ -29,12 +29,12 @@ function verifyVersions() { const ref = process.env.GITHUB_REF || ''; const publish = process.env.RELEASE_PUBLISH === 'true'; if (publish && !ref.startsWith(TAG_PREFIX)) { - throw new Error('publishing requires running the workflow from a landlock-run-v* tag'); + throw new Error('publishing requires running the workflow from a node-addon-system-v* tag'); } if (ref.startsWith(TAG_PREFIX)) { const tagVersion = ref.slice(TAG_PREFIX.length); if (tagVersion !== version) { - throw new Error(`tag/version mismatch: tag landlock-run-v${tagVersion}, packages ${version}`); + throw new Error(`tag/version mismatch: tag node-addon-system-v${tagVersion}, packages ${version}`); } } diff --git a/native/landlock-run/test/entry.test.js b/native/system/test/entry.test.js similarity index 92% rename from native/landlock-run/test/entry.test.js rename to native/system/test/entry.test.js index 76e2e4321a..b93bc2a6eb 100644 --- a/native/landlock-run/test/entry.test.js +++ b/native/system/test/entry.test.js @@ -15,7 +15,11 @@ import { grantArgs, launcherPath, probe, -} from '@deepseek-ai/node-addon-landlock-run'; +} from '@deepseek-ai/node-addon-system/landlock-run'; + +await assert.rejects(import('@deepseek-ai/node-addon-system'), { + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', +}); // --- constants are part of the CLI contract --- assert.equal(LAUNCHER_BIN, 'landlock-run'); @@ -31,7 +35,7 @@ assert.deepEqual( assert.deepEqual(grantArgs({ readWrite: ['/a'], readOnly: ['/b'] }), ['--ro', '/b', '--rw', '/a']); // --- launcherPath: resolves the platform package next to its package.json --- -const platformPackage = `@deepseek-ai/node-addon-landlock-run-${process.platform}-${process.arch}`; +const platformPackage = `@deepseek-ai/node-addon-system-${process.platform}-${process.arch}`; const resolvedViaSeam = launcherPath((specifier) => { assert.equal(specifier, `${platformPackage}/package.json`); return path.join('/fake-install', specifier); diff --git a/native/system/test/fixtures/flock-binding.js b/native/system/test/fixtures/flock-binding.js new file mode 100644 index 0000000000..ee2d00d052 --- /dev/null +++ b/native/system/test/fixtures/flock-binding.js @@ -0,0 +1,12 @@ +/** Load the private callback API to test native completion independently of its Promise wrapper. */ +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +/** @returns The built addon's private callback binding for this host. */ +export function loadFlockBinding() { + const libc = process.platform === 'linux' + ? `${process.report.getReport().header.glibcVersionRuntime ? 'glibc' : 'musl'}/` + : ''; + const binary = new URL(`../../packages/${process.platform}-${process.arch}/bin/${libc}system.node`, import.meta.url); + return createRequire(import.meta.url)(fileURLToPath(binary)); +} diff --git a/native/system/test/fixtures/flock-callback-throws.js b/native/system/test/fixtures/flock-callback-throws.js new file mode 100644 index 0000000000..f1039b8895 --- /dev/null +++ b/native/system/test/fixtures/flock-callback-throws.js @@ -0,0 +1,6 @@ +/** Isolate an uncaught native-callback exception from the test runner. */ +import { loadFlockBinding } from './flock-binding.js'; + +const binding = loadFlockBinding(); +process.send({ type: 'ready' }); +binding.tryLock(-1, () => { throw new Error('flock callback failure'); }); diff --git a/native/system/test/fixtures/flock-child.js b/native/system/test/fixtures/flock-child.js new file mode 100644 index 0000000000..81033f4bd6 --- /dev/null +++ b/native/system/test/fixtures/flock-child.js @@ -0,0 +1,42 @@ +/** IPC-controlled lock holder; acknowledgements follow settled syscalls or close. */ +import assert from 'node:assert/strict'; +import { once, on } from 'node:events'; +import { closeSync, openSync } from 'node:fs'; +import { tryLockExclusive } from '../../packages/entry/lib/flock.js'; + +const messages = on(process, 'message'); +let fd = openSync(process.argv[2], 'a+', 0o600); +try { + await send({ type: 'ready' }); + for await (const [command] of messages) { + if (command === 'close') { + closeSync(fd); + fd = undefined; + await send({ type: 'closed' }); + break; + } + assert.equal(command, 'tryLock'); + let reply; + try { + await tryLockExclusive(fd); + reply = { type: 'locked' }; + } catch (error) { + reply = { type: 'error', code: error.code, errno: error.errno, syscall: error.syscall }; + } + await send(reply); + } +} finally { + await messages.return(); + if (fd !== undefined) closeSync(fd); + if (process.connected) { + const disconnected = once(process, 'disconnect'); + process.disconnect(); + await disconnected; + } +} + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => error ? reject(error) : resolve()); + }); +} diff --git a/native/system/test/fixtures/flock-import.js b/native/system/test/fixtures/flock-import.js new file mode 100644 index 0000000000..49ca011fe8 --- /dev/null +++ b/native/system/test/fixtures/flock-import.js @@ -0,0 +1,22 @@ +/** A separate process keeps unsupported-platform simulation away from other tests. */ +import assert from 'node:assert/strict'; + +const platform = process.argv[2]; +const descriptor = Object.getOwnPropertyDescriptor(process, 'platform'); +try { + if (platform) Object.defineProperty(process, 'platform', { value: platform }); + const { tryLockExclusive } = await import('../../packages/entry/lib/flock.js'); + assert.equal(typeof tryLockExclusive, 'function'); + if (platform || (process.platform !== 'linux' && process.platform !== 'darwin')) { + await assert.rejects(tryLockExclusive(-1), { + code: 'ERR_FLOCK_UNSUPPORTED_PLATFORM', + syscall: 'flock', + }); + } +} finally { + Object.defineProperty(process, 'platform', descriptor); +} +await new Promise((resolve, reject) => { + process.send({ type: 'ready' }, (error) => error ? reject(error) : resolve()); +}); +process.disconnect(); diff --git a/native/system/test/fixtures/flock-inherited-child.js b/native/system/test/fixtures/flock-inherited-child.js new file mode 100644 index 0000000000..2d434af541 --- /dev/null +++ b/native/system/test/fixtures/flock-inherited-child.js @@ -0,0 +1,37 @@ +/** fd 4 is inherited through spawn's stdio mapping, never reopened by path. */ +import assert from 'node:assert/strict'; +import { on } from 'node:events'; +import { closeSync, fstatSync } from 'node:fs'; +import { tryLockExclusive } from '../../packages/entry/lib/flock.js'; + +const messages = on(process, 'message'); +let fd = 4; +try { + assert.ok(fstatSync(fd).isFile()); + await send({ type: 'ready' }); + for await (const [command] of messages) { + if (command === 'quit') { + await send({ type: 'bye' }); + break; + } + if (command === 'close') { + closeSync(fd); + fd = undefined; + await send({ type: 'closed' }); + continue; + } + assert.equal(command, 'tryLock'); + await tryLockExclusive(fd); + await send({ type: 'locked' }); + } +} finally { + await messages.return(); + if (fd !== undefined) closeSync(fd); + if (process.connected) process.disconnect(); +} + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => error ? reject(error) : resolve()); + }); +} diff --git a/native/system/test/fixtures/flock-io-child.js b/native/system/test/fixtures/flock-io-child.js new file mode 100644 index 0000000000..3c96377425 --- /dev/null +++ b/native/system/test/fixtures/flock-io-child.js @@ -0,0 +1,19 @@ +/** Ordinary file I/O from a process that never acquires a lock. */ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const request = once(process, 'message'); +await send({ type: 'ready' }); +const [command] = await request; +assert.equal(command, 'read-write'); +const previous = readFileSync(process.argv[2], 'utf8'); +writeFileSync(process.argv[2], 'written without acquiring a lock'); +await send({ type: 'written', previous }); +process.disconnect(); + +function send(message) { + return new Promise((resolve, reject) => { + process.send(message, (error) => error ? reject(error) : resolve()); + }); +} diff --git a/native/system/test/fixtures/flock-oracle.c b/native/system/test/fixtures/flock-oracle.c new file mode 100644 index 0000000000..ef1e9f6857 --- /dev/null +++ b/native/system/test/fixtures/flock-oracle.c @@ -0,0 +1,75 @@ +/* Independent system flock(2) oracle; stdin commands produce flushed JSON lines. */ +#include +#include +#include +#include +#include +#include + +static int reply(int fd, int operation) { + const int result = flock(fd, operation); + const int error = result == 0 ? 0 : errno; + if (printf("{\"errno\":%d}\n", error) < 0 || fflush(stdout) == EOF) { + perror("flock-oracle: stdout"); + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + int operation = LOCK_EX; + if (argc < 2 || argc > 3) { + fprintf(stderr, "usage: flock-oracle [exclusive|shared]\n"); + return 1; + } + if (argc == 3) { + if (strcmp(argv[2], "shared") == 0) { + operation = LOCK_SH; + } else if (strcmp(argv[2], "exclusive") != 0) { + fprintf(stderr, "flock-oracle: mode must be exclusive or shared\n"); + return 1; + } + } + + const int fd = open(argv[1], O_RDWR | O_CREAT, 0600); + if (fd == -1) { + perror("flock-oracle: open"); + return 1; + } + int status = 0; + if (puts("{\"ready\":true}") == EOF || fflush(stdout) == EOF) { + perror("flock-oracle: stdout"); + status = 1; + goto cleanup; + } + + char command[3]; + while (fgets(command, sizeof(command), stdin) != NULL) { + if (command[1] != '\n') { + fprintf(stderr, "flock-oracle: commands must be one letter followed by a newline\n"); + status = 1; + break; + } + if (command[0] == 'q') break; + if (command[0] != 't' && command[0] != 'u') { + fprintf(stderr, "flock-oracle: expected t, u, or q\n"); + status = 1; + break; + } + if (reply(fd, command[0] == 't' ? operation | LOCK_NB : LOCK_UN) != 0) { + status = 1; + break; + } + } + if (ferror(stdin)) { + perror("flock-oracle: stdin"); + status = 1; + } + +cleanup: + if (close(fd) == -1) { + perror("flock-oracle: close"); + status = 1; + } + return status; +} diff --git a/native/system/test/fixtures/flock-worker.js b/native/system/test/fixtures/flock-worker.js new file mode 100644 index 0000000000..28cc056023 --- /dev/null +++ b/native/system/test/fixtures/flock-worker.js @@ -0,0 +1,31 @@ +/** Hold work before or inside its callback while the parent terminates this environment. */ +import assert from 'node:assert/strict'; +import { createHook } from 'node:async_hooks'; +import { parentPort, workerData } from 'node:worker_threads'; +import { tryLockExclusive } from '../../packages/entry/lib/flock.js'; +import { loadFlockBinding } from './flock-binding.js'; + +let nativeWork = 0; +const hook = createHook({ + init(_id, type) { + if (type === 'flock') nativeWork++; + }, +}); +hook.enable(); +try { + if (workerData.phase === 'callback') { + loadFlockBinding().tryLock(workerData.fd, () => { + parentPort.postMessage({ type: 'callback', nativeWork }); + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); + }); + } else { + const pending = tryLockExclusive(workerData.fd); + assert.equal(nativeWork, 1); + parentPort.postMessage({ type: 'queued', nativeWork }); + // No JS yield precedes this wait, so the native completion cannot run first. + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); + await pending; + } +} finally { + hook.disable(); +} diff --git a/native/system/test/flock.test.js b/native/system/test/flock.test.js new file mode 100644 index 0000000000..16e66327e9 --- /dev/null +++ b/native/system/test/flock.test.js @@ -0,0 +1,423 @@ +/** Kernel behavior through the built flock entry; each case owns its files and processes. */ +import assert from 'node:assert/strict'; +import { fork, spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { constants, tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createInterface } from 'node:readline'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { Worker } from 'node:worker_threads'; +import { tryLockExclusive } from '../packages/entry/lib/flock.js'; +import { loadFlockBinding } from './fixtures/flock-binding.js'; + +const posix = process.platform === 'linux' || process.platform === 'darwin'; +const timeout = 120_000; +const nativeOnly = { timeout, skip: posix ? false : 'The flock addon requires Linux or macOS' }; + +function resources(t) { + const disposers = []; + // Match the repository's process-e2e budget for both cases and cleanup. + t.after(async () => { + // Cleanup must await close even when the case's signal is already aborted. + const signal = AbortSignal.timeout(timeout); + const errors = []; + for (const dispose of disposers.reverse()) { + try { + await dispose(signal); + } catch (error) { + errors.push(error); + } + } + if (errors.length) throw new AggregateError(errors, 'flock test cleanup failed'); + }, { timeout }); + // On local Linux filesystems, flock and OFD byte-range locks are independent; + // network filesystems can translate between them and hide a wrong syscall. + const root = mkdtempSync(join(tmpdir(), 'node-addon-system-flock-')); + disposers.push(() => rmSync(root, { recursive: true, force: true })); + return { + file: join(root, 'lock'), + defer: (dispose) => disposers.push(dispose), + open(name = 'lock') { + const fd = openSync(join(root, name), 'a+', 0o600); + let closed = false; + const close = () => { + if (!closed) { + closeSync(fd); + closed = true; + } + }; + disposers.push(close); + return { fd, close }; + }, + }; +} + +function flockError(error, codes) { + assert.ok(codes.includes(error.code), `Unexpected flock error: ${error.code}`); + assert.equal(error.errno, constants.errno[error.code]); + assert.ok(error.errno > 0); + assert.equal(error.syscall, 'flock'); + return true; +} + +const busy = (error) => flockError(error, ['EAGAIN', 'EWOULDBLOCK']); + +async function until(promise, signal) { + let abort; + const cancelled = new Promise((_, reject) => { + abort = () => reject(signal.reason); + if (signal.aborted) abort(); + else signal.addEventListener('abort', abort, { once: true }); + }); + try { + return await Promise.race([promise, cancelled]); + } finally { + signal.removeEventListener('abort', abort); + } +} + +function childEnvironment() { + return Object.fromEntries(Object.entries(process.env) + .filter(([name]) => !/KEY|SECRET|TOKEN|PASSWORD/i.test(name))); +} + +function observeChild(t, scope, child) { + let closed = false; + let stderr = ''; + let processError; + const done = new Promise((resolve) => child.once('close', (code, signal) => { + closed = true; + resolve({ code, signal, stderr, error: processError }); + })); + scope.defer(async (signal) => { + if (!closed) child.kill('SIGKILL'); + await until(done, signal); + }); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => { stderr += chunk; }); + child.on('error', (error) => { processError = error; }); + + async function response(emitter, event, send = () => Promise.resolve()) { + const waiting = new AbortController(); + const signal = AbortSignal.any([t.signal, waiting.signal]); + try { + const received = Promise.race([ + once(emitter, event, { signal }).then(([message]) => message), + done.then((result) => { + throw new Error(`flock fixture exited before replying: ${JSON.stringify(result)}`); + }), + ]); + const [message] = await until(Promise.all([received, send()]), signal); + return message; + } finally { + waiting.abort(); + } + } + + return { child, response, waitForExit: () => until(done, t.signal) }; +} + +async function childFixture(t, scope, fixture, args = [], { execArgv = [], inheritedFd } = {}) { + const stdio = ['ignore', 'ignore', 'pipe', 'ipc']; + if (inheritedFd !== undefined) stdio.push(inheritedFd); + const child = fork(new URL(`./fixtures/${fixture}`, import.meta.url), args, { + execArgv, + env: childEnvironment(), + stdio, + }); + const observed = observeChild(t, scope, child); + const exchange = (command) => observed.response(child, 'message', () => ( + command === undefined ? Promise.resolve() : new Promise((resolve, reject) => { + child.send(command, (error) => error ? reject(error) : resolve()); + }) + )); + assert.deepEqual(await exchange(), { type: 'ready' }); + return { child, waitForExit: observed.waitForExit, exchange }; +} + +async function oracleFixture(t, scope, mode) { + const binary = process.platform === 'linux' + ? `./bin/${process.report.getReport().header.glibcVersionRuntime ? 'glibc' : 'musl'}/flock-oracle` + : './bin/flock-oracle'; + const child = spawn(fileURLToPath(new URL(binary, import.meta.url)), [scope.file, mode], { + env: childEnvironment(), + stdio: ['pipe', 'pipe', 'pipe'], + }); + const observed = observeChild(t, scope, child); + const lines = createInterface({ input: child.stdout }); + child.once('close', () => lines.close()); + let inputError; + child.stdin.on('error', (error) => { inputError = error; }); + const send = (command) => until(new Promise((resolve, reject) => { + if (inputError) reject(inputError); + else child.stdin.write(`${command}\n`, (error) => error ? reject(error) : resolve()); + }), t.signal); + const exchange = async (command) => JSON.parse(await observed.response(lines, 'line', () => ( + command === undefined ? Promise.resolve() : send(command) + ))); + assert.deepEqual(await exchange(), { ready: true }); + return { + exchange, + async quit() { + await send('q'); + cleanExit(await observed.waitForExit()); + }, + }; +} + +function cleanExit(result) { + assert.equal(result.signal, null, result.stderr); + assert.equal(result.error, undefined); + assert.equal(result.code, 0, result.stderr); +} + +test('import succeeds with native addons disabled', { timeout }, async (t) => { + const scope = resources(t); + const child = await childFixture(t, scope, 'flock-import.js', [], { execArgv: ['--no-addons'] }); + cleanExit(await child.waitForExit()); +}); + +for (const platform of ['win32', 'freebsd']) { + test(`calling flock on ${platform} rejects without loading an addon`, { timeout }, async (t) => { + const scope = resources(t); + const child = await childFixture(t, scope, 'flock-import.js', [platform], { execArgv: ['--no-addons'] }); + cleanExit(await child.waitForExit()); + }); +} + +test('acquisition resolves asynchronously to void and the same fd can reacquire', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const result = tryLockExclusive(owner.fd); + assert.ok(result instanceof Promise); + assert.equal(await result, undefined); + assert.equal(await tryLockExclusive(owner.fd), undefined); +}); + +test('separate opens of one file contend', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + await assert.rejects(tryLockExclusive(contender.fd), busy); +}); + +test('different files can be locked concurrently', nativeOnly, async (t) => { + const scope = resources(t); + const first = scope.open('first'); + const second = scope.open('second'); + await Promise.all([tryLockExclusive(first.fd), tryLockExclusive(second.fd)]); +}); + +test('closing the locked fd allows an already-open contender to acquire', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + await assert.rejects(tryLockExclusive(contender.fd), busy); + owner.close(); + await tryLockExclusive(contender.fd); +}); + +test('closing another fd for the same file does not release the lock', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const other = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + other.close(); + await assert.rejects(tryLockExclusive(contender.fd), busy); + owner.close(); + await tryLockExclusive(contender.fd); +}); + +test('invalid fd rejects asynchronously with EBADF and positive errno', nativeOnly, async () => { + let result; + assert.doesNotThrow(() => { result = tryLockExclusive(-1); }); + assert.ok(result instanceof Promise); + await assert.rejects(result, (error) => flockError(error, ['EBADF'])); +}); + +test('native argument errors reject the JavaScript promise without throwing from the entry', nativeOnly, async () => { + let result; + assert.doesNotThrow(() => { result = tryLockExclusive(2 ** 31); }); + assert.ok(result instanceof Promise); + await assert.rejects(result, { name: 'RangeError', message: 'fd must be a signed C int' }); +}); + +test('native callbacks receive asynchronous, request-local success and errno results', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + const binding = loadFlockBinding(); + const results = await Promise.all([owner.fd, contender.fd, -1].map((fd) => new Promise((resolve) => { + let returned = false; + const result = binding.tryLock(fd, (errno) => { + assert.equal(returned, true); + resolve(errno); + }); + assert.equal(result, undefined); + returned = true; + }))); + assert.equal(results[0], 0); + assert.ok([constants.errno.EAGAIN, constants.errno.EWOULDBLOCK].includes(results[1])); + assert.equal(results[2], constants.errno.EBADF); +}); + +test('an exception in the native completion callback is reported as uncaught', nativeOnly, async (t) => { + const scope = resources(t); + const child = await childFixture(t, scope, 'flock-callback-throws.js'); + const exit = await child.waitForExit(); + assert.equal(exit.signal, null, exit.stderr); + assert.equal(exit.error, undefined); + assert.equal(exit.code, 1, exit.stderr); + assert.match(exit.stderr, /Error: flock callback failure/); +}); + +test('concurrent calls retain their own syscall errno', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + await Promise.all([ + assert.rejects(tryLockExclusive(contender.fd), busy), + assert.rejects(tryLockExclusive(-1), (error) => flockError(error, ['EBADF'])), + assert.rejects(tryLockExclusive(contender.fd), busy), + assert.rejects(tryLockExclusive(-1), (error) => flockError(error, ['EBADF'])), + ]); +}); + +test('two child processes exclude each other and normal close transfers ownership', nativeOnly, async (t) => { + const scope = resources(t); + const observer = scope.open(); + const children = await Promise.all([ + childFixture(t, scope, 'flock-child.js', [scope.file]), + childFixture(t, scope, 'flock-child.js', [scope.file]), + ]); + const results = await Promise.all(children.map((child) => child.exchange('tryLock'))); + assert.equal(results.filter((result) => result.type === 'locked').length, 1); + assert.equal(results.filter((result) => result.type === 'error').length, 1); + const winnerIndex = results.findIndex((result) => result.type === 'locked'); + const winner = children[winnerIndex]; + const loser = children[1 - winnerIndex]; + busy(results[1 - winnerIndex]); + await assert.rejects(tryLockExclusive(observer.fd), busy); + + assert.deepEqual(await winner.exchange('close'), { type: 'closed' }); + cleanExit(await winner.waitForExit()); + assert.deepEqual(await loser.exchange('tryLock'), { type: 'locked' }); + await assert.rejects(tryLockExclusive(observer.fd), busy); + assert.deepEqual(await loser.exchange('close'), { type: 'closed' }); + cleanExit(await loser.waitForExit()); + await tryLockExclusive(observer.fd); +}); + +test('SIGKILL releases a child lock after exit', nativeOnly, async (t) => { + const scope = resources(t); + const observer = scope.open(); + const owner = await childFixture(t, scope, 'flock-child.js', [scope.file]); + const contender = await childFixture(t, scope, 'flock-child.js', [scope.file]); + assert.deepEqual(await owner.exchange('tryLock'), { type: 'locked' }); + const rejected = await contender.exchange('tryLock'); + assert.equal(rejected.type, 'error'); + busy(rejected); + assert.equal(owner.child.kill('SIGKILL'), true); + const exit = await owner.waitForExit(); + assert.equal(exit.error, undefined); + assert.equal(exit.signal, 'SIGKILL'); + assert.equal(exit.code, null); + assert.deepEqual(await contender.exchange('tryLock'), { type: 'locked' }); + await assert.rejects(tryLockExclusive(observer.fd), busy); +}); + +for (const phase of ['queued', 'callback']) { + test(`worker termination during ${phase} drains native work without taking ownership of the fd`, nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + const worker = new Worker(new URL('./fixtures/flock-worker.js', import.meta.url), { + workerData: { fd: owner.fd, phase }, + execArgv: [], + }); + scope.defer((signal) => until(worker.terminate(), signal)); + const exited = once(worker, 'exit'); + const waiting = new AbortController(); + try { + const [message] = await Promise.race([ + once(worker, 'message', { signal: AbortSignal.any([t.signal, waiting.signal]) }), + exited.then(([code]) => { throw new Error(`flock worker exited before ${phase}: ${code}`); }), + ]); + assert.deepEqual(message, { type: phase, nativeWork: 1 }); + } finally { + waiting.abort(); + } + assert.equal(await until(worker.terminate(), t.signal), 1); + await until(exited, t.signal); + await tryLockExclusive(owner.fd); + await assert.rejects(tryLockExclusive(contender.fd), busy); + owner.close(); + await tryLockExclusive(contender.fd); + }); +} + +for (const mode of ['exclusive', 'shared']) { + test(`an addon exclusive lock blocks an independent C ${mode} flock until its fd closes`, nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + await tryLockExclusive(owner.fd); + const oracle = await oracleFixture(t, scope, mode); + const result = await oracle.exchange('t'); + assert.ok([constants.errno.EAGAIN, constants.errno.EWOULDBLOCK].includes(result.errno)); + owner.close(); + assert.deepEqual(await oracle.exchange('t'), { errno: 0 }); + await oracle.quit(); + }); + + test(`an independent C ${mode} flock blocks the addon until explicit unlock`, nativeOnly, async (t) => { + const scope = resources(t); + const contender = scope.open(); + const oracle = await oracleFixture(t, scope, mode); + assert.deepEqual(await oracle.exchange('t'), { errno: 0 }); + await assert.rejects(tryLockExclusive(contender.fd), busy); + assert.deepEqual(await oracle.exchange('u'), { errno: 0 }); + await tryLockExclusive(contender.fd); + await oracle.quit(); + }); +} + +test('an advisory exclusive lock permits another process to read and write without locking', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + writeFileSync(scope.file, 'written before locking'); + await tryLockExclusive(owner.fd); + await assert.rejects(tryLockExclusive(contender.fd), busy); + const child = await childFixture(t, scope, 'flock-io-child.js', [scope.file]); + assert.deepEqual(await child.exchange('read-write'), { + type: 'written', previous: 'written before locking', + }); + cleanExit(await child.waitForExit()); + assert.equal(readFileSync(scope.file, 'utf8'), 'written without acquiring a lock'); + await assert.rejects(tryLockExclusive(contender.fd), busy); +}); + +test('an inherited fd shares the lock after parent close until the child closes its last reference', nativeOnly, async (t) => { + const scope = resources(t); + const owner = scope.open(); + const contender = scope.open(); + await tryLockExclusive(owner.fd); + const child = await childFixture(t, scope, 'flock-inherited-child.js', [], { inheritedFd: owner.fd }); + assert.deepEqual(await child.exchange('tryLock'), { type: 'locked' }); + owner.close(); + await assert.rejects(tryLockExclusive(contender.fd), busy); + assert.deepEqual(await child.exchange('close'), { type: 'closed' }); + await tryLockExclusive(contender.fd); + // The child stays alive, so its close acknowledgement—not process exit—releases the lock. + assert.equal(child.child.exitCode, null); + assert.equal(child.child.signalCode, null); + assert.deepEqual(await child.exchange('quit'), { type: 'bye' }); + cleanExit(await child.waitForExit()); +}); diff --git a/native/landlock-run/test/launcher.test.js b/native/system/test/launcher.test.js similarity index 99% rename from native/landlock-run/test/launcher.test.js rename to native/system/test/launcher.test.js index a78501cd4a..01fbc8458e 100644 --- a/native/landlock-run/test/launcher.test.js +++ b/native/system/test/launcher.test.js @@ -22,7 +22,7 @@ import { grantArgs, launcherPath, probe, -} from '@deepseek-ai/node-addon-landlock-run'; +} from '@deepseek-ai/node-addon-system/landlock-run'; const FATAL_PREFIX = 'landlock-run: '; const PARTIAL_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'; diff --git a/native/system/test/link-platform.mjs b/native/system/test/link-platform.mjs new file mode 100644 index 0000000000..15c5aeacb4 --- /dev/null +++ b/native/system/test/link-platform.mjs @@ -0,0 +1,10 @@ +/** Link the downloaded platform artifact for dependency-free ABI tests. */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packages = fileURLToPath(new URL('../packages/', import.meta.url)); +const platform = `${process.platform}-${process.arch}`; +const parent = path.join(packages, 'entry/node_modules/@deepseek-ai'); +fs.mkdirSync(parent, { recursive: true }); +fs.symlinkSync(path.join(packages, platform), path.join(parent, `node-addon-system-${platform}`), 'junction'); diff --git a/native/system/test/package-matrix.test.js b/native/system/test/package-matrix.test.js new file mode 100644 index 0000000000..12ea4c3030 --- /dev/null +++ b/native/system/test/package-matrix.test.js @@ -0,0 +1,186 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'node:test'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { verifyPlatformBinaries } from '../scripts/repo.mjs'; + +test('the real Landlock subpath imports without platform packages or dlopen, while the root is unexported', { timeout: 120_000 }, (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'system-landlock-entry-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true }), { timeout: 120_000 }); + const entry = fileURLToPath(new URL('../packages/entry/', import.meta.url)); + const installed = path.join(dir, 'node_modules', '@deepseek-ai', 'node-addon-system'); + fs.mkdirSync(installed, { recursive: true }); + fs.copyFileSync(path.join(entry, 'package.json'), path.join(installed, 'package.json')); + // Only the real entry payload is present; no platform package or addon is copied. + fs.cpSync(path.join(entry, 'lib'), path.join(installed, 'lib'), { recursive: true }); + const manifest = JSON.parse(fs.readFileSync(path.join(installed, 'package.json'), 'utf8')); + assert.equal(manifest.main, undefined); + assert.equal(manifest.types, undefined); + + const result = spawnSync(process.execPath, ['--no-addons', '--input-type=module', '--eval', ` + import assert from 'node:assert/strict'; + import { createRequire } from 'node:module'; + const originalDlopen = process.dlopen; + let dlopenCalls = 0; + try { + process.dlopen = () => { + dlopenCalls++; + throw new Error('Landlock import attempted dlopen'); + }; + const api = await import('@deepseek-ai/node-addon-system/landlock-run'); + assert.equal(api.LAUNCHER_BIN, 'landlock-run'); + assert.deepEqual(api.grantArgs({}), []); + assert.equal(dlopenCalls, 0); + await assert.rejects(import('@deepseek-ai/node-addon-system'), { + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', + }); + assert.throws(() => createRequire(import.meta.url).resolve('@deepseek-ai/node-addon-system'), { + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED', + }); + } finally { + process.dlopen = originalDlopen; + } + `], { + cwd: dir, + encoding: 'utf8', + timeout: 120_000, + env: Object.fromEntries(Object.entries(process.env) + .filter(([key]) => !/KEY|TOKEN|SECRET|PASSWORD|^NODE_PATH$/i.test(key))), + }); + assert.equal(result.error, undefined); + assert.equal(result.signal, null, result.stderr); + assert.equal(result.status, 0, result.stderr); +}); + +// These minimal headers exercise format rejection, not executable behavior. +// flock.test.js and packed-install verification execute the real addon. +function fixture(t, { platform = 'linux', arch = 'x64', kind = 'node-api' } = {}) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'system-package-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const executable = kind === 'static-musl'; + const binary = executable + ? { tool: 'landlock-run', kind, path: 'bin/landlock-run' } + : { tool: 'flock', kind, napi: 8, ...(platform === 'linux' ? { libc: 'glibc' } : {}), path: 'bin/system.node' }; + const spec = { platform: `${platform}-${arch}`, binaries: [binary] }; + const manifest = { name: 'fixture', os: [platform], cpu: [arch] }; + const bytes = Buffer.alloc(256); + if (platform === 'linux') { + bytes.writeUInt32LE(0x464c457f, 0); + bytes[4] = 2; + bytes[5] = 1; + bytes.writeUInt16LE(executable ? 2 : 3, 16); + bytes.writeUInt16LE(arch === 'x64' ? 62 : 183, 18); + } else { + bytes.writeUInt32LE(0xfeedfacf, 0); + bytes.writeUInt32LE(arch === 'x64' ? 0x01000007 : 0x0100000c, 4); + bytes.writeUInt32LE(8, 12); + } + bytes.write('napi_register_module_v1\0node_api_module_get_api_version_v1', 64); + const file = path.join(dir, binary.path); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, bytes, { mode: executable ? 0o755 : 0o644 }); + const save = () => { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(manifest)); + fs.writeFileSync(path.join(dir, 'prebuilds.json'), JSON.stringify(spec)); + }; + save(); + return { dir, file, bytes, binary, spec, manifest, save }; +} + +for (const platform of ['linux', 'darwin']) { + for (const arch of ['x64', 'arm64']) { + test(`accepts ${platform}-${arch} addon metadata and header`, (t) => { + assert.equal(verifyPlatformBinaries(fixture(t, { platform, arch }).dir).count, 1); + }); + } +} + +test('accepts the Linux static launcher format', (t) => { + assert.equal(verifyPlatformBinaries(fixture(t, { kind: 'static-musl' }).dir).count, 1); +}); + +for (const [name, change, expected] of [ + ['unknown platform', (f) => { f.manifest.os = ['win32']; }, /os\/cpu/], + ['mismatched platform', (f) => { f.spec.platform = 'linux-arm64'; }, /disagrees/], + ['path outside bin', (f) => { f.binary.path = '../system.node'; }, /inside bin/], + ['duplicate binary', (f) => { f.spec.binaries.push({ ...f.binary }); }, /duplicate/], + ['unknown kind', (f) => { f.binary.kind = 'unknown'; }, /kind\/tool\/NAPI/], + ['wrong NAPI version', (f) => { f.binary.napi = 9; }, /kind\/tool\/NAPI/], + ['missing Linux libc', (f) => { delete f.binary.libc; }, /declare glibc or musl/], + ['missing payload', (f) => { fs.unlinkSync(f.file); }, /missing/], + ['wrong ELF architecture', (f) => { f.bytes.writeUInt16LE(183, 18); fs.writeFileSync(f.file, f.bytes); }, /ELF architecture/], + ['wrong ELF type', (f) => { f.bytes.writeUInt16LE(2, 16); fs.writeFileSync(f.file, f.bytes); }, /ELF file type/], + ['truncated ELF', (f) => { fs.writeFileSync(f.file, Buffer.alloc(8)); }, /ELF64/], + ['missing NAPI exports', (f) => { f.bytes.fill(0, 64); fs.writeFileSync(f.file, f.bytes); }, /Node-API entry points/], + ['undeclared nested file', (f) => { fs.mkdirSync(path.join(f.dir, 'bin/extra')); fs.writeFileSync(path.join(f.dir, 'bin/extra/other.node'), 'x'); }, /undeclared/], +]) { + test(`rejects ${name}`, (t) => { + const f = fixture(t); + change(f); + f.save(); + assert.throws(() => verifyPlatformBinaries(f.dir), expected); + }); +} + +test('rejects Linux libc metadata on macOS', (t) => { + const f = fixture(t, { platform: 'darwin' }); + f.binary.libc = 'musl'; + f.save(); + assert.throws(() => verifyPlatformBinaries(f.dir), /must not declare/); +}); + +for (const [offset, value] of [[0, 0], [4, 0], [12, 2]]) { + test(`rejects invalid Mach-O field at ${offset}`, (t) => { + const f = fixture(t, { platform: 'darwin' }); + f.bytes.writeUInt32LE(value, offset); + fs.writeFileSync(f.file, f.bytes); + assert.throws(() => verifyPlatformBinaries(f.dir), /Mach-O/); + }); +} + +test('rejects a launcher whose executable bit was lost', { skip: process.platform === 'win32' }, (t) => { + const f = fixture(t, { kind: 'static-musl' }); + fs.chmodSync(f.file, 0o644); + assert.throws(() => verifyPlatformBinaries(f.dir), /not executable/); +}); + +test('rejects a symbolic-link payload', { skip: process.platform === 'win32' }, (t) => { + const f = fixture(t); + fs.renameSync(f.file, f.file + '.target'); + fs.symlinkSync(f.file + '.target', f.file); + assert.throws(() => verifyPlatformBinaries(f.dir), /not a regular file/); +}); + +test('entry prepack rejects a missing exported flock file even when the Landlock entry exists', (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'system-entry-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + fs.mkdirSync(path.join(dir, 'lib')); + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'entry-fixture', + exports: { + './landlock-run': { types: './lib/index.d.ts', default: './lib/index.js' }, + './flock': { types: './lib/flock.d.ts', default: './lib/flock.js' }, + }, + })); + for (const file of ['index.js', 'index.d.ts', 'flock.d.ts']) fs.writeFileSync(path.join(dir, 'lib', file), ''); + const script = fileURLToPath(new URL('../scripts/verify-entry-lib.mjs', import.meta.url)); + const options = { + cwd: dir, + encoding: 'utf8', + timeout: 120_000, + env: Object.fromEntries(Object.entries(process.env).filter(([key]) => !/KEY|TOKEN|SECRET|PASSWORD/i.test(key))), + }; + const missing = spawnSync(process.execPath, [script], options); + assert.equal(missing.error, undefined); + assert.equal(missing.signal, null); + assert.equal(missing.status, 1); + assert.match(missing.stderr, /lib\/flock\.js/); + fs.writeFileSync(path.join(dir, 'lib/flock.js'), ''); + const complete = spawnSync(process.execPath, [script], options); + assert.equal(complete.error, undefined); + assert.equal(complete.signal, null); + assert.equal(complete.status, 0, complete.stderr); +}); diff --git a/native/landlock-run/tsconfig.base.json b/native/system/tsconfig.base.json similarity index 100% rename from native/landlock-run/tsconfig.base.json rename to native/system/tsconfig.base.json diff --git a/native/landlock-run/tsconfig.json b/native/system/tsconfig.json similarity index 100% rename from native/landlock-run/tsconfig.json rename to native/system/tsconfig.json diff --git a/package.json b/package.json index 1c4c8f6f86..820a6764d3 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "workspaces": [ "vendor/*", "packages/*/*", - "native/landlock-run", - "native/landlock-run/packages/*", + "native/system", + "native/system/packages/*", "apps/*", "website" ], "scripts": { "build": "tsx scripts/build.ts", - "build:bench": "npm run build:lib && tsdown --config benchmarks/tsdown.config.ts", + "build:bench": "npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts", "build:official": "tsx scripts/build.ts --profile official", "build:lib": "pnpm run build:lib:host && pnpm run build:lib:client", "build:lib:host": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host", @@ -48,8 +48,9 @@ "lint:fix": "npm run build:lib:host && npm run lint:fix:contracts-ready", "lint:fix:contracts-ready": "tsx scripts/run-oxlint.ts --config .oxlintrc.staged.json packages/typert/generator/tests/fixtures/type-model --fix && tsx scripts/run-oxlint.ts . --fix", "duplication": "jscpd --config .jscpd.json packages scripts", - "test": "vitest run", - "test:coverage": "vitest run --coverage", + "test": "pnpm run build:native-system && vitest run", + "test:coverage": "pnpm run build:native-system && vitest run --coverage", + "build:native-system": "tsx native/system/scripts/build.ts --host-addon-only", "test:coverage:partitioned": "tsx scripts/run-coverage-partitions.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts", "test:bench": "npm run build:bench && npm run build:web && npm run test:bench:built", diff --git a/packages/experimental/webworker-packer/README.i18n.yaml b/packages/experimental/webworker-packer/README.i18n.yaml index 597b602ea3..2f08893658 100644 --- a/packages/experimental/webworker-packer/README.i18n.yaml +++ b/packages/experimental/webworker-packer/README.i18n.yaml @@ -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 packages/experimental/webworker-packer/README.md -README.md: 598a175637a6111001b07b0186c0f1a465d54a9c -README.zh.md: 8f05e39d34bd33613065cf86fd9c6e51ff90d88e +README.md: 4cf8b569ef288905ea555e4206553b5aeada1708 +README.zh.md: 712118f24bb686a3fb61b6a272471a45817928ae diff --git a/packages/experimental/webworker-packer/README.md b/packages/experimental/webworker-packer/README.md index 598a175637..4cf8b569ef 100644 --- a/packages/experimental/webworker-packer/README.md +++ b/packages/experimental/webworker-packer/README.md @@ -31,7 +31,7 @@ The pack is a three-layer standard stack: 2. **Publish view** — each workspace or vendored package contributes its built npm slice (`files` through picomatch) without source or workspace `dist/`. External packages retain published JavaScript under both `src/` and `dist/` because their `main` or `exports` may point there; only generic test, map, declaration, and archive exclusions apply. 3. **Reachability sweep** — the runtime loader's own resolution walks from every workspace export face plus the worker assembly's seeds (`IMAGE_ENTRY_SEEDS`), lowering each reached module to the wrapper contract at pack time. The transform reports statically named imports, re-exports, and dynamic imports; calls through `require`; and module-scope direct calls of the form `createRequire(import.meta.url)('pkg')` through a named import from `node:module` or `module`, including an import alias. Page assets (`lib/client.js` behind `./client` exports) ship verbatim; an unresolvable request from our own code fails the pack, third-party ones are tolerated to fail loud at require time. -`repository.ts` owns the repo-shaped inputs (workspace scan of `vendor/`, `packages/`, `native/landlock-run/packages/`, and `apps/`; profile composition through the real CLI dump path); `pack.ts` owns none of them, so the same library packs a different tree by being called differently. The native scan makes the Landlock entry package an ordinary published-view dependency while its executable remains a Worker platform implementation. The CLI is `dsh-pack-vfs-image --out [--profile web]`; `apps/web`'s `build:preview` runs it after the preview shell build. +`repository.ts` owns the repo-shaped inputs (workspace scan of `vendor/`, `packages/`, `native/system/packages/`, and `apps/`; profile composition through the real CLI dump path); `pack.ts` owns none of them, so the same library packs a different tree by being called differently. The native scan makes the Landlock entry package an ordinary published-view dependency while its executable remains a Worker platform implementation. The CLI is `dsh-pack-vfs-image --out [--profile web]`; `apps/web`'s `build:preview` runs it after the preview shell build. The repository adapter also declares the preview-only fixture trees under `webworker-runtime/tests/fixtures/`. The CLI packs each named fixture into a separate deterministic overlay archive plus a browser-readable manifest. Overlay files bypass npm publish-view and module-reachability exclusions, so dot directories and example source files remain intact; their mounts are limited to `home/` and `workspace/`. `pack.ts` treats them as opaque bytes, and Session and Workspace interpretation stays in the runtime packages that own those formats. diff --git a/packages/experimental/webworker-packer/README.zh.md b/packages/experimental/webworker-packer/README.zh.md index 8f05e39d34..712118f24b 100644 --- a/packages/experimental/webworker-packer/README.zh.md +++ b/packages/experimental/webworker-packer/README.zh.md @@ -31,7 +31,7 @@ VFS 镜像打包器:把一份合成 profile 变成浏览器 worker 挂载为 2. **发布视图**——每个 workspace 或 vendored 包贡献其构建后的 npm 切片(`files` 走 picomatch),不带源码和 workspace `dist/`。外部包的 `main` 或 `exports` 可能指向 `src/` 或 `dist/`,因此两处发布 JavaScript 都会保留,只应用通用的测试、map、声明与归档排除规则。 3. **可达性 sweep**——用运行时加载器自己的解析,从全部 workspace 导出面加 worker 装配种子(`IMAGE_ENTRY_SEEDS`)出发,pack 时把每个可达模块降低到包装契约。Transform 会报告具名静态 import、re-export 与动态 import、经 `require` 发起的调用,以及通过 `node:module` 或 `module` 具名导入(含导入别名)在模块作用域直接发起的 `createRequire(import.meta.url)('pkg')` 调用。页面资产(`./client` 导出背后的 `lib/client.js`)原样直发;自家代码的不可解析请求打包即失败,第三方的容忍到 require 时 fail loud。 -`repository.ts` 拥有仓库形态输入(`vendor/`、`packages/`、`native/landlock-run/packages/` 与 `apps/` 的 workspace 扫描;经真 CLI dump 路径合成 profile);`pack.ts` 一概不拥有,同一库换参即可打另一棵树。Native 扫描使 Landlock 入口包成为普通发布视图依赖,其可执行文件仍由 Worker 平台实现。CLI 为 `dsh-pack-vfs-image --out [--profile web]`;`apps/web` 的 `build:preview` 在预览壳构建后运行它。 +`repository.ts` 拥有仓库形态输入(`vendor/`、`packages/`、`native/system/packages/` 与 `apps/` 的 workspace 扫描;经真 CLI dump 路径合成 profile);`pack.ts` 一概不拥有,同一库换参即可打另一棵树。Native 扫描使 Landlock 入口包成为普通发布视图依赖,其可执行文件仍由 Worker 平台实现。CLI 为 `dsh-pack-vfs-image --out [--profile web]`;`apps/web` 的 `build:preview` 在预览壳构建后运行它。 仓库适配层还声明 `webworker-runtime/tests/fixtures/` 下仅用于 preview 的 fixture tree。CLI 会把每套具名 fixture 打成一份独立的确定性 overlay 归档,并写出浏览器可读的 manifest。Overlay 文件绕过 NPM 发布视图和模块可达性排除规则,因此点目录与示例源码会完整保留;其挂载位置仅限 `home/` 与 `workspace/`。`pack.ts` 把它们视为不透明字节;Session 与 Workspace 的解释仍归拥有这些格式的 runtime 包。 diff --git a/packages/experimental/webworker-packer/src/repository.ts b/packages/experimental/webworker-packer/src/repository.ts index 535eb6f78a..0d23c38957 100644 --- a/packages/experimental/webworker-packer/src/repository.ts +++ b/packages/experimental/webworker-packer/src/repository.ts @@ -21,7 +21,7 @@ import type { ConfigTree, ImageTree, PackResult } from './pack.ts' * package family contributes its unchanged JavaScript entry from `native/`; * examples and python never occur on a roster's dependency chain. */ -const WORKSPACE_SCAN_ROOTS = ['vendor', 'packages', 'native/landlock-run/packages', 'apps'] +const WORKSPACE_SCAN_ROOTS = ['vendor', 'packages', 'native/system/packages', 'apps'] /** Composition entry point package: the `dsh` CLI, run from source. */ const CLI_PACKAGE = 'apps/cli' diff --git a/packages/experimental/webworker-packer/tests/image-loadable.spec.ts b/packages/experimental/webworker-packer/tests/image-loadable.spec.ts index 886707ef73..453b6b3e9c 100644 --- a/packages/experimental/webworker-packer/tests/image-loadable.spec.ts +++ b/packages/experimental/webworker-packer/tests/image-loadable.spec.ts @@ -35,7 +35,8 @@ const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url)) /** A leaf workspace package: real build output, no dependencies to drag in. */ const SUBJECT = '@deepseek-ai/dsh-timeout' -const LANDLOCK = '@deepseek-ai/node-addon-landlock-run' +const LANDLOCK = '@deepseek-ai/node-addon-system' +const LANDLOCK_ENTRY = `${LANDLOCK}/landlock-run` const PLUGIN_INVENTORY = '@deepseek-ai/dsh-plugin-package-inventory-deepseek' const WEB_SERVER = '@deepseek-ai/dsh-host-webserver' @@ -86,7 +87,7 @@ const subjectBuilt = [ 'vendor/loader/lib/index.js', 'packages/host/webserver/lib/index.js', 'packages/llm/plugin-package-inventory-deepseek/lib/index.js', - 'native/landlock-run/packages/entry/lib/index.js', + 'native/system/packages/entry/lib/index.js', 'packages/preset/agent-presets/lib/typert.host.js', 'packages/preset/agent-presets/lib/typert.remote-client.js', ].every(path => existsSync(join(repoRoot, path))) @@ -240,7 +241,7 @@ const archive = async (): Promise => expect(result.packages.has(LANDLOCK)).toBe(true) expect(result.missing).toEqual([]) expect(Object.hasOwn(result.files, `node_modules/${LANDLOCK}/lib/index.js`)).toBe(true) - expect(createNodeBuiltins()[LANDLOCK]).toBeUndefined() + expect(createNodeBuiltins()[LANDLOCK_ENTRY]).toBeUndefined() const vfs = loadVfsImage(await inflateImage(result.image, 'the packed Landlock package'), DEFAULT_ROOT) const loader = new WorkerModuleLoader({ @@ -251,7 +252,7 @@ const archive = async (): Promise => }) setActiveVfs(vfs) setActiveModuleLoader(loader) - const landlock = loader.requireFrom(`${DEFAULT_ROOT}/workspace`)(LANDLOCK) as { + const landlock = loader.requireFrom(`${DEFAULT_ROOT}/workspace`)(LANDLOCK_ENTRY) as { LAUNCHER_BIN: string LAUNCHER_FAILURE_EXIT: number launcherPath(): string diff --git a/packages/experimental/webworker-runtime/README.i18n.yaml b/packages/experimental/webworker-runtime/README.i18n.yaml index d6487a8f4b..e4d516667d 100644 --- a/packages/experimental/webworker-runtime/README.i18n.yaml +++ b/packages/experimental/webworker-runtime/README.i18n.yaml @@ -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 packages/experimental/webworker-runtime/README.md -README.md: 1a3bc30e259ac352f1c8e4485be7851ab0035812 -README.zh.md: c934afc6a613f8bd9cd106bb52ac830d30278039 +README.md: b9454e308e92ba78e3d11aa72dfd3723e8c07568 +README.zh.md: 4d39f145d2ce2c826906ea1e60a26488eab9467e diff --git a/packages/experimental/webworker-runtime/README.md b/packages/experimental/webworker-runtime/README.md index 1a3bc30e25..b9454e308e 100644 --- a/packages/experimental/webworker-runtime/README.md +++ b/packages/experimental/webworker-runtime/README.md @@ -50,7 +50,7 @@ None; this package neither assembles nor sends a provider request. - **The worker composition writes plaintext session logs** (`compression: 'none'` boot patch): it carries no Zstandard codec, so exported logs are `.jsonl`, never `.jsonl.zstd`. - **`node:dns/promises`, `node:vm`, `node:net`, `node:sqlite`, `node:worker_threads` are structural stubs**: every call reports its refusal on the console and throws. Rows needing native DNS, a real process, or realm isolation cannot run here. - **Filesystem watchers observe only the mounted VFS**: image seeding is silent and the VFS has no symlinks or external writers. `persistent`, `ref()`, and `unref()` preserve the Node API but cannot control a dedicated Worker's lifetime because browsers expose no ref-counted event loop. -- **Worker confinement is a VFS boundary, not kernel Landlock**: `read-only` and `workspace-write` run the unchanged `@deepseek-ai/node-addon-landlock-run` JavaScript and launcher argv, but the process layer implements the logical `landlock-run` executable and enforces its grants on every shell filesystem request. `full` therefore covers the Worker command table and mounted VFS only; it does not claim arbitrary native-process execution or Linux kernel isolation. +- **Worker confinement is a VFS boundary, not kernel Landlock**: `read-only` and `workspace-write` run the unchanged `@deepseek-ai/node-addon-system/landlock-run` JavaScript and launcher argv, but the process layer implements the logical `landlock-run` executable and enforces its grants on every shell filesystem request. `full` therefore covers the Worker command table and mounted VFS only; it does not claim arbitrary native-process execution or Linux kernel isolation. - **The worker bundle pins a path inside `@yarnpkg/parsers`** — the build resolves the package's own `lib/shell.js` instead of its root, whose barrel also re-exports the Syml parser and so drags js-yaml into a bundle that never parses that format (around 175 kB, plus its module body at worker start). The path is derived from the package manifest, so a layout change fails the build rather than reinstating the barrel; upgrading the dependency means re-checking that the shell parser still lives there. - **The shell is not bash**: no loops, functions, `case`, job control, or process substitution — the grammar stops at pipelines, `&&`/`||`, subshells, groups, redirections, and expansion. `&` runs its command to completion in place, `sed` accepts only substitution scripts, patterns are JavaScript regular expressions, and the command table holds coreutils only (no `git`, no network tools). - **A shell process has no synchronous filesystem**: it reads and writes the host's VFS by message, because blocking on a reply would need `SharedArrayBuffer`, which requires a cross-origin isolation GitHub Pages cannot grant. Directory-walking commands therefore cost one round trip per entry, and two concurrent commands can interleave their writes. diff --git a/packages/experimental/webworker-runtime/README.zh.md b/packages/experimental/webworker-runtime/README.zh.md index c934afc6a6..4d39f145d2 100644 --- a/packages/experimental/webworker-runtime/README.zh.md +++ b/packages/experimental/webworker-runtime/README.zh.md @@ -50,7 +50,7 @@ kind: "package-library" - **worker 组合写明文会话日志**(`compression: 'none'` boot patch):不带 Zstandard 编解码器,导出日志是 `.jsonl`,不会是 `.jsonl.zstd`。 - **`node:dns/promises`、`node:vm`、`node:net`、`node:sqlite`、`node:worker_threads` 是结构化 stub**:每次调用在 console 报告拒绝并抛出。需要原生 DNS、真进程或真 realm 隔离的行在此无法运行。 - **文件 watcher 只能观察已挂载的 VFS**:镜像 seed 不产生事件,VFS 也没有符号链接或外部写入方。`persistent`、`ref()` 和 `unref()` 保留 Node API,但浏览器没有引用计数事件循环,因此这些接口不能控制 dedicated Worker 的生存期。 -- **Worker confinement 是 VFS 边界,不是内核 Landlock**:`read-only` 和 `workspace-write` 运行未经修改的 `@deepseek-ai/node-addon-landlock-run` JavaScript 与 launcher argv,进程层则实现逻辑 `landlock-run` 可执行文件,并在 shell 的每次文件系统请求上执行其授权。`full` 仅覆盖 Worker 命令表和已挂载 VFS,不表示能够执行任意 native 进程,也不表示 Linux 内核隔离。 +- **Worker confinement 是 VFS 边界,不是内核 Landlock**:`read-only` 和 `workspace-write` 运行未经修改的 `@deepseek-ai/node-addon-system/landlock-run` JavaScript 与 launcher argv,进程层则实现逻辑 `landlock-run` 可执行文件,并在 shell 的每次文件系统请求上执行其授权。`full` 仅覆盖 Worker 命令表和已挂载 VFS,不表示能够执行任意 native 进程,也不表示 Linux 内核隔离。 - **worker 束钉住了 `@yarnpkg/parsers` 的包内路径**——构建解析到该包自己的 `lib/shell.js` 而非包根,因为包根 barrel 还 re-export 了 Syml 解析器,会把 js-yaml 拖进一个从不解析该格式的束(约 175 kB,外加 worker 启动时的模块体求值)。该路径由包 manifest 派生,包内布局一变即构建期失败、不会静默退回 barrel;升级这个依赖时须复核 shell 解析器是否仍在那里。 - **这个 shell 不是 bash**:没有循环、函数、`case`、作业控制或进程替换——语法止步于管道、`&&`/`||`、子 shell、group、重定向与展开。`&` 会就地把命令跑完,`sed` 只接受替换脚本,模式是 JavaScript 正则,命令表只有 coreutils(没有 `git`,没有网络工具)。 - **shell 进程没有同步文件面**:它靠消息读写宿主的 VFS,因为阻塞等待回帧需要 `SharedArrayBuffer`,而那要求 GitHub Pages 给不了的跨源隔离。因此目录遍历类命令每个条目一次往返,并发的两条命令写入可以交错。 diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index fa34ea105c..bf82846901 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -57,7 +57,7 @@ "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subprocess-local": "workspace:^", - "@deepseek-ai/node-addon-landlock-run": "workspace:^", + "@deepseek-ai/node-addon-system": "workspace:^", "@types/picomatch": "^3.0.2", "@types/readable-stream": "^4.0.24", "chokidar": "^5.0.0" diff --git a/packages/experimental/webworker-runtime/src/module-proxies.ts b/packages/experimental/webworker-runtime/src/module-proxies.ts index 8fef2b62b6..49f6fcb250 100644 --- a/packages/experimental/webworker-runtime/src/module-proxies.ts +++ b/packages/experimental/webworker-runtime/src/module-proxies.ts @@ -1,7 +1,7 @@ /** * The worker bundle's module proxy table: the ONLY platform fork of the host - * tree. Every entry replaces a Node builtin or an external npm package; - * workspace and vendored modules are always mounted as they ship. + * tree. Entries replace Node builtins, external npm packages, and the native + * flock subpath; other workspace and vendored modules are mounted as they ship. * * The build turns these into bundler aliases, and `node/builtins.ts` turns the * same modules into the loader's static table — one list, two consumers. @@ -16,9 +16,9 @@ */ /** - * Module proxy table — the ONLY platform fork of the worker host. Every entry - * replaces a Node builtin or an external npm package; workspace and vendored - * modules are always mounted as-is. Keys are exact module specifiers. + * Module proxy table — the ONLY platform fork of the worker host. Keys are + * exact module specifiers; the native system package's Landlock entry stays + * unmodified while its flock subpath is replaced. */ export const MODULE_PROXIES: Record = { // VFS-backed real implementations. @@ -65,7 +65,7 @@ export const MODULE_PROXIES: Record = { 'node:worker_threads': './node/builtin_modules/mock/worker_threads.ts', 'node:sqlite': './node/builtin_modules/mock/sqlite.ts', // External npm replacements, named after the package each stands in for. - 'fs-ext': './node/external_packages/fs-ext.ts', + '@deepseek-ai/node-addon-system/flock': './node/external_packages/node-addon-system-flock.ts', 'koffi': './node/external_packages/koffi.ts', 'sharp': './node/external_packages/sharp.ts', 'node-pty': './node/external_packages/node-pty.ts', diff --git a/packages/experimental/webworker-runtime/src/node/builtins.ts b/packages/experimental/webworker-runtime/src/node/builtins.ts index ac44024aec..7a7d077e60 100644 --- a/packages/experimental/webworker-runtime/src/node/builtins.ts +++ b/packages/experimental/webworker-runtime/src/node/builtins.ts @@ -45,7 +45,7 @@ import * as nodeNet from './builtin_modules/mock/net.ts' import * as nodeSqlite from './builtin_modules/mock/sqlite.ts' import * as nodeVm from './builtin_modules/mock/vm.ts' import * as nodeWorkerThreads from './builtin_modules/mock/worker_threads.ts' -import * as fsExt from './external_packages/fs-ext.ts' +import * as systemFlock from './external_packages/node-addon-system-flock.ts' import * as koffi from './external_packages/koffi.ts' import * as nodePty from './external_packages/node-pty.ts' import * as piAi from './external_packages/pi-ai.ts' @@ -84,9 +84,9 @@ const BUILTINS: Record = { zlib: () => nodeZlib, } -/** External npm packages replaced wholesale (structural not-implemented stubs and fakes). */ +/** Exact package or subpath specifiers served by worker stubs and fakes. */ const EXTERNALS: Record = { - 'fs-ext': () => fsExt, + '@deepseek-ai/node-addon-system/flock': () => systemFlock, 'koffi': () => koffi, 'sharp': () => sharp, 'node-pty': () => nodePty, diff --git a/packages/experimental/webworker-runtime/src/node/external_packages/fs-ext.ts b/packages/experimental/webworker-runtime/src/node/external_packages/fs-ext.ts deleted file mode 100644 index 049aa5c2b5..0000000000 --- a/packages/experimental/webworker-runtime/src/node/external_packages/fs-ext.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * `fs-ext` stub: the kernel file-lock bridge the JSONL session backend uses - * for cross-process write exclusion. The worker is a single-process - * deployment whose in-process write claim already excludes every writer, so - * both flock faces succeed immediately; every other entry is loud because - * nothing in the worker reaches it. - */ -import { notImplementedFail } from '../notImplementedFail.ts' - -const MODULE = 'fs-ext' - -/** - * Asynchronous flock face; the single-process worker grants every lock. - * @param _fd - file descriptor (unused). - * @param _flags - lock flags (unused). - * @param callback - completion callback, invoked with no error. - */ -export function flock(_fd: number, _flags: unknown, callback: (error: null) => void): void { - queueMicrotask(() => { callback(null) }) -} - -/** - * Synchronous flock face; the single-process worker grants every lock. - */ -export function flockSync(): void {} - -/** Unreached in the worker; loud refusal. */ -export const fcntl = notImplementedFail(MODULE, 'fcntl') -/** Unreached in the worker; loud refusal. */ -export const fcntlSync = notImplementedFail(MODULE, 'fcntlSync') -/** Unreached in the worker; loud refusal. */ -export const seek = notImplementedFail(MODULE, 'seek') -/** Unreached in the worker; loud refusal. */ -export const seekSync = notImplementedFail(MODULE, 'seekSync') -/** Unreached in the worker; loud refusal. */ -export const statVFS = notImplementedFail(MODULE, 'statVFS') - -/** CommonJS interop marker: the worker loader hands `default` to default imports (see ./builtins.ts). */ -export const __esModule = true - -/** The fs-ext face its consumers read. */ -export default { flock, flockSync, fcntl, fcntlSync, seek, seekSync, statVFS } diff --git a/packages/experimental/webworker-runtime/src/node/external_packages/node-addon-system-flock.ts b/packages/experimental/webworker-runtime/src/node/external_packages/node-addon-system-flock.ts new file mode 100644 index 0000000000..2e5d91d3c9 --- /dev/null +++ b/packages/experimental/webworker-runtime/src/node/external_packages/node-addon-system-flock.ts @@ -0,0 +1,20 @@ +/** + * Single-process worker replacement for `@deepseek-ai/node-addon-system/flock`. + * The JSONL backend's in-process write claim already excludes every writer, + * so its kernel-lock request succeeds without acquiring another resource. + */ + +/** + * Grant the single-process worker's exclusive-lock request immediately. + * @param _fd - file descriptor (unused). + * @returns an already-fulfilled promise. + */ +export function tryLockExclusive(_fd: number): Promise { + return Promise.resolve() +} + +/** CommonJS interop marker for the worker module loader. */ +export const __esModule = true + +/** The native flock API used by the JSONL backend. */ +export default { tryLockExclusive } diff --git a/packages/experimental/webworker-runtime/src/node/external_packages/replaced-externals.ts b/packages/experimental/webworker-runtime/src/node/external_packages/replaced-externals.ts index 453e9864f6..ecc16421b4 100644 --- a/packages/experimental/webworker-runtime/src/node/external_packages/replaced-externals.ts +++ b/packages/experimental/webworker-runtime/src/node/external_packages/replaced-externals.ts @@ -1,16 +1,15 @@ /** - * Names of external npm packages the worker replaces wholesale. Kept in a module - * with no imports so both consumers can read it: the runtime builtin table - * (`./builtins.ts`) and the build-time VFS image collector, which must leave - * these packages out of the image entirely — the loader answers them from the - * bundle before it ever reaches `node_modules`. + * Exact package or subpath specifiers served from the worker bundle. Kept + * import-free for the runtime builtin table and the VFS image collector. + * Whole-package entries are omitted from the image; subpath entries leave + * their parent package available to other consumers. */ -/** External packages served from the worker bundle instead of the VFS. */ +/** Package or subpath specifiers served from the worker bundle instead of the VFS. */ export const REPLACED_EXTERNAL_PACKAGES: readonly string[] = [ '@earendil-works/pi-ai', '@vscode/ripgrep', - 'fs-ext', + '@deepseek-ai/node-addon-system/flock', 'koffi', 'node-pty', 'sharp', diff --git a/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts b/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts index a1e6fd3e9c..e105a94f34 100644 --- a/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts @@ -18,7 +18,7 @@ import { setActiveVfs } from '@deepseek-ai/dsh-experimental-webworker-runtime/sr import { spawn, spawnSync } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/node/builtin_modules/implemented/child_process.ts' import { LAUNCHER_FAILURE_EXIT, grantArgs, launcherPath, probe, -} from '@deepseek-ai/node-addon-landlock-run' +} from '@deepseek-ai/node-addon-system/landlock-run' import { processAlive, signalProcess } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/node/process-table.ts' import { hostFileSystem } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/shell/fs-access.ts' import { diff --git a/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts b/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts index ea4f1d02a8..dbd3de5193 100644 --- a/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/node-stubs.spec.ts @@ -118,7 +118,8 @@ describe('constructible-but-inert fakes', () => { describe('replaced external packages', () => { it('lists the packages the loader serves from the bundle', () => { expect(REPLACED_EXTERNAL_PACKAGES).not.toContain('chokidar') - expect(REPLACED_EXTERNAL_PACKAGES).not.toContain('@deepseek-ai/node-addon-landlock-run') + expect(REPLACED_EXTERNAL_PACKAGES).not.toContain('@deepseek-ai/node-addon-system') + expect(REPLACED_EXTERNAL_PACKAGES).not.toContain('@deepseek-ai/node-addon-system/landlock-run') expect(REPLACED_EXTERNAL_PACKAGES).toContain('ws') }) diff --git a/packages/lsp/lsp-stdio/tests/fixture-server.ts b/packages/lsp/lsp-stdio/tests/fixture-server.ts index fc4491507f..f81402b63b 100644 --- a/packages/lsp/lsp-stdio/tests/fixture-server.ts +++ b/packages/lsp/lsp-stdio/tests/fixture-server.ts @@ -13,7 +13,7 @@ * - LSP_FAKE_EXIT_AFTER_REPLY: "1" exits the process right after answering a textDocument/* request, * simulating a server that dies while idle so the pool holds a dead instance (eviction test). * - LSP_FAKE_OPEN_MARKER: appends each didOpen document text as one JSON line to this path. - * - LSP_FAKE_INITIALIZED_MARKER: records when the initialized notification is received. + * - LSP_FAKE_INITIALIZED_MARKER: records initialized receipt after any requested stdin pause. * - LSP_FAKE_PAUSE_STDIN_AFTER_INITIALIZED: "1" stops consuming stdin after initialized. * - LSP_FAKE_EXIT_DELAY_MS / LSP_FAKE_EXIT_MARKER: delay protocol exit and record exit/termination. * - LSP_FAKE_NO_SHUTDOWN: "1" ignores the shutdown request (forces kill escalation). @@ -144,8 +144,8 @@ function handle(message: { id?: number; method?: string; params?: unknown; resul return } if (method === 'initialized') { - if (initializedMarker !== undefined) appendFileSync(initializedMarker, 'INITIALIZED\n') if (pauseStdinAfterInitialized) process.stdin.pause() + if (initializedMarker !== undefined) appendFileSync(initializedMarker, 'INITIALIZED\n') return } if (method === 'textDocument/didClose') return diff --git a/packages/lsp/lsp-stdio/tests/instance.spec.ts b/packages/lsp/lsp-stdio/tests/instance.spec.ts index 7e7ee17696..e8198110cd 100644 --- a/packages/lsp/lsp-stdio/tests/instance.spec.ts +++ b/packages/lsp/lsp-stdio/tests/instance.spec.ts @@ -20,7 +20,7 @@ let root: string let ws: string let ctx: Context let fs: LocalFileSystem -let live: LspInstance[] = [] +const live: LspInstance[] = [] beforeEach(async () => { root = await realpath(await mkdtemp(join(tmpdir(), 'lsp-inst-'))) @@ -33,10 +33,12 @@ beforeEach(async () => { }) afterEach(async () => { - for (const instance of live) await instance.dispose() - live = [] - await ctx.fiber.dispose() - await rm(root, { recursive: true, force: true }) + const instances = live.splice(0) + const ownedContext = ctx + const directory = root + for (const instance of instances) await instance.dispose() + await ownedContext.fiber.dispose() + await rm(directory, { recursive: true, force: true }) }) function makeInstance( @@ -202,25 +204,40 @@ describe('LspInstance query and abort', () => { await instance.dispose() }) - it('terminates when abort interrupts a backpressured didOpen write', async () => { + it('terminates when abort interrupts a backpressured didOpen write', async ({ task, signal }) => { // The fixture consumes initialized, then stops reading. A document larger than the stdio pipe // keeps didOpen's write callback pending until cancellation forces bounded process teardown. await writeFile(join(ws, 'a.ts'), 'x'.repeat(2_000_000)) const marker = join(root, 'initialized.log') + const didOpenStarted = Promise.withResolvers() + let didOpenFinished = false const instance = makeInstance({ LSP_FAKE_INITIALIZED_MARKER: marker, LSP_FAKE_PAUSE_STDIN_AFTER_INITIALIZED: '1', }, { shutdownTimeoutMs: 100, killGraceMs: 100, + }, (stdin, message, done) => { + if ((message as { method?: unknown }).method !== 'textDocument/didOpen') { + stdin.write(encodeMessage(message), done) + return + } + stdin.write(encodeMessage(message), (error) => { + didOpenFinished = true + done(error) + }) + didOpenStarted.resolve(undefined) }) const controller = new AbortController() - const pending = run(instance, 'goToDefinition', controller.signal) - await waitForFile(marker) - // Let the client enter the large didOpen write after the fixture has paused stdin. - await new Promise(resolve => setTimeout(resolve, 100)) + const outcome = run(instance, 'goToDefinition', controller.signal) + .then(() => undefined, (error: unknown) => error) + await waitForFile(marker, task.timeout, signal) + await didOpenStarted.promise + signal.throwIfAborted() + expect(didOpenFinished).toBe(false) controller.abort(new Error('didOpen-abort')) - await expect(pending).rejects.toThrow(/didOpen-abort/) + const failure = await outcome + expect(() => { throw failure }).toThrow(/didOpen-abort/) expect(instance.dead).toBe(true) }) @@ -382,9 +399,10 @@ function failingWriter(method: string, failure = new Error(`fixture ${method} fa } /** Wait until a fixture marker exists, bounded so a broken handshake cannot hang the test. */ -async function waitForFile(path: string, timeoutMs = 3000): Promise { +async function waitForFile(path: string, timeoutMs: number, signal: AbortSignal): Promise { const started = Date.now() for (;;) { + signal.throwIfAborted() try { await readFile(path) return diff --git a/packages/sandbox/sandbox-local/README.i18n.yaml b/packages/sandbox/sandbox-local/README.i18n.yaml index b7d72d665d..ff45f7054b 100644 --- a/packages/sandbox/sandbox-local/README.i18n.yaml +++ b/packages/sandbox/sandbox-local/README.i18n.yaml @@ -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 packages/sandbox/sandbox-local/README.md -README.md: 75bfb1124ece23a3a9cea0f733fb1cca42a19c16 -README.zh.md: 0edfce8c6c53a663bfe2d2e80758da83e75172f6 +README.md: 33853b68407c2d3294ad1323e47b5473419b0e12 +README.zh.md: 8d62c20c65e2d7555844a1ba81e13694c5c6a8df diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index 75bfb1124e..33853b6840 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -74,7 +74,7 @@ Selection is by platform first, probes second: each platform has a runner chain The bwrap profile combines a read-only host root, a fresh `/dev`, and `/proc` from a private PID namespace — commands manage their descendants but cannot see host processes, so procfs magic links cannot bypass the mounts; `workspace-write` adds an ephemeral `/tmp` and a writable workspace bind. The [private-PID note](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md) records the boundary. -The Landlock launcher ships as an npm-distributed native addon (`@deepseek-ai/node-addon-landlock-run`) that supplies the platform launcher, functional probe, and grant vocabulary; this provider maps mode to grants only, keeping path resolution and probe parsing with the versioned binary. +The `@deepseek-ai/node-addon-system/landlock-run` API supplies the platform launcher, functional probe, and grant vocabulary; this provider maps mode to grants only, keeping path resolution and probe parsing with the versioned binary. The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists derived from the shared `writableRoots` helper, so exactly the mode's promised file effects are governed; every root is canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). diff --git a/packages/sandbox/sandbox-local/README.zh.md b/packages/sandbox/sandbox-local/README.zh.md index 0edfce8c6c..8d62c20c65 100644 --- a/packages/sandbox/sandbox-local/README.zh.md +++ b/packages/sandbox/sandbox-local/README.zh.md @@ -74,7 +74,7 @@ kind: "package-reference" bwrap profile 组合只读宿主根目录、全新 `/dev` 与私有 PID 命名空间中的 `/proc`——命令可管理其后代,但看不到宿主进程,因此 procfs 魔法链接无法绕过挂载;`workspace-write` 另加临时的 `/tmp` 与可写工作区绑定挂载。[私有 PID 笔记](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.zh.md)记录该边界。 -Landlock launcher 以 npm 分发的原生插件(`@deepseek-ai/node-addon-landlock-run`)提供平台 launcher、功能探测与授权词汇;此提供方只做模式到授权的映射,把路径解析与探测解析保留在带版本的 binary 中。 +`@deepseek-ai/node-addon-system/landlock-run` API 提供平台 launcher、功能探测与授权词汇;此提供方只做模式到授权的映射,把路径解析与探测解析保留在带版本的 binary 中。 Seatbelt profile 默认允许,带 `(deny file-write*)` 与来自共享 `writableRoots` 辅助函数的写入 allow-list,因此恰好管辖模式承诺的文件操作;每个根目录都经过规范化,因为 Seatbelt 匹配解析后的路径(`/tmp` 就是 `/private/tmp`)。 diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index 12657291d0..bfff2bfdc8 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -35,7 +35,7 @@ "dependencies": { "@deepseek-ai/dsh-sandbox-windows-acl": "workspace:^", "@deepseek-ai/dsh-util-values": "workspace:^", - "@deepseek-ai/node-addon-landlock-run": "workspace:^", + "@deepseek-ai/node-addon-system": "workspace:^", "@deepseek-ai/schemastery": "workspace:^" }, "devDependencies": { diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index f9415d59d4..9e9b45a741 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -30,7 +30,7 @@ import { LAUNCHER_FAILURE_EXIT, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock, -} from '@deepseek-ai/node-addon-landlock-run' +} from '@deepseek-ai/node-addon-system/landlock-run' import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' diff --git a/packages/sandbox/sandbox-local/src/profiles.ts b/packages/sandbox/sandbox-local/src/profiles.ts index 4d647580c5..23c10d5f82 100644 --- a/packages/sandbox/sandbox-local/src/profiles.ts +++ b/packages/sandbox/sandbox-local/src/profiles.ts @@ -4,7 +4,7 @@ * @module @deepseek-ai/dsh-sandbox-local/profiles */ -import { grantArgs as landlockGrantArgs } from '@deepseek-ai/node-addon-landlock-run' +import { grantArgs as landlockGrantArgs } from '@deepseek-ai/node-addon-system/landlock-run' import { writableRoots } from '@deepseek-ai/dsh-sandbox' import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' diff --git a/packages/sandbox/sandbox-local/tests/landlock.e2e.ts b/packages/sandbox/sandbox-local/tests/landlock.e2e.ts index 4bee858b78..9bc81d6e8e 100644 --- a/packages/sandbox/sandbox-local/tests/landlock.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/landlock.e2e.ts @@ -6,7 +6,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' -import { launcherPath } from '@deepseek-ai/node-addon-landlock-run' +import { launcherPath } from '@deepseek-ai/node-addon-system/landlock-run' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' /** diff --git a/packages/sandbox/sandbox-local/tests/local.spec.ts b/packages/sandbox/sandbox-local/tests/local.spec.ts index 3cfcdde6e3..e6d7aaa258 100644 --- a/packages/sandbox/sandbox-local/tests/local.spec.ts +++ b/packages/sandbox/sandbox-local/tests/local.spec.ts @@ -12,7 +12,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-landlock-run' +import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-system/landlock-run' import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import { diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts index 8f353ffcd2..de6c8f1c09 100644 --- a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts @@ -22,11 +22,11 @@ import { packedWorkspaceClosure, readWorkspacePackages } from './packed-workspac const packageDir = fileURLToPath(new URL('..', import.meta.url)) const repoRoot = fileURLToPath(new URL('../../../..', import.meta.url)) -const nativeDir = join(repoRoot, 'native/landlock-run') +const nativeDir = join(repoRoot, 'native/system') const sourceLauncher = join(nativeDir, 'packages', `linux-${process.arch}`, 'bin', 'landlock-run') -const platformPackageName = `@deepseek-ai/node-addon-landlock-run-linux-${process.arch}` +const platformPackageName = `@deepseek-ai/node-addon-system-linux-${process.arch}` -const NATIVE_PACKAGE_PREFIX = '@deepseek-ai/node-addon-landlock-run' +const NATIVE_PACKAGE_PREFIX = '@deepseek-ai/node-addon-system' /** ELF `e_machine` (offset 18, LE) for this host: x86-64 = 62, AArch64 = 183. */ const E_MACHINE = { x64: 62, arm64: 183 }[process.arch as 'x64' | 'arm64'] @@ -109,7 +109,7 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish- import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { Context } from '@deepseek-ai/cordis' - import { launcherPath } from '@deepseek-ai/node-addon-landlock-run' + import { launcherPath } from '@deepseek-ai/node-addon-system/landlock-run' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' const ctx = new Context() await ctx.plugin(LocalSandboxProvider, {}) diff --git a/packages/sandbox/sandbox-local/tsconfig.json b/packages/sandbox/sandbox-local/tsconfig.json index 67e8ee678a..701e4c0501 100644 --- a/packages/sandbox/sandbox-local/tsconfig.json +++ b/packages/sandbox/sandbox-local/tsconfig.json @@ -18,7 +18,7 @@ "path": "../../../vendor/schemastery" }, { - "path": "../../../native/landlock-run/packages/entry" + "path": "../../../native/system/packages/entry" }, { "path": "../../llm/llm" diff --git a/packages/session/session-format-v2-to-v3/tests/preset-migration.spec.ts b/packages/session/session-format-v2-to-v3/tests/preset-migration.spec.ts index 4121ba6619..5c4d929b88 100644 --- a/packages/session/session-format-v2-to-v3/tests/preset-migration.spec.ts +++ b/packages/session/session-format-v2-to-v3/tests/preset-migration.spec.ts @@ -21,7 +21,7 @@ function migrate(event: SessionFormatEvent): SessionFormatEvent | undefined { } describe('released code preset references', () => { - it.each([undefined, 'code', 'ptc', 'standard', 'minimal', 'cordis', 'code-custom', 'Code'])( + it.each([undefined, 'code', 'ptc', 'standard', 'minimal', 'custom-agent', 'code-custom', 'Code'])( 'migrates only the exact legacy header preset (%s)', (agentPreset) => { const source = Object.freeze({ ...header, ...(agentPreset === undefined ? {} : { agentPreset }) }) expect(sessionFormatV2ToV3.migrateHeader(source)).toEqual({ @@ -32,7 +32,7 @@ describe('released code preset references', () => { }, ) - it.each(['code', 'ptc', 'standard', 'minimal', 'cordis', 'code-custom', 'Code'])( + it.each(['code', 'ptc', 'standard', 'minimal', 'custom-agent', 'code-custom', 'Code'])( 'preserves admitted selection metadata (%s)', (agentPreset) => { const event = Object.freeze({ type: 'agent-preset/selected', seq: 0, time: -7, ignorable: true, diff --git a/packages/session/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml index 1ad458bf11..e07e97d658 100644 --- a/packages/session/session-persistence-jsonl/README.i18n.yaml +++ b/packages/session/session-persistence-jsonl/README.i18n.yaml @@ -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 packages/session/session-persistence-jsonl/README.md -README.md: 1683e6eaf828f36019631e4cb1bde962a087819b -README.zh.md: f83b0b9f0dea8a4daf46884a700317f773f8f863 +README.md: 76d4c635d52d705adc401120a8acbee0fb86adf4 +README.zh.md: e2ed4465f1132c43ee7d13e1d36e8e34d6d53a5b diff --git a/packages/session/session-persistence-jsonl/README.md b/packages/session/session-persistence-jsonl/README.md index 1683e6eaf8..76d4c635d5 100644 --- a/packages/session/session-persistence-jsonl/README.md +++ b/packages/session/session-persistence-jsonl/README.md @@ -157,6 +157,7 @@ These limits define when this backend is a poor fit or needs special operational - **Nothing deletes session files** — logs accumulate under `root` until removed externally; the seam has no deletion API. - **One live writer per session** — the write-handle claim excludes a second writer inside the owning backend instance, and a kernel lock (non-blocking `flock(2)` on `session.lock`; on Windows a named kernel semaphore derived from that path, with no filesystem footprint) excludes every other instance and process; the lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write, so an unmaterialized session leaves no filesystem footprint. A crashed holder's lock dies with its process, so its session is writable again immediately, while a live-but-wedged holder blocks writers until its process exits (on POSIX, removing the lock file forfeits that exclusion; release itself never removes it). Advisory `flock` is unreliable on some network filesystems (NFSv3), and the Windows semaphore name is per login session. - **POSIX materialization requires hard-link support** — first append uses `link()` so same-id races fail instead of overwriting a committed log; Windows uses write-through rename without replacement. +- **POSIX writes require the matching prebuilt system addon** — [`node-addon-system`](../../../native/system/README.md) supplies asynchronous flock without consumer-side compilation. A missing addon rejects write ownership; Windows retains its semaphore implementation. ### Dev Note diff --git a/packages/session/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md index f83b0b9f0d..e2ed4465f1 100644 --- a/packages/session/session-persistence-jsonl/README.zh.md +++ b/packages/session/session-persistence-jsonl/README.zh.md @@ -157,6 +157,7 @@ JSONL 存储不修改实时请求前缀。只有重建历史、当前 envelope - **不删除会话文件**——日志在 `root` 下累积,直到外部移除;seam 无删除接口。 - **每会话一个活动写入方**——写句柄认领在所属后端实例内排除第二个写入方,内核锁(`session.lock` 上的非阻塞 `flock(2)`;Windows 上为由该路径派生的命名内核信号量,零文件系统足迹)排除其他所有实例与进程;锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取,因此未物化的会话不留任何文件系统足迹。崩溃持有者的锁随其进程消亡,会话立即可再写入,而活着但卡死的持有者会阻塞写入方直到其进程退出(POSIX 上删除锁文件即放弃该排他;释放本身从不删除它)。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠,Windows 信号量名按登录会话隔离。 - **POSIX 实体化需要硬链接支持**——第一次 append 使用 `link()`,使同 id 竞态失败而不覆盖已提交日志;Windows 使用无替换 write-through rename。 +- **POSIX 写入需要匹配的预编译系统 addon**——[`node-addon-system`](../../../native/system/README.zh.md) 提供异步 flock,无须在用户侧编译。addon 缺失时拒绝写入所有权;Windows 保留其信号量实现。 ### 开发备注 diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index 794e72ccbf..88adc24c83 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -37,7 +37,7 @@ "@deepseek-ai/dsh-session-format": "workspace:^", "@deepseek-ai/dsh-session-format-catalog": "workspace:^", "@deepseek-ai/dsh-session-format-v2-to-v3": "workspace:^", - "fs-ext": "2.1.1", + "@deepseek-ai/node-addon-system": "workspace:^", "koffi": "^3.1.0", "@deepseek-ai/schemastery": "workspace:^" }, @@ -45,7 +45,6 @@ "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-format-v0-to-v1": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", - "@types/fs-ext": "2.0.3" + "@deepseek-ai/cordis": "workspace:^" } } diff --git a/packages/session/session-persistence-jsonl/src/lease.ts b/packages/session/session-persistence-jsonl/src/lease.ts index 033f96c5c4..59dfe27fca 100644 --- a/packages/session/session-persistence-jsonl/src/lease.ts +++ b/packages/session/session-persistence-jsonl/src/lease.ts @@ -1,7 +1,7 @@ /** * Cross-process write-ownership lock for one session's artifact directory, * held for the whole life of a write handle. The arbiter is the kernel: - * POSIX takes a non-blocking `flock(2)` (through fs-ext) on `session.lock` + * POSIX takes a non-blocking `flock(2)` via native system support on `session.lock` * beside the log, and Windows holds a named kernel semaphore derived from * that path — never a file lock or handle, so readers, searches, and * directory removal proceed freely while the lock is held. Contention maps @@ -22,7 +22,7 @@ * unmaterialized session has no filesystem footprint. Release never removes * the POSIX lock file: every acquired lock belongs to a materialized or * materializing session, and the surviving file keeps the stable inode later - * lockers verify against. The browser worker deployment stubs fs-ext to + * lockers verify against. The browser worker stubs the native flock entry to * immediate success: it is single-process, so the in-process write claim * already excludes every writer. * @module @deepseek-ai/dsh-session-persistence-jsonl/lease @@ -31,7 +31,7 @@ import { mkdir, open, stat } from 'node:fs/promises' import type { FileHandle } from 'node:fs/promises' import { join } from 'node:path' -import { flock } from 'fs-ext' +import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock' import { SessionAlreadyOwnedError } from '@deepseek-ai/dsh-session-persistence' import type { SessionId } from '@deepseek-ai/dsh-session' import { acquireLockHandleWin32, releaseLockHandleWin32 } from './win32.ts' @@ -44,16 +44,6 @@ type HeldLock = | { readonly kind: 'posix'; readonly handle: FileHandle } | { readonly kind: 'win32'; readonly handle: number } -/** Promise face over fs-ext's callback flock, pinned to its string-flag overload. */ -function flockAsync(fd: number, flags: 'exnb' | 'un'): Promise { - return new Promise((resolve, reject) => { - flock(fd, flags, (error) => { - if (error) reject(error) - else resolve() - }) - }) -} - /** Whether a flock failure means another descriptor holds the lock. */ function isLockContention(error: unknown): boolean { const code = (error as NodeJS.ErrnoException | null)?.code @@ -101,7 +91,7 @@ export class SessionWriteLease { const handle = await open(path, 'w') try { try { - await flockAsync(handle.fd, 'exnb') + await tryLockExclusive(handle.fd) } catch (error: unknown) { if (isLockContention(error)) throw new SessionAlreadyOwnedError(id) throw error diff --git a/packages/session/session-persistence-jsonl/tests/lease.spec.ts b/packages/session/session-persistence-jsonl/tests/lease.spec.ts index 3933c865fd..00012ab1e2 100644 --- a/packages/session/session-persistence-jsonl/tests/lease.spec.ts +++ b/packages/session/session-persistence-jsonl/tests/lease.spec.ts @@ -39,7 +39,7 @@ const refuse = vi.hoisted(() => ({ lockOpen: false, /** Next flock call fails EACCES (a non-contention kernel refusal). */ flock: false, - /** Next flock call fails EWOULDBLOCK (the Windows LockFileEx contention code). */ + /** Next flock call fails EWOULDBLOCK. */ flockBusy: false, /** Next stat of a lock file fails EACCES (unreadable path). */ lockStat: false, @@ -84,23 +84,20 @@ vi.mock('node:fs/promises', async (importOriginal) => { } }) -vi.mock('fs-ext', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@deepseek-ai/node-addon-system/flock', async (importOriginal) => { + const actual = await importOriginal() return { - ...actual, - flock: ((fd: number, flags: never, callback: (error: Error | null) => void) => { + tryLockExclusive: async (fd: number): Promise => { if (refuse.flock) { refuse.flock = false - callback(Object.assign(new Error('EACCES: injected flock refusal'), { code: 'EACCES' })) - return + throw Object.assign(new Error('EACCES: injected flock refusal'), { code: 'EACCES' }) } if (refuse.flockBusy) { refuse.flockBusy = false - callback(Object.assign(new Error('EWOULDBLOCK: injected contention'), { code: 'EWOULDBLOCK' })) - return + throw Object.assign(new Error('EWOULDBLOCK: injected contention'), { code: 'EWOULDBLOCK' }) } - (actual.flock as (fd: number, flags: never, callback: (error: Error | null) => void) => void)(fd, flags, callback) - }) as typeof actual.flock, + return actual.tryLockExclusive(fd) + }, } }) diff --git a/packages/session/session-persistence-jsonl/tsconfig.json b/packages/session/session-persistence-jsonl/tsconfig.json index c0eab86dcd..393ada381f 100644 --- a/packages/session/session-persistence-jsonl/tsconfig.json +++ b/packages/session/session-persistence-jsonl/tsconfig.json @@ -8,6 +8,9 @@ "src" ], "references": [ + { + "path": "../../../native/system/packages/entry" + }, { "path": "../../../vendor/cosmokit" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index 381aff48f5..1427ef8f68 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -41,7 +41,7 @@ "@deepseek-ai/dsh-sandbox-local": "workspace:^", "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/node-addon-landlock-run": "workspace:^", + "@deepseek-ai/node-addon-system": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^" } } diff --git a/packages/shell/bash-sandbox/tests/landlock.e2e.ts b/packages/shell/bash-sandbox/tests/landlock.e2e.ts index 0160caaa56..b0caa5e91f 100644 --- a/packages/shell/bash-sandbox/tests/landlock.e2e.ts +++ b/packages/shell/bash-sandbox/tests/landlock.e2e.ts @@ -5,7 +5,7 @@ import { homedir, tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { launcherPath } from '@deepseek-ai/node-addon-landlock-run' +import { launcherPath } from '@deepseek-ai/node-addon-system/landlock-run' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' @@ -21,7 +21,7 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local' * `@deepseek-ai/dsh-sandbox-local`. * * Self-skips when the running kernel does not enforce Landlock. CI builds the launcher from - * `native/landlock-run` before running this file. + * `native/system` before running this file. */ const probe = spawnSync(launcherPath(), ['--probe'], { timeout: 5_000, encoding: 'utf8' }) diff --git a/packages/shell/bash-sandbox/tests/partial-landlock.spec.ts b/packages/shell/bash-sandbox/tests/partial-landlock.spec.ts index d2355c7dad..10aa1ecf2c 100644 --- a/packages/shell/bash-sandbox/tests/partial-landlock.spec.ts +++ b/packages/shell/bash-sandbox/tests/partial-landlock.spec.ts @@ -10,7 +10,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-landlock-run' +import { LAUNCHER_FAILURE_EXIT } from '@deepseek-ai/node-addon-system/landlock-run' import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' diff --git a/packages/shell/bash-sandbox/tsconfig.json b/packages/shell/bash-sandbox/tsconfig.json index 5a2d00fe3c..fccff8c4c9 100644 --- a/packages/shell/bash-sandbox/tsconfig.json +++ b/packages/shell/bash-sandbox/tsconfig.json @@ -15,7 +15,7 @@ "path": "../../../vendor/cordis" }, { - "path": "../../../native/landlock-run/packages/entry" + "path": "../../../native/system/packages/entry" }, { "path": "../../util/brand" diff --git a/packages/shell/pwsh-local/tests/executor.spec.ts b/packages/shell/pwsh-local/tests/executor.spec.ts index f4dc5b6df6..46f963031a 100644 --- a/packages/shell/pwsh-local/tests/executor.spec.ts +++ b/packages/shell/pwsh-local/tests/executor.spec.ts @@ -30,8 +30,14 @@ afterAll(() => { /** Per-test temp dirs, removed after each test. */ const tempDirs: string[] = [] -afterEach(() => { - for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true }) +const contexts: Context[] = [] +afterEach(async () => { + const ownedContexts = contexts.splice(0) + const directories = tempDirs.splice(0) + const results = await Promise.allSettled(ownedContexts.map(ctx => ctx.fiber.dispose())) + for (const dir of directories) rmSync(dir, { recursive: true, force: true }) + const failures: unknown[] = results.flatMap((result): unknown[] => result.status === 'rejected' ? [result.reason] : []) + if (failures.length > 0) throw new AggregateError(failures, 'PowerShell fixture cleanup failed') }) // The probe follows the executor's own resolution (Program Files installs on @@ -51,6 +57,7 @@ function samePath(actual: string, expected: string): boolean { async function setup(config: ConstructorParameters[1] = {}) { const ctx = new Context() + contexts.push(ctx) await ctx.plugin(LocalSubprocessRuntime) ;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir } // A short kill grace via the REAL config path, so escalation tests stay fast. @@ -422,6 +429,7 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)' })) const partialOutput = await readUntil(proc, '[bg-env][bg-dsh-env]', task.timeout) await proc.done + expect(proc.status).toBe('completed') const output = partialOutput + lf(proc.readOutput().delta) expect(output).toBe('bg-stdin\n[bg-env][bg-dsh-env]\n') expect(proc.exitCode).toBe(0) diff --git a/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts b/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts index f2fd65ee42..c5f5dd4ee3 100644 --- a/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts +++ b/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts @@ -124,31 +124,49 @@ describe('sqlite backend specifics', () => { await backend.close() }) + // Repeated durable DDL across four connections needs an instrumented I/O budget. it('leaves a failed materialization unstamped so a repaired medium reopens', async () => { const path = await freshDbPath() // Obstruct table creation: an index squatting on the unit_globals name // makes CREATE TABLE IF NOT EXISTS throw AFTER the units table exists. const setup = new DatabaseSync(path) - setup.exec('CREATE TABLE squatter (x TEXT)') - setup.exec('CREATE INDEX unit_globals ON squatter(x)') - setup.close() + try { + setup.exec('CREATE TABLE squatter (x TEXT)') + setup.exec('CREATE INDEX unit_globals ON squatter(x)') + } finally { + setup.close() + } const broken = backendAt(path) - await expect(broken.kv.open(DESCRIPTOR)).rejects.toThrow(/already an index/) - await broken.close() + try { + await expect(broken.kv.open(DESCRIPTOR)).rejects.toThrow(/already an index/) + } finally { + await broken.close() + } // Clear the obstruction; the medium must still be version 0, not a // half-materialized database stamped as current. const repair = new DatabaseSync(path) - expect((repair.prepare('PRAGMA user_version').get() as { user_version: number }).user_version).toBe(0) - repair.exec('DROP INDEX unit_globals') - repair.close() + try { + expect((repair.prepare('PRAGMA user_version').get() as { user_version: number }).user_version).toBe(0) + expect(repair.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'units'").get()) + .toEqual({ name: 'units' }) + repair.exec('DROP INDEX unit_globals') + expect(repair.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'unit_globals'").get()) + .toBeUndefined() + } finally { + repair.close() + } const backend = backendAt(path) - const unit = await backend.kv.open(DESCRIPTOR) - await unit.putRecord('records', 'k', { n: 1 }) - await backend.close() - }) + try { + const unit = await backend.kv.open(DESCRIPTOR) + await unit.putRecord('records', 'k', { n: 1 }) + expect((await unit.loadAll()).tables['records']).toEqual({ k: { n: 1 } }) + } finally { + await backend.close() + } + }, 90_000) it('rejects unparsable stored JSON with malformed-medium', async () => { const path = await freshDbPath() diff --git a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts index d88856a13e..1de31c00ae 100644 --- a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts +++ b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts @@ -12,6 +12,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import type { SubprocessHandle, SubprocessOutcome } from '@deepseek-ai/dsh-subprocess' import * as acp from '../src/index.ts' +import * as acpRun from '../src/run.ts' import { acpStopReason, acpContentText, DEFAULT_DISPOSE_EOF_GRACE_MS, DEFAULT_DISPOSE_GRACE_MS, disposeAcpChild, startAcpRun, toAcpPrompt, type AcpRunSpec } from '../src/run.ts' import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local' import { spawnSubprocess } from '@deepseek-ai/dsh-subprocess-local/src/spawn.ts' @@ -941,40 +942,54 @@ describe('dsh-subagent-acp', () => { } }) - it('dispose escalates SIGTERM → SIGKILL for a child that traps SIGTERM (bounded quiescence)', async ({ task }) => { - // The child traps SIGTERM and keeps its event loop alive, so a graceful - // term alone would hang dispose forever. With a short grace, dispose must - // escalate to SIGKILL and return once the process is actually gone. + it('dispose reaps a child that ignores EOF with the host force-termination outcome', async ({ onTestFinished, task }) => { + // POSIX must escalate past the armed SIGTERM trap; Windows force-terminates + // the process tree without delivering a catchable signal. + let child: ReturnType | undefined + const active: { run?: Awaited> } = {} + let exited = false const tmp = mkdtempSync(join(tmpdir(), 'acp-trap-')) - const ready = join(tmp, 'trap-armed') - try { - const spec: AcpRunSpec = { - command: process.execPath, - args: [mockServer], - cwd: process.cwd(), - permission: 'reject', - env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready }, - // Short on BOTH tiers: the trap ignores EOF and SIGTERM, so dispose must - // burn the EOF window, then the SIGTERM window, then SIGKILL — keep each - // small so the whole ladder finishes well within the 4000ms bound. - disposeEofGraceMs: 150, - disposeGraceMs: 150, - spawn: spawnSubprocess, + onTestFinished(async () => { + try { + if (!exited) child?.terminateForHostExit() + const cleanup = await Promise.allSettled([active.run?.dispose(), child?.waitForExit(), child?.done]) + const failures: unknown[] = [] + for (const result of cleanup) { + if (result.status === 'rejected') failures.push(result.reason) + } + if (failures.length > 0) throw new AggregateError(failures, 'ACP test cleanup failed') + } finally { + rmSync(tmp, { recursive: true, force: true }) } - const run = await startAcpRun(request(), spec) - // Wait until the child has BOOTED AND ARMED THE TRAP (a condition, not a - // sleep) — otherwise SIGTERM races the trap install and the default handler - // terminates the child, never exercising the escalation. - await waitForFile(ready, task.timeout) - // Don't await result (the child hangs). Dispose must still return promptly - // via the SIGKILL escalation — bound it so a regression (no escalation) - // fails loud instead of hanging the suite. - await expect(Promise.race([ - run.dispose(), - new Promise((_r, reject) => { setTimeout(() => { reject(new Error('dispose did not return — no SIGKILL escalation')) }, 4000) }), - ])).resolves.toBeUndefined() - } finally { - rmSync(tmp, { recursive: true, force: true }) + }) + const ready = join(tmp, 'trap-armed') + const spec: AcpRunSpec = { + command: process.execPath, + args: [mockServer], + cwd: process.cwd(), + permission: 'reject', + env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready }, + disposeEofGraceMs: 150, + disposeGraceMs: 150, + spawn: (spec) => { + child = spawnSubprocess(spec) + return child + }, + } + const run = await startAcpRun(request(), spec) + active.run = run + await waitForFile(ready, task.timeout) + // Process reaping uses the lane's test/hook budget, not a second grace timer. + await expect(run.dispose()).resolves.toBeUndefined() + expect(await child!.waitForExit()).toBe(true) + exited = true + const outcome = await child!.done + if (process.platform === 'win32') { + expect(outcome.signal).toBeNull() + expect(outcome.exitCode).not.toBeNull() + expect(outcome.exitCode).not.toBe(0) + } else { + expect(outcome.signal).toBe('SIGKILL') } }) @@ -1375,37 +1390,38 @@ describe('dsh-subagent-acp', () => { expect(errors).toEqual([rawMessage]) }) - it.skipIf(process.platform === 'win32')('plugin-config dispose graces reach the run (SIGKILL escalation through the provider)', async ({ task }) => { - // Same trap scenario as the direct startAcpRun escalation test, but the - // graces arrive via the PLUGIN CONFIG through the registered provider — so a - // regression that stops threading config into AcpRunSpec (falling back to - // the 6s/3s defaults) blows past the 4000ms bound and fails loud. + it('plugin-config dispose graces reach the real ACP run', async ({ onTestFinished, task }) => { + const ctx = new Context() const tmp = mkdtempSync(join(tmpdir(), 'acp-cfg-trap-')) + onTestFinished(async () => { + try { + await ctx.fiber.dispose() + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) + const start = vi.spyOn(acpRun, 'startAcpRun') + onTestFinished(() => { start.mockRestore() }) const ready = join(tmp, 'trap-armed') - try { - const ctx = new Context() - await ctx.plugin(SessionProjectionRegistry) - await ctx.plugin(SubagentRuntime) - await ctx.plugin(LocalSubprocessRuntime) - await ctx.plugin(acp, { - providerName: 'acp', - command: process.execPath, - args: [mockServer], - permission: 'reject', - env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready }, - disposeEofGraceMs: 150, - disposeGraceMs: 150, - }) - const run = await ctx.subagents.start('acp', request()) - await waitForFile(ready, task.timeout) - await expect(Promise.race([ - run.dispose(), - new Promise((_r, reject) => { setTimeout(() => { reject(new Error('dispose did not return — config graces not threaded to the run')) }, 4000) }), - ])).resolves.toBeUndefined() - await ctx.fiber.dispose() - } finally { - rmSync(tmp, { recursive: true, force: true }) - } + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(LocalSubprocessRuntime) + await ctx.plugin(acp, { + providerName: 'acp', + command: process.execPath, + args: [mockServer], + permission: 'reject', + env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready }, + disposeEofGraceMs: 150, + disposeGraceMs: 150, + }) + const run = await ctx.subagents.start('acp', request()) + expect(start).toHaveBeenCalledExactlyOnceWith(expect.anything(), expect.objectContaining({ + disposeEofGraceMs: 150, + disposeGraceMs: 150, + })) + await waitForFile(ready, task.timeout) + await expect(run.dispose()).resolves.toBeUndefined() }) it('rejects a dispose grace outside the Node timer range at load', async () => { diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index 37e20bd655..70ecc62c99 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -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 packages/subagent/subagent-claude-code/README.md -README.md: a04fa1ad086cc9d8aaf67bd410ee64db1546f347 -README.zh.md: 52bb88df396cfaef1c8e10aabfcae40590698151 +README.md: 947c3fe3bf14b853c6c0e76717adb12ab5a001cb +README.zh.md: dce034b94b2cac31284aea9721a2d9f6ff714caa diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index a04fa1ad08..947c3fe3bf 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -190,7 +190,7 @@ These limits define when this provider is a poor fit or needs special operationa This Dev Note is working context for maintainers: open questions and undecided directions. It is explicitly non-authoritative — shipped behavior and limits live in the sections above and in the package code. - **Payload size disclosure** — the current darwin-arm64 platform payload packs to about 92 MB and unpacks to about 325 MB; these are disclosure numbers, not installation thresholds. -- **Version-pinned protocol** — the runtime dependency is pinned to Agent SDK 0.3.241; upgrading pins a new SDK version and requires re-running the keyless real-product and loader-composition evidence. +- **Version-pinned protocol** — the runtime dependency is pinned to Agent SDK 0.3.263; upgrading pins a new SDK version and requires re-running the keyless real-product and loader-composition evidence. diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 52bb88df39..dce034b94b 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -190,7 +190,7 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为与限制以上文和包代码为准。 - **载荷体积披露**——当前 darwin-arm64 平台载荷压缩后约 92 MB、解包后约 325 MB;这些是披露数字,不是安装阈值。 -- **版本锁定的协议**——运行时依赖锁定为 Agent SDK 0.3.241;升级会锁定新的 SDK 版本,并需要重新运行无密钥真实产品与 loader 组合证据。 +- **版本锁定的协议**——运行时依赖锁定为 Agent SDK 0.3.263;升级会锁定新的 SDK 版本,并需要重新运行无密钥真实产品与 loader 组合证据。 diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index 4d86ac89d3..e567eb83cc 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -41,7 +41,7 @@ "@deepseek-ai/dsh-timeout": "workspace:^" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.3.241", + "@anthropic-ai/claude-agent-sdk": "0.3.263", "@anthropic-ai/sdk": "0.93.0", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", diff --git a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts index 5611e4fce2..8ddba2c05f 100644 --- a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts +++ b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts @@ -126,13 +126,13 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)( }) await ctx.plugin(claudeCode, { env, disposeGraceMs: 3_000 }) - expect(sdkPackage.version).toBe('0.3.241') - expect(sdkPackage.claudeCodeVersion).toBe('2.1.241') - expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.241') + expect(sdkPackage.version).toBe('0.3.263') + expect(sdkPackage.claudeCodeVersion).toBe('2.1.263') + expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.263') const version = await execFileAsync(claudeBin, ['--version'], { env: { ...process.env, ...env }, }) - expect(version.stdout.trim()).toBe('2.1.241 (Claude Code)') + expect(version.stdout.trim()).toBe('2.1.263 (Claude Code)') const nonce = `DSH_CLAUDE_DEEPSEEK_${randomUUID()}` const parent = { diff --git a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts index 4e24003911..279c844e0b 100644 --- a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts @@ -286,7 +286,7 @@ function startRequest( }) } -describe('real Claude Agent SDK 0.3.241 and its distributed Claude Code 2.1.241 fixture', { +describe('real Claude Agent SDK 0.3.263 and its distributed Claude Code 2.1.263 fixture', { timeout: 60_000, }, () => { it('inherits host settings and sends the exact task and fake key to local Messages', async () => { @@ -296,13 +296,13 @@ describe('real Claude Agent SDK 0.3.241 and its distributed Claude Code 2.1.241 kind: 'complete', text: sentinel, }) - expect(sdkPackage.version).toBe('0.3.241') - expect(sdkPackage.claudeCodeVersion).toBe('2.1.241') - expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.241') + expect(sdkPackage.version).toBe('0.3.263') + expect(sdkPackage.claudeCodeVersion).toBe('2.1.263') + expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.263') const version = await execFileAsync(claudeBin, ['--version'], { env: { ...process.env, ...harness.env }, }) - expect(version.stdout.trim()).toBe('2.1.241 (Claude Code)') + expect(version.stdout.trim()).toBe('2.1.263 (Claude Code)') const run = await startRequest(harness, task) await expect(run.result).resolves.toEqual({ @@ -315,7 +315,7 @@ describe('real Claude Agent SDK 0.3.241 and its distributed Claude Code 2.1.241 (message): message is SDKSystemMessage => message.type === 'system' && message.subtype === 'init', ) - expect(initMessage?.claude_code_version).toBe('2.1.241') + expect(initMessage?.claude_code_version).toBe('2.1.263') const spawnedExecutable = harness.spawnSpecs[0]?.argv[0] expect(spawnedExecutable).toBeDefined() expect(process.platform === 'win32' diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts index 750929d405..781a13a587 100644 --- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts @@ -58,8 +58,8 @@ type QueryFactory = (params: { const queryMock = vi.hoisted(() => vi.fn()) -const CLAUDE_AGENT_SDK_VERSION = '0.3.241' -const CLAUDE_CODE_VERSION = '2.1.241' +const CLAUDE_AGENT_SDK_VERSION = '0.3.263' +const CLAUDE_CODE_VERSION = '2.1.263' const CLAUDE_PLATFORM_PACKAGES = [ '@anthropic-ai/claude-agent-sdk-darwin-arm64', '@anthropic-ai/claude-agent-sdk-darwin-x64', diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index 430334ebae..1cdedddd09 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -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 packages/subagent/subagent-codex/README.md -README.md: 57d623f67b5dba4002a1dfb62a3d76243872b193 -README.zh.md: 61c3dece3fa4139fba66f22c49f6d4298dcf8680 +README.md: 3f90c9430c296d1233f1d2973e51ed3517e61dc1 +README.zh.md: 7e5f854af342319233a20b5847f4590dcf66c1f0 diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index 57d623f67b..3f90c9430c 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -173,7 +173,7 @@ These limits define when this provider is a poor fit or needs special operationa - **Static instance selection** — Profile rows fix provider names, optional models, and tool bindings; calls cannot choose or change either a provider or model dynamically, and every exposed tool needs a unique `toolName`. - **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, trust a project, or rewrite Codex settings; configuration and authentication failures surface with their lifecycle stage and the safe `unknown` fallback rather than a separate public taxonomy. - **The native platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first run; there is no host-CLI fallback. -- **Compatibility is pinned by development evidence** — upgrading from the verified 0.149.1 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests. +- **Compatibility is pinned by development evidence** — upgrading from the verified 0.153.4 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests. - **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; the three Profile modes never create a DSH interaction channel or per-call allow policy. - **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, commentary, intermediate messages, tool traffic, usage, raw stderr, and workspace diffs remain outside the parent Session, while generic Job ids, notices, and status come from the shared job runtime. - **No optional shared capabilities** — `agentOptions`, output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. @@ -188,7 +188,7 @@ These limits define when this provider is a poor fit or needs special operationa This Dev Note is working context for maintainers: open questions and undecided directions. It is explicitly non-authoritative — shipped behavior and limits live in the sections above and in the package code. - **Payload size disclosure** — the current darwin-arm64 platform payload packs to about 114 MB and unpacks to about 282 MB; these are disclosure numbers, not installation thresholds. -- **Version-pinned protocol** — the runtime dependency is pinned to `@openai/codex@0.149.1`; upgrading requires regenerating the upstream schema evidence and rerunning the credentialed nonce tests. +- **Version-pinned protocol** — the runtime dependency is pinned to `@openai/codex@0.153.4`; upgrading requires regenerating the upstream schema evidence and rerunning the credentialed nonce tests. diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index 61c3dece3f..7e5f854af3 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -173,7 +173,7 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些 - **静态选择实例**——Profile 配置项固定提供方名称、可选模型与工具绑定;调用无法动态选择或修改提供方与模型,而且每个公开工具都需要唯一的 `toolName`。 - **身份验证与账户状态仍由原生机制管理**——Bundle 会提供 CLI,但不会创建账户、登录、信任项目或改写 Codex 设置;配置与身份验证失败会公开其生命周期阶段与安全的 `unknown` 回退,而不会增加单独的公开分类体系。 - **委派时必须存在原生平台载荷**——省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次运行时失败;不会回退到宿主 CLI。 -- **兼容性由开发证据锁定**——若要从已验证的 0.149.1 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。 +- **兼容性由开发证据锁定**——若要从已验证的 0.153.4 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。 - **没有人工审批路径**——已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;三种 Profile 模式都不会创建 DSH 交互通道或逐次调用 allow 策略。 - **assistant 载荷仅包含最终文本**——失败运行可以额外公开独立的安全诊断;推理、过程说明、中间消息、工具通信、用量信息、原始 stderr 和工作区差异不会进入父会话,通用 Job id、通知与状态来自共享作业运行时。 - **没有可选的共享能力**——对于本提供方,共享服务会拒绝 `agentOptions`、输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 @@ -188,7 +188,7 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为与限制以上文和包代码为准。 - **载荷体积披露**——当前 darwin-arm64 平台载荷压缩后约 114 MB、解包后约 282 MB;这些是披露数字,不是安装阈值。 -- **版本锁定的协议**——运行时依赖锁定为 `@openai/codex@0.149.1`;升级需要重新生成上游 schema 证据并重新运行带凭证的随机数测试。 +- **版本锁定的协议**——运行时依赖锁定为 `@openai/codex@0.153.4`;升级需要重新生成上游 schema 证据并重新运行带凭证的随机数测试。 diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index 9a4a2984e3..77717ffa05 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -44,7 +44,7 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-sdk-protocol": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", - "@openai/codex": "0.149.1" + "@openai/codex": "0.153.4" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", diff --git a/packages/subagent/subagent-codex/src/wire.ts b/packages/subagent/subagent-codex/src/wire.ts index 474d7a9a32..f12d4ae8da 100644 --- a/packages/subagent/subagent-codex/src/wire.ts +++ b/packages/subagent/subagent-codex/src/wire.ts @@ -1,5 +1,5 @@ /** - * Minimal Codex app-server 0.149.1 protocol adapter. The shared JSON-RPC + * Minimal Codex app-server 0.153.4 protocol adapter. The shared JSON-RPC * transport owns framing and request correlation; this module owns only the * product methods, current thread/turn association, unattended approval * responses, and terminal-answer selection. diff --git a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts index 8e390067bd..e72f2d1244 100644 --- a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts +++ b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts @@ -111,8 +111,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)( const version = await execFileAsync(process.execPath, [codexEntry, '--version'], { env: { ...process.env, ...env }, }) - expect(codexPackage.version).toBe('0.149.1') - expect(version.stdout.trim()).toBe('codex-cli 0.149.1') + expect(codexPackage.version).toBe('0.153.4') + expect(version.stdout.trim()).toBe('codex-cli 0.153.4') const parent = { id: 'deepseek-e2e-parent', diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts index e21b384866..0446ab3446 100644 --- a/packages/subagent/subagent-codex/tests/real-product.spec.ts +++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts @@ -208,18 +208,18 @@ function responseInputTexts(body: Record): string[] { }) } -describe('real @openai/codex 0.149.1 product', () => { +describe('real @openai/codex 0.153.4 product', () => { it('starts approve-for-me through the real app-server and returns exact text', async () => { const sentinel = 'REAL_CODEX_SENTINEL_0_149_1' const task = 'Return the fixture sentinel exactly.' const { harness, fixture } = await realHarness([ { kind: 'complete', text: sentinel }, ], 'approve-for-me') - expect(codexPackage.version).toBe('0.149.1') + expect(codexPackage.version).toBe('0.153.4') const version = await execFileAsync(process.execPath, [codexEntry, '--version'], { env: { ...process.env, ...harness.env }, }) - expect(version.stdout.trim()).toBe('codex-cli 0.149.1') + expect(version.stdout.trim()).toBe('codex-cli 0.153.4') const schemaRoot = mkdtempSync(join(tmpdir(), 'dsh-codex-schema-')) roots.push(schemaRoot) await execFileAsync(process.execPath, [ diff --git a/packages/subagent/subagent-codex/tests/responses-fixture.ts b/packages/subagent/subagent-codex/tests/responses-fixture.ts index e960f36bb2..8fe071b046 100644 --- a/packages/subagent/subagent-codex/tests/responses-fixture.ts +++ b/packages/subagent/subagent-codex/tests/responses-fixture.ts @@ -94,7 +94,7 @@ function responseObject(text: string): Record { } /** - * Build the minimal Responses SSE event sequence consumed by Codex 0.149.1. + * Build the minimal Responses SSE event sequence consumed by Codex 0.153.4. * @param text - exact assistant answer. * @returns ordered response lifecycle events. */ diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts index 32347c4784..7169ca6753 100644 --- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts +++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts @@ -64,7 +64,7 @@ vi.mock('node:fs', async (importOriginal) => { type JsonObject = Record -const CODEX_VERSION = '0.149.1' +const CODEX_VERSION = '0.153.4' const CODEX_PLATFORM_PACKAGES = [ '@openai/codex-darwin-arm64', '@openai/codex-darwin-x64', @@ -263,7 +263,7 @@ async function initializeWire(): Promise<{ wire.start() const initializing = wire.initialize(new AbortController().signal) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await initializing expect(await child.peer.nextMethod('initialized')).toEqual({ jsonrpc: '2.0', @@ -283,7 +283,7 @@ async function publishRun( ) { const starting = startCodexRun(request(undefined, signal), runSpec(child, specOverrides)) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } }) @@ -504,7 +504,7 @@ describe('task admission and package contracts', () => { [bypassChild, 'codex-bypass-model'], ] as const) { const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') expect(threadStart.params).toMatchObject({ model }) @@ -604,7 +604,7 @@ describe('task admission and package contracts', () => { expect(ctx.subagents.getProvider('codex')).toBeDefined() const starting = ctx.subagents.start('codex', request()) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') expect(threadStart.params).not.toHaveProperty('model') @@ -645,7 +645,7 @@ describe('task admission and package contracts', () => { wire.start() const initializing = wire.initialize(new AbortController().signal) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await initializing await child.peer.nextMethod('initialized') const starting = wire.startThread('/workspace', new AbortController().signal) @@ -672,7 +672,7 @@ describe('task admission and package contracts', () => { wire.start() const initializing = wire.initialize(new AbortController().signal) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await initializing await child.peer.nextMethod('initialized') const starting = wire.startThread('/workspace', new AbortController().signal) @@ -739,7 +739,7 @@ describe('CodexAppServerWire', () => { requestAttestation: false, }, }) - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await initializing await child.peer.nextMethod('initialized') @@ -1489,7 +1489,7 @@ describe('run lifecycle and quiescence', () => { void starting.then(() => { published = true }) const initialize = await child.peer.nextMethod('initialize') expect(published).toBe(false) - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') expect(published).toBe(false) @@ -1940,7 +1940,7 @@ describe('run lifecycle and quiescence', () => { const threadChild = fakeChild() const threadStarting = startCodexRun(request(), runSpec(threadChild)) const threadInitialize = await threadChild.peer.nextMethod('initialize') - threadChild.peer.respond(threadInitialize, { userAgent: 'codex-cli 0.149.1' }) + threadChild.peer.respond(threadInitialize, { userAgent: 'codex-cli 0.153.4' }) await threadChild.peer.nextMethod('initialized') const invalidThread = await threadChild.peer.nextMethod('thread/start') threadChild.peer.respond(invalidThread, { thread: { id: '', ephemeral: true } }) @@ -1955,7 +1955,7 @@ describe('run lifecycle and quiescence', () => { ) const exitedThreadInitialize = await exitedThreadChild.peer.nextMethod('initialize') exitedThreadChild.peer.respond(exitedThreadInitialize, { - userAgent: 'codex-cli 0.149.1', + userAgent: 'codex-cli 0.153.4', }) await exitedThreadChild.peer.nextMethod('initialized') await exitedThreadChild.peer.nextMethod('thread/start') @@ -1974,7 +1974,7 @@ describe('run lifecycle and quiescence', () => { const eofBeforeCloseInitialize = await eofBeforeCloseChild.peer .nextMethod('initialize') eofBeforeCloseChild.peer.respond(eofBeforeCloseInitialize, { - userAgent: 'codex-cli 0.149.1', + userAgent: 'codex-cli 0.153.4', }) await eofBeforeCloseChild.peer.nextMethod('initialized') await eofBeforeCloseChild.peer.nextMethod('thread/start') @@ -1992,7 +1992,7 @@ describe('run lifecycle and quiescence', () => { const stderrStarting = startCodexRun(request(), runSpec(stderrChild)) const stderrInitialize = await stderrChild.peer.nextMethod('initialize') stderrChild.stderr.emit('error', new Error('startup stderr broke')) - stderrChild.peer.respond(stderrInitialize, { userAgent: 'codex-cli 0.149.1' }) + stderrChild.peer.respond(stderrInitialize, { userAgent: 'codex-cli 0.153.4' }) await stderrChild.peer.nextMethod('initialized') const stderrThreadStart = await stderrChild.peer.nextMethod('thread/start') stderrChild.peer.respond(stderrThreadStart, { @@ -2018,7 +2018,7 @@ describe('run lifecycle and quiescence', () => { runSpec(child), ) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') expect(threadStart.params).toEqual({ @@ -2174,7 +2174,7 @@ describe('run lifecycle and quiescence', () => { signal: new AbortController().signal, }) const initialize = await child.peer.nextMethod('initialize') - child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' }) + child.peer.respond(initialize, { userAgent: 'codex-cli 0.153.4' }) await child.peer.nextMethod('initialized') const threadStart = await child.peer.nextMethod('thread/start') expect(threadStart.params).toEqual({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb4e737322..0c3122497b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -739,9 +739,9 @@ importers: specifier: ^1.49.0 version: 1.61.1 - native/landlock-run: + native/system: devDependencies: - '@deepseek-ai/node-addon-landlock-run': + '@deepseek-ai/node-addon-system': specifier: workspace:* version: link:packages/entry '@types/node': @@ -754,18 +754,28 @@ importers: specifier: ^6.0.3 version: 6.0.3 - native/landlock-run/packages/entry: + native/system/packages/darwin-arm64: {} + + native/system/packages/darwin-x64: {} + + native/system/packages/entry: optionalDependencies: - '@deepseek-ai/node-addon-landlock-run-linux-arm64': + '@deepseek-ai/node-addon-system-darwin-arm64': + specifier: workspace:* + version: link:../darwin-arm64 + '@deepseek-ai/node-addon-system-darwin-x64': + specifier: workspace:* + version: link:../darwin-x64 + '@deepseek-ai/node-addon-system-linux-arm64': specifier: workspace:* version: link:../linux-arm64 - '@deepseek-ai/node-addon-landlock-run-linux-x64': + '@deepseek-ai/node-addon-system-linux-x64': specifier: workspace:* version: link:../linux-x64 - native/landlock-run/packages/linux-arm64: {} + native/system/packages/linux-arm64: {} - native/landlock-run/packages/linux-x64: {} + native/system/packages/linux-x64: {} packages/acp/acp: dependencies: @@ -5661,9 +5671,9 @@ importers: '@deepseek-ai/dsh-subprocess-local': specifier: workspace:^ version: link:../../subprocess/subprocess-local - '@deepseek-ai/node-addon-landlock-run': + '@deepseek-ai/node-addon-system': specifier: workspace:^ - version: link:../../../native/landlock-run/packages/entry + version: link:../../../native/system/packages/entry '@types/picomatch': specifier: ^3.0.2 version: 3.0.2 @@ -7435,9 +7445,9 @@ importers: '@deepseek-ai/dsh-util-values': specifier: workspace:^ version: link:../../util/values - '@deepseek-ai/node-addon-landlock-run': + '@deepseek-ai/node-addon-system': specifier: workspace:^ - version: link:../../../native/landlock-run/packages/entry + version: link:../../../native/system/packages/entry '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery @@ -8036,12 +8046,12 @@ importers: '@deepseek-ai/dsh-session-format-v2-to-v3': specifier: workspace:^ version: link:../session-format-v2-to-v3 + '@deepseek-ai/node-addon-system': + specifier: workspace:^ + version: link:../../../native/system/packages/entry '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - fs-ext: - specifier: 2.1.1 - version: 2.1.1 koffi: specifier: ^3.1.0 version: 3.1.1 @@ -8058,9 +8068,6 @@ importers: '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ version: link:../session-persistence - '@types/fs-ext': - specifier: 2.0.3 - version: 2.0.3 packages/session/session-projection: dependencies: @@ -8472,9 +8479,9 @@ importers: '@deepseek-ai/dsh-subprocess-local': specifier: workspace:^ version: link:../../subprocess/subprocess-local - '@deepseek-ai/node-addon-landlock-run': + '@deepseek-ai/node-addon-system': specifier: workspace:^ - version: link:../../../native/landlock-run/packages/entry + version: link:../../../native/system/packages/entry packages/shell/pwsh-local: dependencies: @@ -9170,8 +9177,8 @@ importers: packages/subagent/subagent-claude-code: dependencies: '@anthropic-ai/claude-agent-sdk': - specifier: 0.3.241 - version: 0.3.241(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + specifier: 0.3.263 + version: 0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) '@anthropic-ai/sdk': specifier: 0.93.0 version: 0.93.0(zod@4.4.3) @@ -9252,8 +9259,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery '@openai/codex': - specifier: 0.149.1 - version: 0.149.1 + specifier: 0.153.4 + version: 0.153.4 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -11302,52 +11309,52 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.241': - resolution: {integrity: sha512-v26ta54lKFMFEZzbOE+6p3YhKERWnDiEA6OmkSAg+3fAQHOa1+aLTKw222cfgzxgiVixwFtHMk8c63zsDd8aXQ==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.263': + resolution: {integrity: sha512-H4eLd4Tkx3rJkt739CHb+9AcaKiiOpibU4tYsmma47mV+2zAPjUyFxpuE2N57VSmpAgbLQxu44du0TFN+UH5dg==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.241': - resolution: {integrity: sha512-5jweT0vft1ZCaGSoxZHF9vJlHbx8Yxx4+x5aHAIXTd4lx7ZbT4o5buEF8kpmTeHUB+Fw9jtFIm4QDsRiBXgf+Q==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.263': + resolution: {integrity: sha512-jKwmfkem1s/TcK7u83cJf2zLHMz846irV4vqYGUlo74uX2qfX57R4UsrCMlcTUMou/U9vQyNCc/Kk0s+zSxYRg==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.241': - resolution: {integrity: sha512-GslvPvSzehfCZyzOaJAt4lgodznm5zpl/LMXN8ygD12z5qnpM+I9/eFnmAaISJ0L8/vyohtlAP1jjaeR2jz1AQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.263': + resolution: {integrity: sha512-TiOmi9mqYvu3uGgM+0eQrUQzgXb7pgTzqahtRyEjpJrNigddGoANHcbNKL7P7y4etJpyIuYXGuVh5SLJzySYVA==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.241': - resolution: {integrity: sha512-SxszQGffXiLzMEnAv+pJXEmQbA8haijKyRjjH/jOt1CLeMIfpjKcO9WQDv8dEA8nREWS3zJ103zjgecAF7oOQQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.263': + resolution: {integrity: sha512-2KPY1wu1hdjpVsw+Tg3i50uhxhhetO1fB24wjUJAQJ8YPSogqO32UbtYPJ6I3F17lBNHb/Mq3nRYGAuSU5yjtg==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.241': - resolution: {integrity: sha512-kZigJ5Ug2I2G/n7Cunmwy4TGr0lOGnWrz6TkzyWiDcUmJOodoTH6GZECNarWAtETfN03AAeLfrpiz8z3hOEDqA==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.263': + resolution: {integrity: sha512-PWctt/XjebBxtI37i2xx2OMxSscPMnKmxU82MWdtIvoAyJ40U5yfqpm92fm45R02eLn9j4LQpvNocVTcRAHv+g==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.241': - resolution: {integrity: sha512-gJRa922Qcm7loumHcXMDFEFg//tz1aOi7Nx0sQa9I9lC1JSN8yL6i7/idzOU5Hp193tEDFOgqIMFL/yRiXg+rw==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.263': + resolution: {integrity: sha512-un7HJzTT+DSQLgM33emQ0qHAwIY3CXIwe8JXoD9PaT7pdLepK4Ffa4r5j84pHTVS5uoGqxcf196tBgqb5/Z7FA==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.241': - resolution: {integrity: sha512-/3yA9jQuCvHDVlILzhtslH6kFYOvydXyMZiKwnzqM8ZfvFTNO41w8TpiFpBLseyM+4A4E8QMeTKu3L01Xyb5IQ==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.263': + resolution: {integrity: sha512-n8owOwNSSi7/KpONb/ut+uXRjBIp9N6EjwADiB+YygPwGRtpWP+PR/qFsGdTVBE5cpPwhgKVAf27xEZEfNvcSQ==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.241': - resolution: {integrity: sha512-cHYdAgORl9kynujMeYXyV1uj/hbmsBjRw9dRVkIW4/4sF7S6L4u/qDSzn1/wiNP7g2yWSJ4KbsvHDH2WWDnCBQ==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.263': + resolution: {integrity: sha512-EwqzsOLxIJTX6RIXtQ21ekOKmYBNfwbtGtqaPqdWZGzpMbHaie8kGsM630h9RFaoxeytS/jbg8B3fPCdUKfhPw==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.241': - resolution: {integrity: sha512-pIHdCSTywFe30H0oWDCKZzC4ipBLtF5YMDRKjf6PHyARg57O4l/72v3b6QKnnefwtKKMe6uWJ1Y9lUJg/sKWyA==} + '@anthropic-ai/claude-agent-sdk@0.3.263': + resolution: {integrity: sha512-0QWoHgWWlSmgXfEqZRbVYRoXT9p4tx/yKaZ4eLJ8l1MR65SIvMGR+cd5ZXUpGYZpj9RzbYiYU9LoFg90mdIjmw==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -12799,43 +12806,43 @@ packages: resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} engines: {node: '>= 20'} - '@openai/codex@0.149.1': - resolution: {integrity: sha512-6q5pbcpFbJbqOpkubSDBwXmktQ55aD8eUzGzBF1zASob2DjwhBKDSNGtdZKalfrNJUdTDTPDMmzCXEXs5tMBYA==} + '@openai/codex@0.153.4': + resolution: {integrity: sha512-wbHDmit7S/YvBGVX1DQmk13xtWblZ2cApeJ/pB7xDZ10Cna+DZc5ij7f0F4OxdsXN4FW1oLT48OpogUI1+8Y2w==} engines: {node: '>=16'} hasBin: true - '@openai/codex@0.149.1-darwin-arm64': - resolution: {integrity: sha512-6X84kTCbnTgPIJ2EdcPsrvwS0Wxsqpa+bCswGmRf4BjhcQ5nPMnBC6yCAaCMj+vrbXQHj+L6sa9FaR4QkmA1qw==} + '@openai/codex@0.153.4-darwin-arm64': + resolution: {integrity: sha512-B1qhN3fa1ay0R0wGziXqgwSkB5icpYChNKHhtBHff/0UtSTC7z+l8aTtvMlGjH3E8HEvY3+njIJelM9CAAoVWg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@openai/codex@0.149.1-darwin-x64': - resolution: {integrity: sha512-MfLBQLfcElJL9tvj6y45qVHHMGSXCPnQOixuD3/Zq0g1BW/eFizkrGLdn48cFpc+l8cK+gt5nYG5pQYwVs6g4A==} + '@openai/codex@0.153.4-darwin-x64': + resolution: {integrity: sha512-vnSbbPzfoDZmmyzsxswsDDXQ06IVFBzkQU7/hroB3ji93Ok2utcsq8Psfk2tjF5r9mEx8RWFJhzuTGHG26/NDA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@openai/codex@0.149.1-linux-arm64': - resolution: {integrity: sha512-OqxUfZ1TVvHd18zHPKK/8ZRlpk8Vy11mg5CMHaLxNWldTbwVImDKtSLWT+m8m4NM5Sz4PbjtZMrVT/RfpBW/mQ==} + '@openai/codex@0.153.4-linux-arm64': + resolution: {integrity: sha512-QKdjYLYV4hXIuUQDP3P6F4NXuWFoKo9WUoV4nAREIx55kiUyi8UsYdsVobkeXir5n/maEQgYMCKLHVma4rNPiw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@openai/codex@0.149.1-linux-x64': - resolution: {integrity: sha512-Of5fGYgr7tAMsyj6vhXb4/RM/UoA3Zq8BLegUBDC09UNy1XTLGYP/2XD+UX8z3qh0NDwxYdCjFIWdDNijKZggQ==} + '@openai/codex@0.153.4-linux-x64': + resolution: {integrity: sha512-x1EcwBlY3AObM1VTUHNM2AzAJQsyreGdagpF+qFiYi/Oa30VBktvvG0C6tLtCzqW6hjZNWkGZQWmeVk7MuJKWg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@openai/codex@0.149.1-win32-arm64': - resolution: {integrity: sha512-5K0DmOKGK9Bos627p8sK8ATHjovPK0sDyT6h9Cb+4v+5CW5SGw1HLgjGxoLfJ8g3cg6mtg/pRCXXo2L/j71UVA==} + '@openai/codex@0.153.4-win32-arm64': + resolution: {integrity: sha512-/FBh42976ltF1kxDoPQBg1Q6+hwChRU5/sm5dfeC8kFVQMvOCGoGeY5d8rRZGVJE8XojlXo74VQb0sHowcfgBw==} engines: {node: '>=16'} cpu: [arm64] os: [win32] - '@openai/codex@0.149.1-win32-x64': - resolution: {integrity: sha512-G3QXGAg7nyyhqOeooAMUekBCeHd8a1QByhKcVAFyzNBaI06t6Ft7nsF+1SzFS0spuIdU4YyMi5YD26ukADBQUQ==} + '@openai/codex@0.153.4-win32-x64': + resolution: {integrity: sha512-lMkB43kJZH0VFr+hoXc11qqR7QtQIbkr07ALgj4urKL1osNyUyuy1iXd3Vzz2iCYvBUCSw7I0l/W1cEPGx9euQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -13873,9 +13880,6 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/fs-ext@2.0.3': - resolution: {integrity: sha512-0j2F+laosJF2NTd2DVheQ5GvXo8ln9L175VwLPfbsppE33iYC+6gn6XlOQS0pGvZm2yrQ32/LRZh0As/7rCs2Q==} - '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -15384,10 +15388,6 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-ext@2.1.1: - resolution: {integrity: sha512-/TrISPOFhCkbgIRWK9lzscRzwPCu0PqtCcvMc9jsHKBgZGoqA0VzhspVht5Zu8lxaXjIYIBWILHpRotYkCCcQA==} - engines: {node: '>= 8.0.0'} - fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -16387,9 +16387,6 @@ packages: multistream@4.1.0: resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} - nan@2.28.0: - resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -17195,13 +17192,13 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.1.1: + resolution: {integrity: sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==} + stat-mode@1.0.0: resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} engines: {node: '>= 6'} - standardwebhooks@1.1.1: - resolution: {integrity: sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -18071,44 +18068,44 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.2.4 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.241': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.241': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.241': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.241': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.241': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.241': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.241': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.241': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.241(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.241 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.241 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.241 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.241 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.241 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.241 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.241 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.241 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.263 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.263 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.263 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.263 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.263 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.263 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.263 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.263 '@anthropic-ai/sdk@0.123.0(zod@4.4.3)': dependencies: @@ -19537,31 +19534,31 @@ snapshots: '@octokit/request-error': 7.1.1 '@octokit/webhooks-methods': 6.0.0 - '@openai/codex@0.149.1': + '@openai/codex@0.153.4': optionalDependencies: - '@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64' - '@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64' - '@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64' - '@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64' - '@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64' - '@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64' + '@openai/codex-darwin-arm64': '@openai/codex@0.153.4-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.153.4-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.153.4-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.153.4-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.153.4-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.153.4-win32-x64' - '@openai/codex@0.149.1-darwin-arm64': + '@openai/codex@0.153.4-darwin-arm64': optional: true - '@openai/codex@0.149.1-darwin-x64': + '@openai/codex@0.153.4-darwin-x64': optional: true - '@openai/codex@0.149.1-linux-arm64': + '@openai/codex@0.153.4-linux-arm64': optional: true - '@openai/codex@0.149.1-linux-x64': + '@openai/codex@0.153.4-linux-x64': optional: true - '@openai/codex@0.149.1-win32-arm64': + '@openai/codex@0.153.4-win32-arm64': optional: true - '@openai/codex@0.149.1-win32-x64': + '@openai/codex@0.153.4-win32-x64': optional: true '@opentelemetry/api-logs@0.220.0': @@ -20408,10 +20405,6 @@ snapshots: '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 - '@types/fs-ext@2.0.3': - dependencies: - '@types/node': 22.20.0 - '@types/fs-extra@9.0.13': dependencies: '@types/node': 22.20.0 @@ -22179,10 +22172,6 @@ snapshots: fs-constants@1.0.0: {} - fs-ext@2.1.1: - dependencies: - nan: 2.28.0 - fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -23390,8 +23379,6 @@ snapshots: once: 1.4.0 readable-stream: 3.6.2 - nan@2.28.0: {} - nanoid@3.3.12: {} napi-build-utils@2.0.0: {} @@ -24315,13 +24302,13 @@ snapshots: stackback@0.0.2: {} - stat-mode@1.0.0: {} - standardwebhooks@1.1.1: dependencies: '@stablelib/base64': 1.0.1 fast-sha256: 1.3.0 + stat-mode@1.0.0: {} + statuses@2.0.2: {} std-env@4.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7cb1746e11..68eff9cd1c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,9 +2,9 @@ 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/* + # its native build and publication scripts under native/system. + - native/system + - native/system/packages/* # Product assemblies over the package tier; apps/cli owns the `dsh` bin. - apps/* # Private package owning repository-level benchmark dependencies. @@ -44,9 +44,6 @@ allowBuilds: node-addon-require-builtin: false # JSONL durability calls MoveFileExW with write-through publication on Windows. koffi: true - # The session write lock is flock(2) / LockFileEx; fs-ext compiles its - # binding with node-gyp at install. - fs-ext: true # The Python runtime deploy includes the reviewed workspace postinstall that # restores the executable bit on node-pty's macOS spawn helper. '@deepseek-ai/dsh-subprocess-local@file:packages/subprocess/subprocess-local': true @@ -73,18 +70,18 @@ minimumReleaseAgeExclude: - node-addon-require-builtin@0.1.4 # The active pnpm supply-chain policy blocks this reviewed runtime closure # until its release-age window expires unless every exact package is exempt. - - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.241' - - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.241' - - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.241' - - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.241' - - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.241' - - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.241' - - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.241' - - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.241' - - '@anthropic-ai/claude-agent-sdk@0.3.241' + - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.263' + - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.263' + - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.263' + - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.263' + - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.263' + - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.263' + - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.263' + - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.263' + - '@anthropic-ai/claude-agent-sdk@0.3.263' # All platform aliases resolve to the @openai/codex package name, so their # reviewed exact versions must share one version-union policy entry. - - '@openai/codex@0.149.1||0.149.1-darwin-arm64||0.149.1-darwin-x64||0.149.1-linux-arm64||0.149.1-linux-x64||0.149.1-win32-arm64||0.149.1-win32-x64' + - '@openai/codex@0.153.4||0.153.4-darwin-arm64||0.153.4-darwin-x64||0.153.4-linux-arm64||0.153.4-linux-x64||0.153.4-win32-arm64||0.153.4-win32-x64' patchedDependencies: '@yao-pkg/pkg@6.21.0': patches/@yao-pkg__pkg@6.21.0.patch diff --git a/scripts/benchmark-npm-resolution.ts b/scripts/benchmark-npm-resolution.ts index b8089117ad..8ee0d7fa92 100644 --- a/scripts/benchmark-npm-resolution.ts +++ b/scripts/benchmark-npm-resolution.ts @@ -16,8 +16,8 @@ const WORKSPACE_MANIFEST_GLOBS = [ 'apps/*/package.json', 'packages/*/*/package.json', 'vendor/*/package.json', - 'native/landlock-run/package.json', - 'native/landlock-run/packages/*/package.json', + 'native/system/package.json', + 'native/system/packages/*/package.json', ] const INSTALLED_MANIFEST_GLOBS = [ 'node_modules/.pnpm/*/node_modules/*/package.json', @@ -131,7 +131,7 @@ export function parseBenchmarkOptions(args: readonly string[]): BenchmarkOptions } function workspaceManifestPath(path: string): boolean { - return /^(?:apps\/[^/]+|packages\/[^/]+\/[^/]+|vendor\/[^/]+|native\/landlock-run(?:\/packages\/[^/]+)?)\/package\.json$/.test(path) + return /^(?:apps\/[^/]+|packages\/[^/]+\/[^/]+|vendor\/[^/]+|native\/system(?:\/packages\/[^/]+)?)\/package\.json$/.test(path) } function workspaceManifestPaths(root: string, ref: string | undefined): string[] { diff --git a/scripts/build.ts b/scripts/build.ts index 6d6bcb259f..e64fd1486e 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -41,6 +41,7 @@ function main(): void { const buildEnvironment = clientBuildProcessEnvironment(process.env, clientEnvironment) rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true }) + runScript('build:native-system', buildEnvironment) runScript('build:lib', buildEnvironment) runScript('build:web', buildEnvironment) const record = writeClientBuildRecord(root, clientEnvironment) diff --git a/scripts/check-workspace-constraints.spec.ts b/scripts/check-workspace-constraints.spec.ts index 3b1475d68c..5ac40d3c99 100644 --- a/scripts/check-workspace-constraints.spec.ts +++ b/scripts/check-workspace-constraints.spec.ts @@ -102,7 +102,7 @@ describe('dsh family version coherence', () => { it('leaves other sequences to their own version lines', () => { expect(checkDshFamilyVersion({ name: '@deepseek-ai/cordis', version: '4.0.1' }, '0.1.2-rc.1')).toBeUndefined() expect(checkDshFamilyVersion( - { name: '@deepseek-ai/node-addon-landlock-run', version: '0.1.1' }, + { name: '@deepseek-ai/node-addon-system', version: '0.1.1' }, '0.1.2-rc.1', )).toBeUndefined() expect(checkDshFamilyVersion({ version: '0.1.2-alpha.5' }, '0.1.2-rc.1')).toBeUndefined() diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 1620e29b03..e7b0e948e8 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -18,7 +18,7 @@ const workspaceGlobs = [ { dir: 'vendor', depth: 1 }, { dir: 'packages', depth: 2 }, { dir: 'native', depth: 1 }, - { dir: 'native/landlock-run/packages', depth: 1 }, + { dir: 'native/system/packages', depth: 1 }, { dir: 'apps', depth: 1 }, ] as const const vendoredPackages = new Set([ @@ -32,14 +32,16 @@ const vendoredPackages = new Set([ '@deepseek-ai/cordis-plugin-hmr', '@deepseek-ai/cordis-plugin-logger-console', ]) -const publicLandlockPackages = new Set([ - '@deepseek-ai/node-addon-landlock-run', - '@deepseek-ai/node-addon-landlock-run-linux-arm64', - '@deepseek-ai/node-addon-landlock-run-linux-x64', +const publicNativePackages = new Set([ + '@deepseek-ai/node-addon-system', + '@deepseek-ai/node-addon-system-darwin-arm64', + '@deepseek-ai/node-addon-system-darwin-x64', + '@deepseek-ai/node-addon-system-linux-arm64', + '@deepseek-ai/node-addon-system-linux-x64', ]) /** Deliberate source payloads whose exact bytes are part of the package's audit surface. */ const publicationSourceAllowlist: Readonly> = { - '@deepseek-ai/node-addon-landlock-run': ['src/main.c'], + '@deepseek-ai/node-addon-system': ['src/main.c', 'src/flock.c'], } const repositoryUrl = 'git+https://github.com/deepseek-harness/deepseek-harness.git' /** @@ -114,8 +116,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 +const nativeWorkspaceManifest = readJson(join(root, 'native/system/package.json')) +const nativeVersion = nativeWorkspaceManifest.version /** Repo-relative dirs holding a package.json, walked to the configured depth. */ function packageDirs(base: string, depth: number): string[] { @@ -310,12 +312,12 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st const label = manifest.name ?? dir const familyVersionError = checkDshFamilyVersion(manifest, repositoryVersion) if (familyVersionError !== undefined) errors.push(familyVersionError) - const isLandlockPackageDir = dir.startsWith('native/landlock-run/packages/') - const isPublicLandlockPackage = isLandlockPackageDir + const isNativePackageDir = dir.startsWith('native/system/packages/') + const isPublicNativePackage = isNativePackageDir && manifest.name !== undefined - && publicLandlockPackages.has(manifest.name) + && publicNativePackages.has(manifest.name) - if (isPublicLandlockPackage) { + if (isPublicNativePackage) { if (manifest.private === true) { errors.push(`${label}: published Landlock package must not set "private": true`) } @@ -375,12 +377,12 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st } } - if (isLandlockPackageDir) { - if (!isPublicLandlockPackage) { + if (isNativePackageDir) { + if (!isPublicNativePackage) { 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 (manifest.version !== nativeVersion) { + errors.push(`${label}: package.json version must match native workspace version ${nativeVersion ?? '(missing)'}`) } } diff --git a/scripts/clean.spec.ts b/scripts/clean.spec.ts index bd0c3be9af..92ebac7735 100644 --- a/scripts/clean.spec.ts +++ b/scripts/clean.spec.ts @@ -64,16 +64,16 @@ describe('RepositoryCleaner', () => { it('removes the native Landlock entry output and solution build info', async () => { const root = fixture() - const entry = 'native/landlock-run/packages/entry' + const entry = 'native/system/packages/entry' addProject(root, entry, 'lib') write(join(root, entry, 'lib/index.js')) - write(join(root, 'native/landlock-run/tsconfig.tsbuildinfo')) + write(join(root, 'native/system/tsconfig.tsbuildinfo')) await new RepositoryCleaner(root).clean() expect(existsSync(join(root, entry, 'lib'))).toBe(false) expect(existsSync(join(root, entry, 'src/index.ts'))).toBe(true) - expect(existsSync(join(root, 'native/landlock-run/tsconfig.tsbuildinfo'))).toBe(false) + expect(existsSync(join(root, 'native/system/tsconfig.tsbuildinfo'))).toBe(false) }) it('refuses project outputs reached through a symlink outside the repository', async () => { diff --git a/scripts/clean.ts b/scripts/clean.ts index be13a95026..bb789f298c 100644 --- a/scripts/clean.ts +++ b/scripts/clean.ts @@ -77,7 +77,7 @@ export class RepositoryCleaner { } await this.addIfPresent( targets, - join(this.root, 'native/landlock-run/tsconfig.tsbuildinfo'), + join(this.root, 'native/system/tsconfig.tsbuildinfo'), canonicalRoot, ) @@ -122,7 +122,7 @@ export class RepositoryCleaner { const outputs = new Set() const pending = [join(this.root, 'tsconfig.json')] const visited = new Set() - const nativeEntryOutput = join(this.root, 'native/landlock-run/packages/entry/lib') + const nativeEntryOutput = join(this.root, 'native/system/packages/entry/lib') while (pending.length > 0) { const nextConfigPath = pending.pop() diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 33d8661aed..7bf3f88b18 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -351,12 +351,12 @@ describe('official Claude distribution authorization', () => { describe('manifestPatterns', () => { it('derives globs from the declared members, so a new member area is read', () => { - expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([ + expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/system', 'native/system/packages/*'])).toEqual([ 'package.json', 'packages/*/*/package.json', 'tools/*/package.json', - 'native/landlock-run/package.json', - 'native/landlock-run/packages/*/package.json', + 'native/system/package.json', + 'native/system/packages/*/package.json', ]) }) }) diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index abbd3c6b3b..b9bf11e182 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -40,9 +40,11 @@ const DEV_ONLY_AREAS = [ /** First-party public native packages: reachable at runtime but not third-party. */ const FIRST_PARTY = new Set([ - '@deepseek-ai/node-addon-landlock-run', - '@deepseek-ai/node-addon-landlock-run-linux-arm64', - '@deepseek-ai/node-addon-landlock-run-linux-x64', + '@deepseek-ai/node-addon-system', + '@deepseek-ai/node-addon-system-darwin-arm64', + '@deepseek-ai/node-addon-system-darwin-x64', + '@deepseek-ai/node-addon-system-linux-arm64', + '@deepseek-ai/node-addon-system-linux-x64', ]) /** Official SDK identity covered by the project's narrow owner authorization. */ @@ -75,7 +77,6 @@ const OVERRIDES: Record = { // No repository field in the published manifest. 'node-addon-require-builtin': { repo: 'https://www.npmjs.com/package/node-addon-require-builtin' }, // No `license` field in the published manifest; the tarball's LICENSE.txt is the MIT text. - 'fs-ext': { license: 'MIT' }, } /** @@ -295,7 +296,7 @@ function installedManifest(name: string, manifests: Map, expec let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined // Workspace-local link farms can expose a dependency that is not linked at // the repository root; both are backed by the root workspace's lockfile. - for (const store of ['node_modules', 'native/landlock-run/node_modules']) { + for (const store of ['node_modules', 'native/system/node_modules']) { const direct = resolve(root, store, name, 'package.json') if (existsSync(direct)) { const candidate = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest @@ -747,7 +748,7 @@ ${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.r ## First-party native packages -\`@deepseek-ai/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. +\`@deepseek-ai/node-addon-system\` (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. ` } diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index eec211e7da..31f6bc2aff 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -160,6 +160,27 @@ describe('gate graph validation', () => { await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length) }) + it('builds the native addon before benchmarks through the ci-bench script chain', () => { + const subject = withPnpmEntrypoint(() => gatesForMode('ci-bench')) + const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { + scripts: Record + } + + expect(scripts['check:ci:bench']).toBe('tsx scripts/run-gates.ts ci-bench') + expect(subject).toHaveLength(1) + expect(subject[0]).toMatchObject({ + id: 'bench', + displayCommand: 'pnpm run test:bench', + args: ['/private/pnpm.cjs', 'run', 'test:bench'], + }) + expect(scripts['test:bench']).toBe('npm run build:bench && npm run build:web && npm run test:bench:built') + expect(scripts['build:bench']).toBe( + 'npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts', + ) + expect(scripts['build:native-system']).toBe('tsx native/system/scripts/build.ts --host-addon-only') + expect(scripts['test:bench:built']).toBe('vitest run --config vitest.bench.config.ts') + }) + it('keeps the public repository link policy in the documentation gate', () => { const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index d8f82f33d5..9276b51c77 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -631,7 +631,8 @@ function coverageGates(): Gate[] { streamOutput: true, }) return [ - instrumented, + pnpmScript('native-system', 'build:native-system'), + { ...instrumented, needs: ['native-system'] }, pnpmExec('coverage-exempt-heavy', [ 'vitest', 'run', @@ -640,6 +641,7 @@ function coverageGates(): Gate[] { ...timeouts, ], { label: 'test:coverage-exempt-heavy', + needs: ['native-system'], }), ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index 1871e71ca8..20d789c2a1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,7 +37,8 @@ "@deepseek-ai/cordis-plugin-timer": ["./vendor/timer/src"], "@deepseek-ai/cordis-plugin-hmr": ["./vendor/hmr/src"], "@deepseek-ai/cordis-plugin-logger-console": ["./vendor/logger-console/src"], - "@deepseek-ai/node-addon-landlock-run": ["./native/landlock-run/packages/entry/src/index.ts"], + "@deepseek-ai/node-addon-system/landlock-run": ["./native/system/packages/entry/src/index.ts"], + "@deepseek-ai/node-addon-system/flock": ["./native/system/packages/entry/src/flock.ts"], "@deepseek-ai/dsh-invariants": ["./packages/runtime-diagnostics/invariants/src/index.ts"], "@deepseek-ai/dsh-tool-call-timeout-policy": ["./packages/guard/timeout-policy/src"], "@deepseek-ai/dsh-spill-policy/notice": ["./packages/spill/spill-policy/src/notice.ts"], diff --git a/tsconfig.host.json b/tsconfig.host.json index a9a017eae9..55f065c7a7 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -253,7 +253,7 @@ { "path": "./packages/shell/pwsh-local" }, { "path": "./packages/shell/pwsh-sandbox" }, { "path": "./packages/shell/tool-pwsh" }, - { "path": "./native/landlock-run/packages/entry" }, + { "path": "./native/system/packages/entry" }, { "path": "./packages/sandbox/sandbox" }, { "path": "./packages/sandbox/sandbox-local" }, { "path": "./packages/sandbox/sandbox-policy" },