Bladeren bron

test(ptc): make sandbox denial recording portable and cover execution descriptors

Tianyi Cui 6 dagen geleden
bovenliggende
commit
4d4e333676

+ 3 - 0
apps/web/tests/ptc-escalation.e2e.ts

@@ -19,6 +19,7 @@ const UI_EXPECTED = join(SNAPSHOT_DIR, 'approval.expected.md')
 const MODE = webSnapshotMode()
 const MODE = webSnapshotMode()
 const PROMPT = 'Use run_code with timeoutMs 120000 and direct Node filesystem access to create approved.txt in the working directory containing exactly "approved\\n". '
 const PROMPT = 'Use run_code with timeoutMs 120000 and direct Node filesystem access to create approved.txt in the working directory containing exactly "approved\\n". '
   + 'Use await import("node:fs/promises") and writeFile; do not call nested tools. First attempt the write under the current read-only sandbox without escalation. '
   + 'Use await import("node:fs/promises") and writeFile; do not call nested tools. First attempt the write under the current read-only sandbox without escalation. '
+  + 'In that first program, catch only filesystem errors with code EPERM, EACCES or EROFS and return exactly "EXPECTED_SANDBOX_DENIAL"; rethrow any other error. '
   + 'If the sandbox denies it, explicitly retry the program with sandbox_permissions "workspace-write" and justification "Create the file requested by the user". '
   + 'If the sandbox denies it, explicitly retry the program with sandbox_permissions "workspace-write" and justification "Create the file requested by the user". '
   + 'I will answer the approval prompt. After the file is written, reply DONE and stop.'
   + 'I will answer the approval prompt. After the file is written, reply DONE and stop.'
 
 
@@ -67,6 +68,8 @@ describe('web e2e: PTC program sandbox escalation', () => {
     expect(calls.every(event => event.data.name === 'run_code')).toBe(true)
     expect(calls.every(event => event.data.name === 'run_code')).toBe(true)
     expect(JSON.stringify(calls[0]?.data)).toContain('node:fs/promises')
     expect(JSON.stringify(calls[0]?.data)).toContain('node:fs/promises')
     expect(JSON.stringify(calls.at(-1)?.data)).toContain('workspace-write')
     expect(JSON.stringify(calls.at(-1)?.data)).toContain('workspace-write')
+    const results = events.filter(event => event.type === 'tool/result')
+    expect(JSON.stringify(results[0]?.data)).toContain('EXPECTED_SANDBOX_DENIAL')
     const file = join(scaffold.workspaceCwd, 'workspace', 'approved.txt')
     const file = join(scaffold.workspaceCwd, 'workspace', 'approved.txt')
     await expect(readFile(file, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
     await expect(readFile(file, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
     if (MODE !== 'record') {
     if (MODE !== 'record') {

+ 2 - 2
docs/subsystems/code-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md
 #   pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md
-code-runtime.md: 085d51e1c51fefe37b9b5913d1a2d1e69fbf5fc8
-code-runtime.zh.md: 887e0dabc6c1c5cf9faa0a9c47813cc0681ccbfa
+code-runtime.md: 19eb28750cddc6b2660d74f24927f65888ba175e
+code-runtime.zh.md: a8fa809846f5ecb57c1bf29fc30f53b42c5b4fdf

+ 1 - 1
docs/subsystems/code-runtime.md

@@ -192,7 +192,7 @@ interface CodeRunFailure {
 
 
 ## The service
 ## The service
 
 
-`CodeRuntime` is defined in [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts). `resolve(request)` returns complete execution inputs, and `run(spec)` executes them. `executionInstructions` supplies provider-owned usage guidance for consumer presentation. `language` selects supported program presentation; `isolation` describes the substrate without claiming security. `sandboxMode` advertises file-policy support, with `undefined` for a provider that does not supply confinement. Each implementation keeps program state separate between runs and terminates and awaits active executions during disposal.
+`CodeRuntime` is defined in [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts). `resolve(request)` returns complete execution inputs, and `run(spec)` executes them. `executionInstructions` supplies provider-owned usage guidance for consumer presentation. `timeout` reports the configured elapsed-time default and maximum when per-call overrides are supported; `resolve` still validates and caps each request. `language` selects supported program presentation; `isolation` describes the substrate without claiming security. `sandboxMode` advertises file-policy support, with `undefined` for a provider that does not supply confinement. Each implementation keeps program state separate between runs and terminates and awaits active executions during disposal.
 
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
 

+ 1 - 1
docs/subsystems/code-runtime.zh.md

@@ -192,7 +192,7 @@ interface CodeRunFailure {
 
 
 ## 服务
 ## 服务
 
 
-`CodeRuntime` 定义于 [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)。`resolve(request)` 返回完整执行输入,`run(spec)` 执行它们。`executionInstructions` 提供由运行时拥有的使用说明,供消费方呈现。`language` 选择支持的程序呈现;`isolation` 描述执行基底,不作安全声明。`sandboxMode` 声明文件策略支持,不提供约束的提供方返回 `undefined`。每个实现将各次运行的程序状态分离,并在资源释放期间终止且等待活跃执行。
+`CodeRuntime` 定义于 [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)。`resolve(request)` 返回完整执行输入,`run(spec)` 执行它们。`executionInstructions` 提供由运行时拥有的使用说明,供消费方呈现。支持逐次覆盖时,`timeout` 报告配置的经过时间默认值和上限;每次请求仍由 `resolve` 验证并截断。`language` 选择支持的程序呈现;`isolation` 描述执行基底,不作安全声明。`sandboxMode` 声明文件策略支持,不提供约束的提供方返回 `undefined`。每个实现将各次运行的程序状态分离,并在资源释放期间终止且等待活跃执行。
 
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
 

+ 7 - 0
packages/code-runtime/code-runtime-node/tests/runtime.spec.ts

@@ -132,6 +132,7 @@ describe('Node program process', () => {
 
 
   it('uses the default deadline and caps explicit requests', async () => {
   it('uses the default deadline and caps explicit requests', async () => {
     const { runtime } = await setup()
     const { runtime } = await setup()
+    expect(runtime.timeout).toEqual({ defaultMs: 120_000, maxMs: 600_000 })
     expect(runtime.executionInstructions).toBe('Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution\'s sandbox policy.')
     expect(runtime.executionInstructions).toBe('Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution\'s sandbox policy.')
     expect(runtime.resolve({ program: '', bindings: [] }).timeoutMs).toBe(120_000)
     expect(runtime.resolve({ program: '', bindings: [] }).timeoutMs).toBe(120_000)
     expect(runtime.resolve({ program: '', bindings: [], timeoutMs: 900_000 }).timeoutMs).toBe(600_000)
     expect(runtime.resolve({ program: '', bindings: [], timeoutMs: 900_000 }).timeoutMs).toBe(600_000)
@@ -139,6 +140,12 @@ describe('Node program process', () => {
     await expect(runtime.run({ program: '', bindings: [], cwd: process.cwd(), timeoutMs: 1000 })).rejects.toThrow('sandbox policy')
     await expect(runtime.run({ program: '', bindings: [], cwd: process.cwd(), timeoutMs: 1000 })).rejects.toThrow('sandbox policy')
   })
   })
 
 
+  it('advertises the capped default when the deployment maximum is lower', async () => {
+    const { runtime } = await setup({ timeoutMs: 2000, maxTimeoutMs: 1000 })
+    expect(runtime.timeout).toEqual({ defaultMs: 1000, maxMs: 1000 })
+    expect(runtime.timeout.defaultMs).toBe(runtime.resolve({ program: '', bindings: [] }).timeoutMs)
+  })
+
   it.each(['for (;;) {}', 'await new Promise(() => {})'])('ends an unfinished program at its elapsed deadline: %s', async (program) => {
   it.each(['for (;;) {}', 'await new Promise(() => {})'])('ends an unfinished program at its elapsed deadline: %s', async (program) => {
     const { run } = await setup({ timeoutMs: 600, graceMs: 50 })
     const { run } = await setup({ timeoutMs: 600, graceMs: 50 })
     expect((await run({ program, bindings: [] })).error?.kind).toBe('timeout')
     expect((await run({ program, bindings: [] })).error?.kind).toBe('timeout')

+ 2 - 1
packages/core/tools/src/ptc.ts

@@ -741,7 +741,8 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
       const runtime = peekRuntime()
       const runtime = peekRuntime()
       const instructions = runtime?.executionInstructions
       const instructions = runtime?.executionInstructions
       return resolveFlavor(peekRuntime).description
       return resolveFlavor(peekRuntime).description
-        + (instructions ? ` ${instructions} The working directory is the Session's current directory.` : '')
+        + (instructions ? ` ${instructions}` : '')
+        + (runtime === undefined ? '' : " The working directory is the Session's current directory.")
         + escalationGuidance(runtime)
         + escalationGuidance(runtime)
     },
     },
   })
   })

+ 1 - 0
packages/core/tools/tests/ptc.spec.ts

@@ -2034,6 +2034,7 @@ describe('per-program execution controls', () => {
       expect(JSON.stringify(schema.parameters)).not.toContain('timeoutMs')
       expect(JSON.stringify(schema.parameters)).not.toContain('timeoutMs')
       expect(JSON.stringify(schema.parameters)).not.toContain('sandbox_permissions')
       expect(JSON.stringify(schema.parameters)).not.toContain('sandbox_permissions')
       expect(schema.description).not.toContain('Programs start with an empty environment.')
       expect(schema.description).not.toContain('Programs start with an empty environment.')
+      expect(schema.description).toContain("The working directory is the Session's current directory.")
       const rejected = await python.tools.execute({
       const rejected = await python.tools.execute({
         callId: ToolCallId('hidden-timeout'), name: RUN_CODE_NAME, signal: testToolSignal,
         callId: ToolCallId('hidden-timeout'), name: RUN_CODE_NAME, signal: testToolSignal,
         arguments: { code: 'pass', description: 'Try unsupported timeout', timeoutMs: 5 },
         arguments: { code: 'pass', description: 'Try unsupported timeout', timeoutMs: 5 },