diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 005bc22132..12b6425f1e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1037,7 +1037,7 @@ export interface Config { } ``` -Source: [`packages/sandbox/sandbox-local/src/index.ts:25`](../packages/sandbox/sandbox-local/src/index.ts) +Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts) ## `@deepseek-ai/dsh-sandbox-policy` diff --git a/examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts b/examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts index 791addaf2d..2ce25885f0 100644 --- a/examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts +++ b/examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts @@ -1,10 +1,7 @@ import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import { SandboxProvider } from '@deepseek-ai/dsh-sandbox' -import { - LAUNCHER_FAILURE_EXIT, - LAUNCHER_FATAL_PREFIX, - PARTIAL_ENFORCEMENT_NOTICE, -} from 'node-addon-landlock-run' + +const NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)' /** Snapshot-only provider that reproduces an older-ABI Landlock launch. */ export default class PartialLandlockSandboxProvider extends SandboxProvider { @@ -13,16 +10,16 @@ export default class PartialLandlockSandboxProvider extends SandboxProvider { argv: [ 'bash', '-c', - `printf '%s\\n' '${PARTIAL_ENFORCEMENT_NOTICE}' >&2; exec "$@"`, + `printf '%s\\n' '${NOTICE}' >&2; exec "$@"`, 'partial-landlock-run', ...argv, ], enforcement: 'partial', denialSignatures: ['permission denied'], runnerFailureRules: [{ - allowedExitCodes: [LAUNCHER_FAILURE_EXIT], - fatalSignatures: [LAUNCHER_FATAL_PREFIX], - informationalLines: [PARTIAL_ENFORCEMENT_NOTICE], + allowedExitCodes: [125], + fatalSignatures: ['landlock-run: '], + informationalLines: [NOTICE], }], } } diff --git a/examples/package.json b/examples/package.json index ce5d4142e6..99d57bf4a6 100644 --- a/examples/package.json +++ b/examples/package.json @@ -100,7 +100,6 @@ "@deepseek-ai/dsh-web": "workspace:*", "@deepseek-ai/dsh-web-fetch-local": "workspace:*", "@deepseek-ai/dsh-workflow-workerthread": "workspace:*", - "@deepseek-ai/dsh-workspace-context": "workspace:*", - "node-addon-landlock-run": "0.0.0-test.0" + "@deepseek-ai/dsh-workspace-context": "workspace:*" } } diff --git a/native/landlock-run/README.i18n.yaml b/native/landlock-run/README.i18n.yaml index 5397c43e4f..e7eb1fb137 100644 --- a/native/landlock-run/README.i18n.yaml +++ b/native/landlock-run/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/landlock-run/README.md -README.md: 483b14df7cb91fc6486f69efc1ddc560ab635ed5 -README.zh.md: a239889ec68a8d56bb901b17feaac7f7acbf66fa +README.md: 19cc18830b90609f648cfb2ce1ee509ad9fe381b +README.zh.md: c93c33b843e3afc15b1890ce14edf436e051592c diff --git a/native/landlock-run/README.md b/native/landlock-run/README.md index 483b14df7c..19cc18830b 100644 --- a/native/landlock-run/README.md +++ b/native/landlock-run/README.md @@ -39,7 +39,7 @@ 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`, `LAUNCHER_FAILURE_EXIT` (125), `LAUNCHER_FATAL_PREFIX`, and `PARTIAL_ENFORCEMENT_NOTICE`: contract constants. Every launcher failure uses 125, but a successfully exec'd child may also return 125, so failure attribution requires the fatal prefix too. +- `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). diff --git a/native/landlock-run/README.zh.md b/native/landlock-run/README.zh.md index a239889ec6..c93c33b843 100644 --- a/native/landlock-run/README.zh.md +++ b/native/landlock-run/README.zh.md @@ -39,7 +39,7 @@ if (probe(launcher) !== 'unusable') { - `launcherPath()`:当前宿主启动器的绝对路径(有意不检查是否存在;探测结果才是可用性信号)。 - `probe(launcher?, { timeoutMs? })`:功能性强制执行探测,返回 `'full' | 'partial' | 'unusable'`。 - `grantArgs({ readOnly?, readWrite? })`:启动器的授权 argv;未授予的一切都被拒绝。 -- `LAUNCHER_BIN`、`LAUNCHER_FAILURE_EXIT`(125)、`LAUNCHER_FATAL_PREFIX` 和 `PARTIAL_ENFORCEMENT_NOTICE`:契约常量。所有 launcher 失败的退出码都是 125,但成功完成 exec 的子进程也可能返回 125,因此还必须有致命前缀,才能将结果归因为 launcher 失败。 +- `LAUNCHER_BIN` 和 `LAUNCHER_FAILURE_EXIT`(125):契约常量。成功完成 exec 的子进程也可能返回 125,因此消费者必须同时看到致命诊断和该状态,才能将结果归因为 launcher 失败。 完整的二进制契约(argv 语法、退出码、报告行)锁定在 [docs/cli-contract.md](docs/cli-contract.md) 中。 diff --git a/native/landlock-run/docs/cli-contract.md b/native/landlock-run/docs/cli-contract.md index b5422d1688..ad9a002d8f 100644 --- a/native/landlock-run/docs/cli-contract.md +++ b/native/landlock-run/docs/cli-contract.md @@ -1,6 +1,6 @@ # CLI contract: landlock-run -This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs` and its protocol constants); changing anything below requires a version bump for the whole package family and a note in the release notes. +This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it through the entry package (`launcherPath`/`probe`/`grantArgs`) and the launcher protocol; changing anything below requires a version bump for the whole package family and a note in the release notes. ## Invocation grammar @@ -20,14 +20,14 @@ landlock-run --probe ## Exit codes - `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run. -- After a successful `exec`, every child status is passed through unchanged, including 125. Consumers therefore require both status 125 and a `LAUNCHER_FATAL_PREFIX` line to attribute launcher failure. +- After a successful `exec`, every child status is passed through unchanged, including 125. Consumers therefore require both status 125 and a `landlock-run: ` fatal line to attribute launcher failure. - `--probe`: `0` when the kernel enforces (fully or partially), `125` otherwise. ## Report lines - Probe success prints exactly one stdout line: `landlock: fully enforced` or `landlock: partially enforced (older ABI)`. The entry package's `probe()` maps these to `full`/`partial`; a non-zero probe exit maps to `unusable`. -- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` (`PARTIAL_ENFORCEMENT_NOTICE`) and proceeds — still confined for everything the kernel supports. -- Every fatal error prints one stderr line prefixed `landlock-run: ` (`LAUNCHER_FATAL_PREFIX`) before exiting `125`. +- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` and proceeds — still confined for everything the kernel supports. +- Every fatal error prints one stderr line prefixed `landlock-run: ` before exiting `125`. ## Confinement semantics diff --git a/native/landlock-run/packages/entry/src/index.ts b/native/landlock-run/packages/entry/src/index.ts index 8cbd845e2a..7a4349a5ca 100644 --- a/native/landlock-run/packages/entry/src/index.ts +++ b/native/landlock-run/packages/entry/src/index.ts @@ -21,18 +21,12 @@ import { fileURLToPath } from 'node:url' /** The launcher binary's file name inside each platform package's `bin/`. */ export const LAUNCHER_BIN = 'landlock-run' -/** Prefix on every launcher-owned fatal stderr line. */ -export const LAUNCHER_FATAL_PREFIX = 'landlock-run: ' - -/** Informational stderr line emitted before child execution under partial enforcement. */ -export const PARTIAL_ENFORCEMENT_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)' - /** * The exit code for every launcher-level failure (usage error, unenforcing * kernel, unopenable grant root, failed exec). After a successful `exec`, the - * wrapped command may also return 125, so consumers require a matching - * {@link LAUNCHER_FATAL_PREFIX} diagnostic to attribute launcher failure. - * Part of the CLI contract. + * wrapped command may also return 125, so consumers also require a matching + * launcher-owned fatal diagnostic to attribute launcher failure. Part of the + * CLI contract. */ export const LAUNCHER_FAILURE_EXIT = 125 diff --git a/native/landlock-run/test/entry.test.js b/native/landlock-run/test/entry.test.js index ea006b649c..2e2cfe8f17 100644 --- a/native/landlock-run/test/entry.test.js +++ b/native/landlock-run/test/entry.test.js @@ -12,8 +12,6 @@ import path from 'node:path'; import { LAUNCHER_BIN, LAUNCHER_FAILURE_EXIT, - LAUNCHER_FATAL_PREFIX, - PARTIAL_ENFORCEMENT_NOTICE, grantArgs, launcherPath, probe, @@ -22,8 +20,6 @@ import { // --- constants are part of the CLI contract --- assert.equal(LAUNCHER_BIN, 'landlock-run'); assert.equal(LAUNCHER_FAILURE_EXIT, 125); -assert.equal(LAUNCHER_FATAL_PREFIX, 'landlock-run: '); -assert.equal(PARTIAL_ENFORCEMENT_NOTICE, 'landlock-run: partial enforcement (older Landlock ABI)'); // --- grantArgs: flag spelling, ordering, and empty grants --- assert.deepEqual(grantArgs({}), []); diff --git a/native/landlock-run/test/launcher.test.js b/native/landlock-run/test/launcher.test.js index b6ca14ab6c..55385d2156 100644 --- a/native/landlock-run/test/launcher.test.js +++ b/native/landlock-run/test/launcher.test.js @@ -19,13 +19,13 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { LAUNCHER_FAILURE_EXIT, - LAUNCHER_FATAL_PREFIX, - PARTIAL_ENFORCEMENT_NOTICE, grantArgs, launcherPath, probe, } from 'node-addon-landlock-run'; +const FATAL_PREFIX = 'landlock-run: '; +const PARTIAL_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'; const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1'; if (process.platform !== 'linux') { @@ -45,7 +45,7 @@ const run = (args, options = {}) => spawnSync(launcher, args, { encoding: 'utf8' { const noCommand = run([]); assert.equal(noCommand.status, LAUNCHER_FAILURE_EXIT); - assert.ok(noCommand.stderr.startsWith(LAUNCHER_FATAL_PREFIX)); + assert.ok(noCommand.stderr.startsWith(FATAL_PREFIX)); assert.match(noCommand.stderr, /usage error: missing `-- \.\.\.` command/); const unknownFlag = run(['--bogus', '--', 'true']); @@ -78,7 +78,7 @@ if (enforcement === 'unusable') { console.log('launcher.test: SKIP enforcement half — kernel does not enforce Landlock'); process.exit(0); } -const expectedNotice = enforcement === 'partial' ? `${PARTIAL_ENFORCEMENT_NOTICE}\n` : ''; +const expectedNotice = enforcement === 'partial' ? `${PARTIAL_NOTICE}\n` : ''; { const probeRun = run(['--probe']); assert.equal(probeRun.status, 0); @@ -129,7 +129,7 @@ const expectedNotice = enforcement === 'partial' ? `${PARTIAL_ENFORCEMENT_NOTICE const marker = path.join(os.tmpdir(), `nalr-should-not-exist-${process.pid}`); const badGrant = run(['--ro', '/no/such/grant/root', '--', '/bin/sh', '-c', `echo x > ${marker}`]); assert.equal(badGrant.status, LAUNCHER_FAILURE_EXIT); - assert.ok(badGrant.stderr.startsWith(LAUNCHER_FATAL_PREFIX)); + assert.ok(badGrant.stderr.startsWith(FATAL_PREFIX)); assert.match(badGrant.stderr, /cannot open rule path/); assert.ok(!fs.existsSync(marker), 'the command must never run when the launcher fails'); } diff --git a/packages/bash/bash-sandbox/tests/partial-landlock.spec.ts b/packages/bash/bash-sandbox/tests/partial-landlock.spec.ts index d3f124151e..1acca21825 100644 --- a/packages/bash/bash-sandbox/tests/partial-landlock.spec.ts +++ b/packages/bash/bash-sandbox/tests/partial-landlock.spec.ts @@ -9,18 +9,16 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from 'cordis' -import { - LAUNCHER_FAILURE_EXIT, - LAUNCHER_FATAL_PREFIX, - PARTIAL_ENFORCEMENT_NOTICE, -} from 'node-addon-landlock-run' +import { LAUNCHER_FAILURE_EXIT } from 'node-addon-landlock-run' import { SANDBOX_UNAVAILABLE } from '@deepseek-ai/dsh-sandbox' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local' -const FATAL = `${LAUNCHER_FATAL_PREFIX}landlock ruleset error: Invalid argument` +const NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)' +const FATAL_PREFIX = 'landlock-run: ' +const FATAL = `${FATAL_PREFIX}landlock ruleset error: Invalid argument` const contexts: Context[] = [] const tempDirs: string[] = [] @@ -41,10 +39,10 @@ while [ "$#" -gt 0 ]; do case "$1" in --ro|--rw) shift 2 ;; --) shift; break ;; - *) printf '%s\\n' '${LAUNCHER_FATAL_PREFIX}usage error: unexpected fake argument' >&2; exit ${LAUNCHER_FAILURE_EXIT} ;; + *) printf '%s\\n' '${FATAL_PREFIX}usage error: unexpected fake argument' >&2; exit ${LAUNCHER_FAILURE_EXIT} ;; esac done -printf '%s\\n' '${PARTIAL_ENFORCEMENT_NOTICE}' >&2 +printf '%s\\n' '${NOTICE}' >&2 ${fatalBranch}exec "$@" `, { mode: 0o755 }) return launcher @@ -113,7 +111,7 @@ describe('partial Landlock runner-failure classification', () => { const bash = await setup() const result = await bash.run(bash.resolve({ command: `exit ${exitCode}` })) expect(result.exitCode).toBe(exitCode) - expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n`) + expect(result.stderr.text).toBe(`${NOTICE}\n`) expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' }) }, ) @@ -122,7 +120,7 @@ describe('partial Landlock runner-failure classification', () => { const bash = await setup() const result = await bash.run(bash.resolve({ command: `exit ${exitCode}` })) expect(result.exitCode).toBe(exitCode) - expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n`) + expect(result.stderr.text).toBe(`${NOTICE}\n`) expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' }) }) @@ -130,7 +128,7 @@ describe('partial Landlock runner-failure classification', () => { const bash = await setup(exitCode) const result = await bash.run(bash.resolve({ command: 'true' })) expect(result.exitCode).toBe(exitCode) - expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n${FATAL}\n`) + expect(result.stderr.text).toBe(`${NOTICE}\n${FATAL}\n`) expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' }) }) @@ -140,13 +138,13 @@ describe('partial Landlock runner-failure classification', () => { expect(error).toMatchObject({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE }) expect(error).toBeInstanceOf(Error) expect((error as Error).message).toContain(`Runner failure: ${FATAL}`) - expect((error as Error).message).not.toContain(PARTIAL_ENFORCEMENT_NOTICE) + expect((error as Error).message).not.toContain(NOTICE) }) it('classifies a notice plus child Permission denied as a denial, not runner failure', async () => { const bash = await setup() const result = await bash.run(bash.resolve({ command: 'printf "%s\\n" "child: Permission denied" >&2; exit 1' })) - expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\nchild: Permission denied\n`) + expect(result.stderr.text).toBe(`${NOTICE}\nchild: Permission denied\n`) expect(result.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' }) }) @@ -156,7 +154,7 @@ describe('partial Landlock runner-failure classification', () => { const task = bash.start(bash.resolve({ command })) await task.done expect(task.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' }) - expect(task.readOutput().delta).toContain(PARTIAL_ENFORCEMENT_NOTICE) + expect(task.readOutput().delta).toContain(NOTICE) } }) @@ -165,7 +163,7 @@ describe('partial Landlock runner-failure classification', () => { const task = bash.start(bash.resolve({ command: 'printf "%s\\n" "child: Permission denied" >&2; exit 1' })) await task.done expect(task.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' }) - expect(task.readOutput().delta).toContain(PARTIAL_ENFORCEMENT_NOTICE) + expect(task.readOutput().delta).toContain(NOTICE) }) it('makes a background fatal line outrank denial text after the notice', async () => { @@ -179,7 +177,7 @@ describe('partial Landlock runner-failure classification', () => { runnerFailed: true, }) const output = task.readOutput().delta - expect(output).toContain(PARTIAL_ENFORCEMENT_NOTICE) + expect(output).toContain(NOTICE) expect(output).toContain(FATAL) }) }) diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index 719bdca01f..bb0e5a56c1 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -8,9 +8,8 @@ import { spawnSync } from 'node:child_process' import { - LAUNCHER_FATAL_PREFIX, + LAUNCHER_BIN, LAUNCHER_FAILURE_EXIT, - PARTIAL_ENFORCEMENT_NOTICE, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock, } from 'node-addon-landlock-run' @@ -157,8 +156,8 @@ const RUNNER_FAILURE_RULES = { bwrap: [{ fatalSignatures: ['bwrap: '] }], landlock: [{ allowedExitCodes: [LAUNCHER_FAILURE_EXIT], - fatalSignatures: [LAUNCHER_FATAL_PREFIX], - informationalLines: [PARTIAL_ENFORCEMENT_NOTICE], + fatalSignatures: [`${LAUNCHER_BIN}: `], + informationalLines: [`${LAUNCHER_BIN}: partial enforcement (older Landlock ABI)`], }], seatbelt: [{ fatalSignatures: ['sandbox-exec: '] }], } as const satisfies Record diff --git a/packages/sandbox/sandbox-local/tests/local.spec.ts b/packages/sandbox/sandbox-local/tests/local.spec.ts index 13f860a94a..74d4c2a8a1 100644 --- a/packages/sandbox/sandbox-local/tests/local.spec.ts +++ b/packages/sandbox/sandbox-local/tests/local.spec.ts @@ -12,11 +12,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' -import { - LAUNCHER_FAILURE_EXIT, - LAUNCHER_FATAL_PREFIX, - PARTIAL_ENFORCEMENT_NOTICE, -} from 'node-addon-landlock-run' +import { LAUNCHER_FAILURE_EXIT } from 'node-addon-landlock-run' import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import { @@ -179,8 +175,8 @@ describe('the platform chains', () => { denialSignatures: ['permission denied'], runnerFailureRules: [{ allowedExitCodes: [LAUNCHER_FAILURE_EXIT], - fatalSignatures: [LAUNCHER_FATAL_PREFIX], - informationalLines: [PARTIAL_ENFORCEMENT_NOTICE], + fatalSignatures: ['landlock-run: '], + informationalLines: ['landlock-run: partial enforcement (older Landlock ABI)'], }], }) expect(probeLandlock).toHaveBeenCalledWith(launcher) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 252e5b3637..d98c864d49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -848,10 +848,6 @@ importers: '@deepseek-ai/dsh-workspace-context': specifier: workspace:* version: link:../packages/context/workspace-context - node-addon-landlock-run: - specifier: 0.0.0-test.0 - version: 0.0.0-test.0 - packages/acp/acp: dependencies: '@agentclientprotocol/sdk':