Просмотр исходного кода

fix(sandbox): resolve source preload independently of command cwd

Tianyi Cui 1 месяц назад
Родитель
Сommit
5bebc7c504

+ 2 - 2
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: 33853b68407c2d3294ad1323e47b5473419b0e12
-README.zh.md: 6abef684c38d3c60caa59d06eb3ac721a2e531fe
+README.md: ab6182ffe86b658c59eebcffbc479097ed1cb25b
+README.zh.md: ca1e4142f10bbdc44ccfc8f143617a5d9b58fcd3

+ 2 - 0
packages/sandbox/sandbox-local/README.md

@@ -80,6 +80,8 @@ The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow
 
 The Windows rung keeps one deterministic write SID and standing ACE per workspace, while every live session/workspace pair gets a random private temp directory with a distinct SID and revocable ACE — sessions sharing a workspace share its intended write authority without inheriting one another's temp authority. A fresh provider always chooses a new temp path and SID, so crash residue cannot block or authorize a resumed session. The rung reports `partial` enforcement because the restricted token must retain Everyone and NTFS hard links alias one file object across paths.
 
+When the built ACL runner is absent, source launch uses the `tsx/esm` module URL resolved from this installation. The command's working directory does not need a Harness checkout or its own loader dependency.
+
 ### Denial and runner-failure dialects
 
 Each runner's kernel speaks its own denial dialect, carried on every wrap as `denialSignatures`, and `runnerFailureRules` give each runner's fatal signature, so consumers classify a runner refusal before checking denial signatures. The exact strings and exit codes live in [`src/index.ts`](src/index.ts).

+ 2 - 0
packages/sandbox/sandbox-local/README.zh.md

@@ -80,6 +80,8 @@ Seatbelt profile 默认允许,带 `(deny file-write*)` 与来自共享 `writab
 
 Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,同时为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可撤销 ACE——共享工作区的会话共享其预期写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权。该档报告 `partial` 强制执行,因为受限令牌必须保留 Everyone,且 NTFS 硬链接会把同一文件对象别名为多个路径。
 
+构建后的 ACL runner 缺失时,源码启动使用从本安装目录解析出的 `tsx/esm` 模块 URL。命令的工作目录不需要包含 Harness 源码检出或独立的加载器依赖。
+
 ### 拒绝与 runner 失败方言
 
 每个 runner 的内核都有自己的拒绝方言,随每次包装以 `denialSignatures` 携带,`runnerFailureRules` 则给出每个 runner 的致命签名,因此消费方先分类 runner 拒绝,再检查拒绝签名。精确的字符串与退出码位于 [`src/index.ts`](src/index.ts)。

+ 2 - 1
packages/sandbox/sandbox-local/src/index.ts

@@ -551,6 +551,7 @@ export class LocalSandboxProvider extends SandboxProvider {
   /**
    * The windows-acl runner argv prefix: the built lib/runner.js entry when
    * present (production), else the package source through tsx (development).
+   * Resolve the source preload in this installation, independently of target cwd.
    * The prefix stays `[node, runner, ...]` — a future native-exe runner keeps
    * the same argv contract and only swaps these entries.
    */
@@ -560,7 +561,7 @@ export class LocalSandboxProvider extends SandboxProvider {
     const builtEntry = this.internals.windowsAclRunnerEntry ?? fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-sandbox-windows-acl/runner'))
     if (existsSync(builtEntry)) return [process.execPath, builtEntry]
     const sourceEntry = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-sandbox-windows-acl/src/runner.ts'))
-    return [process.execPath, '--import', 'tsx/esm', sourceEntry]
+    return [process.execPath, '--import', import.meta.resolve('tsx/esm'), sourceEntry]
   }
 }
 

+ 22 - 1
packages/sandbox/sandbox-local/tests/local.spec.ts

@@ -7,6 +7,7 @@
  * are all exercised through the real `confine()` path.
  */
 
+import { spawnSync } from 'node:child_process'
 import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
@@ -438,10 +439,30 @@ describe('the windows-acl probe (runner invocation contract)', () => {
       windowsAclRunnerEntry: absentRunnerEntry(),
     })
     const confined = sandbox.confine(['true'], RO)
-    expect(confined.argv.slice(0, 3)).toEqual([process.execPath, '--import', 'tsx/esm'])
+    expect(confined.argv.slice(0, 3)).toEqual([process.execPath, '--import', import.meta.resolve('tsx/esm')])
     expect(confined.argv[3]).toMatch(/runner\.ts$/)
   })
 
+  it('loads the source preload from a cwd outside the checkout', async () => {
+    const cwd = mkdtempSync(join(tmpdir(), 'dsh-acl-source-cwd-'))
+    tempDirs.push(cwd)
+    const { sandbox } = await setup({}, {
+      chain: ['windows-acl', 'bwrap'],
+      probeWindowsAcl: () => true,
+      windowsAclRunnerEntry: absentRunnerEntry(),
+    })
+    const { argv } = sandbox.confine(['true'], RO)
+    const result = spawnSync(argv[0]!, [...argv.slice(1, 3), '-e', 'process.stdout.write("source-loader-ready")'], {
+      cwd,
+      encoding: 'utf8',
+      timeout: 5000,
+      env: { ...process.env, NODE_OPTIONS: undefined, TSX_TSCONFIG_PATH: undefined },
+    })
+    expect(result.error).toBeUndefined()
+    expect(result.status, result.stderr).toBe(0)
+    expect(result.stdout).toBe('source-loader-ready')
+  }, 10_000)
+
   it('reads an empty runner invocation as unusable (the probe\'s empty-argv guard)', async () => {
     // windowsAclRunnerInvocation always yields [node, ...] in product; an
     // override returning [] exercises the default probe's empty-argv guard.