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

test(tools): cover PTC standing policy and sandbox outcomes

Tianyi Cui 6 дней назад
Родитель
Сommit
ca7f3a523d

+ 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;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md
-code-runtime.md: 6f01cc446b7e60d8053845bf9df0ecb2547ad918
-code-runtime.zh.md: 9f09e3019ca65957f9722ba8baf4e97e432f7361
+code-runtime.md: efc759db8351a9556ac45376d49d08570fed2a57
+code-runtime.zh.md: 143b9ead9cc0dfceb3b78374b20d64aebbbe41bb

+ 2 - 0
docs/subsystems/code-runtime.md

@@ -179,6 +179,8 @@ Failure kinds are **orthogonal outcomes reported independently** (per [defensive
  * - `'worker-exit'` — the execution substrate died without settling (e.g. OOM).
  * - `'invalid-output'` — the completion value was not lossless JSON.
  * - `'output-limit'` — the serialized outer logs/value/diagnostic exceeded the configured cap.
+ * - `'protocol'` — the program sent invalid or over-budget control traffic.
+ * - `'sandbox-unavailable'` — required confinement could not be established.
  */
 interface CodeRunFailure {
   /** The failure class (see the interface doc for each kind's meaning). */

+ 2 - 0
docs/subsystems/code-runtime.zh.md

@@ -179,6 +179,8 @@ type CodeBindingFunction = (args: unknown) => Promise<CodeJsonValue>
  * - `'worker-exit'` — the execution substrate died without settling (e.g. OOM).
  * - `'invalid-output'` — the completion value was not lossless JSON.
  * - `'output-limit'` — the serialized outer logs/value/diagnostic exceeded the configured cap.
+ * - `'protocol'` — the program sent invalid or over-budget control traffic.
+ * - `'sandbox-unavailable'` — required confinement could not be established.
  */
 interface CodeRunFailure {
   /** The failure class (see the interface doc for each kind's meaning). */

+ 2 - 0
packages/code-runtime/code-runtime/src/types.ts

@@ -123,6 +123,8 @@ export interface CodeRunSandbox {
  * - `'worker-exit'` — the execution substrate died without settling (e.g. OOM).
  * - `'invalid-output'` — the completion value was not lossless JSON.
  * - `'output-limit'` — the serialized outer logs/value/diagnostic exceeded the configured cap.
+ * - `'protocol'` — the program sent invalid or over-budget control traffic.
+ * - `'sandbox-unavailable'` — required confinement could not be established.
  */
 export interface CodeRunFailure {
   /** The failure class (see the interface doc for each kind's meaning). */

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

@@ -12,6 +12,8 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
 import type { SessionEventMap } from '@deepseek-ai/dsh-session'
 import type { JsonValue } from '@deepseek-ai/dsh-util-values'
+import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
+import SessionProjections from '@deepseek-ai/dsh-session-projection'
 
 const testToolSignal = new AbortController().signal
 
@@ -1910,3 +1912,59 @@ describe('per-agent presentation', () => {
       .rejects.toThrow('mode "both" requires a code runtime')
   })
 })
+
+
+class ConfinedFakeRuntime extends FakeRuntime {
+  override get sandboxMode() { return 'read-only' as const }
+}
+
+describe('PTC standing file policy and sandbox outcomes', () => {
+  it('requires a policy owner before dispatching a confined runtime', async () => {
+    const { ctx } = await setup({ runtime: false })
+    try {
+      await ctx.plugin(ConfinedFakeRuntime)
+      const result = await runCode(ctx, 'return 1')
+      expect(result.isError).toBe(true)
+      expect(result.content).toEqual([{ type: 'text', text: 'Error: dsh-tools: confined code runtime requires sandboxPolicy' }])
+      expect((ctx.codeRuntime as ConfinedFakeRuntime).lastRequest).toBeUndefined()
+    } finally { await ctx.fiber.dispose() }
+  })
+
+  it('resolves deployment policy for an agentless program', async () => {
+    const { ctx } = await setup({ runtime: false })
+    try {
+      await ctx.plugin(SessionProjections)
+      await ctx.plugin(SandboxPolicy, { mode: 'read-only', workspaceRoot: process.cwd() })
+      await ctx.plugin(ConfinedFakeRuntime)
+      const result = await runCode(ctx, 'return 1')
+      expect(result.isError).not.toBe(true)
+      expect((ctx.codeRuntime as ConfinedFakeRuntime).lastRequest?.sandboxPolicy).toEqual(ctx.sandboxPolicy.resolve())
+    } finally { await ctx.fiber.dispose() }
+  })
+
+  it('preserves partial enforcement and observed denial in successful output', async () => {
+    const { ctx, runtime } = await setup()
+    try {
+      runtime.behavior = async () => ({ logs: [], sandbox: { mode: 'read-only', enforcement: 'partial', denied: true } })
+      const result = await runCode(ctx, 'return 1')
+      expect(result.value).toEqual({ logs: [], sandbox: { mode: 'read-only', enforcement: 'partial', denied: true } })
+      expect(result.content).toEqual([{ type: 'text', text: 'File sandbox enforcement is partial on this host.\nThe read-only file sandbox denied an operation.' }])
+    } finally { await ctx.fiber.dispose() }
+  })
+
+  it.each([
+    { mode: 'danger-full-access' as const, denied: false },
+    { mode: 'read-only' as const, denied: true, enforcement: 'full' as const },
+  ])('includes the available sandbox facts with a failed program ($mode)', async (sandbox) => {
+    const { ctx, runtime } = await setup()
+    try {
+      runtime.behavior = async () => ({ logs: [], error: { kind: 'exception', message: 'failed' }, sandbox })
+      const result = await runCode(ctx, 'throw new Error("failed")')
+      expect(result.isError).toBe(true)
+      const text = result.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
+      expect(text).toContain(`File sandbox: ${sandbox.mode}`)
+      expect(text.includes('enforcement: full')).toBe('enforcement' in sandbox)
+      expect(text.includes('operation denied')).toBe(sandbox.denied)
+    } finally { await ctx.fiber.dispose() }
+  })
+})