فهرست منبع

fix(sandbox): pin ACL source resolution and recognize Node denials

Tianyi Cui 1 ماه پیش
والد
کامیت
b35a3b29eb

+ 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: ab6182ffe86b658c59eebcffbc479097ed1cb25b
-README.zh.md: ca1e4142f10bbdc44ccfc8f143617a5d9b58fcd3
+README.md: 9e777274c64971a2c7762507e5b354d239330289
+README.zh.md: 0ed93e2fae8ea4b17e7a084c46c0fd9af514cf24

+ 1 - 1
packages/sandbox/sandbox-local/README.md

@@ -80,7 +80,7 @@ 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.
+When the built ACL runner is absent, source launch pins the `tsx/esm/api` loader and TypeScript path mapping to this installation. The command's working directory and ambient `TSX_TSCONFIG_PATH` cannot select the runner's source dependencies.
 
 ### Denial and runner-failure dialects
 

+ 1 - 1
packages/sandbox/sandbox-local/README.zh.md

@@ -80,7 +80,7 @@ Seatbelt profile 默认允许,带 `(deny file-write*)` 与来自共享 `writab
 
 Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,同时为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可撤销 ACE——共享工作区的会话共享其预期写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权。该档报告 `partial` 强制执行,因为受限令牌必须保留 Everyone,且 NTFS 硬链接会把同一文件对象别名为多个路径。
 
-构建后的 ACL runner 缺失时,源码启动使用从本安装目录解析出的 `tsx/esm` 模块 URL。命令的工作目录不需要包含 Harness 源码检出或独立的加载器依赖。
+构建后的 ACL runner 缺失时,源码启动将 `tsx/esm/api` 加载器和 TypeScript 路径映射固定到本安装目录。命令的工作目录和环境中的 `TSX_TSCONFIG_PATH` 无法选择 runner 的源码依赖。
 
 ### 拒绝与 runner 失败方言
 

+ 7 - 4
packages/sandbox/sandbox-local/src/index.ts

@@ -207,8 +207,8 @@ const DENIAL_SIGNATURES = {
   landlock: ['permission denied'],
   seatbelt: ['operation not permitted'],
   // pwsh/.NET: "Access to the path '...' is denied."; cmd: "Access is denied.";
-  // node EACCES: "permission denied".
-  'windows-acl': ['access is denied', 'access to the path', 'permission denied'],
+  // Node EACCES: "permission denied"; EPERM: "operation not permitted".
+  'windows-acl': ['access is denied', 'access to the path', 'permission denied', 'operation not permitted'],
   runnerCommand: ['read-only file system', 'permission denied'],
 } as const satisfies Record<SelectedRunner['runner'] | 'runnerCommand', readonly string[]>
 
@@ -551,7 +551,8 @@ 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.
+   * Pin the source loader and TypeScript paths to this installation, independently
+   * of target cwd or environment overrides.
    * The prefix stays `[node, runner, ...]` — a future native-exe runner keeps
    * the same argv contract and only swaps these entries.
    */
@@ -561,7 +562,9 @@ 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', import.meta.resolve('tsx/esm'), sourceEntry]
+    const sourceConfig = fileURLToPath(new URL('../../../../tsconfig.base.json', import.meta.url))
+    const registration = `import { register } from ${JSON.stringify(import.meta.resolve('tsx/esm/api'))}; register({ tsconfig: ${JSON.stringify(sourceConfig)} });`
+    return [process.execPath, '--import', `data:text/javascript,${encodeURIComponent(registration)}`, sourceEntry]
   }
 }
 

+ 17 - 12
packages/sandbox/sandbox-local/tests/local.spec.ts

@@ -405,7 +405,7 @@ describe('the windows-acl probe (runner invocation contract)', () => {
     expect(probeWindowsAcl).toHaveBeenCalledTimes(1)
     expect(confined.argv.slice(-4)).toEqual(['--mode', 'read-only', '--', 'true'])
     expect(confined.enforcement).toBe('partial')
-    expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied'])
+    expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied', 'operation not permitted'])
     expect(confined.runnerFailureRules).toEqual([{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }])
   })
 
@@ -439,28 +439,33 @@ 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', import.meta.resolve('tsx/esm')])
+    expect(confined.argv.slice(0, 2)).toEqual([process.execPath, '--import'])
+    expect(confined.argv[2]).toMatch(/^data:text\/javascript,/)
     expect(confined.argv[3]).toMatch(/runner\.ts$/)
   })
 
-  it('loads the source preload from a cwd outside the checkout', async () => {
+  it('loads the full source runner independently of cwd and ambient tsconfig', async () => {
     const cwd = mkdtempSync(join(tmpdir(), 'dsh-acl-source-cwd-'))
     tempDirs.push(cwd)
+    writeFileSync(join(cwd, 'tsconfig.json'), '{ invalid workspace configuration')
     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')
+    for (const ambientConfig of [undefined, join(cwd, 'absent-ambient-tsconfig.json')]) {
+      const result = spawnSync(argv[0]!, argv.slice(1, 4), {
+        cwd,
+        encoding: 'utf8',
+        timeout: 5000,
+        env: { ...process.env, NODE_OPTIONS: undefined, TSX_TSCONFIG_PATH: ambientConfig },
+      })
+      expect(result.error).toBeUndefined()
+      expect(result.signal).toBeNull()
+      expect(result.status, result.stderr).toBe(127)
+      expect(result.stderr).toBe('windows-acl-run: missing --workspace\n')
+    }
   }, 10_000)
 
   it('reads an empty runner invocation as unusable (the probe\'s empty-argv guard)', async () => {

+ 1 - 1
packages/sandbox/sandbox-windows-acl/tests/provider-chain.spec.ts

@@ -42,7 +42,7 @@ describe('windows-acl win32 chain (LocalSandboxProvider)', () => {
       'pwsh', '/Command', 'x',
     ])
     expect(confined.enforcement).toBe('partial')
-    expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied'])
+    expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied', 'operation not permitted'])
     expect(confined.runnerFailureRules).toEqual([{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }])
     // A sole candidate is selected unprobed.
     expect(probeWindowsAcl).not.toHaveBeenCalled()