diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 4c54f9cd65..1ecaa1abd6 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.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/module-graph.md -module-graph.md: d2f7fd72e9026a7f7eb248a1c4ae234125f3571a -module-graph.zh.md: cc85a23a7866411aa69edf47a1b437f05270a930 +module-graph.md: 1fe166cfbe4a5c8a8d214cf5cd8438cbff32e5f7 +module-graph.zh.md: a2adf21e35734e8cd28a325cd3398c3638c963b0 diff --git a/docs/module-graph.md b/docs/module-graph.md index d2f7fd72e9..1fe166cfbe 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -423,6 +423,7 @@ flowchart TD pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_home_paths pkg_credentials_local --> pkg_launch_environment + pkg_sandbox_windows_acl --> pkg_subprocess pkg_subprocess_local --> pkg_subprocess pkg_subprocess_local --> pkg_timeout pkg_skill_badge --> pkg_skill @@ -1262,7 +1263,6 @@ flowchart TD | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — | | [`host-webserver`](../packages/host/webserver) | `host` | — | | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — | -| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — | | [`session-format`](../packages/session/session-format) | `session` | — | | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — | | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — | @@ -1301,6 +1301,7 @@ flowchart TD | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) | +| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) | | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) | | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index cc85a23a78..a2adf21e35 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -425,6 +425,7 @@ flowchart TD pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_home_paths pkg_credentials_local --> pkg_launch_environment + pkg_sandbox_windows_acl --> pkg_subprocess pkg_subprocess_local --> pkg_subprocess pkg_subprocess_local --> pkg_timeout pkg_skill_badge --> pkg_skill @@ -1264,7 +1265,6 @@ flowchart TD | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — | | [`host-webserver`](../packages/host/webserver) | `host` | — | | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — | -| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — | | [`session-format`](../packages/session/session-format) | `session` | — | | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — | | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — | @@ -1303,6 +1303,7 @@ flowchart TD | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) | +| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) | | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) | | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | diff --git a/packages/sandbox/sandbox-windows-acl/tests/control.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/control.spec.ts index c69deb8a4f..8e419b480c 100644 --- a/packages/sandbox/sandbox-windows-acl/tests/control.spec.ts +++ b/packages/sandbox/sandbox-windows-acl/tests/control.spec.ts @@ -23,7 +23,7 @@ describe.skipIf(process.platform !== 'win32')('managed Windows ACL control pipe' ctx = new Context() await ctx.plugin(LocalSubprocessRuntime) const runner = fileURLToPath(new URL('../src/runner.ts', import.meta.url)) - const helper = import.meta.resolve('@deepseek-ai/dsh-subprocess/control') + const helper = import.meta.resolve('@deepseek-ai/dsh-subprocess/src/control.ts') const program = ` const { openInheritedControlChannel } = await import(process.argv[1]); const { writeFileSync } = await import('node:fs'); diff --git a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts index 64899d9db8..0d07389301 100644 --- a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts +++ b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts @@ -358,6 +358,23 @@ describe('AclSandbox init', () => { }) describe('AclSandbox spawn', () => { + it('refuses control with piped stdio before starting a restricted process', async () => { + const sandbox = new AclSandbox({ writableDirs: [], tempDir: null, mode: 'read-only' }) + await sandbox.init() + expect(() => sandbox.spawn({ command: 'probe.exe', controlFileDescriptor: 7 })).toThrow('control pipe requires inherited stdio') + sandbox.dispose() + }) + + it('forwards the inherited control pipe to the restricted child', async () => { + const { api } = state.stubs as HappyStubs + Object.assign(api, { uvGetOsfhandle: vi.fn(() => 107n), getFileType: vi.fn(() => 3) }) + const sandbox = new AclSandbox({ writableDirs: [], tempDir: null, mode: 'read-only' }) + await sandbox.init() + const child = sandbox.spawn({ command: 'probe.exe', stdio: 'inherit', controlFileDescriptor: 7 }) + await expect(child.wait()).resolves.toEqual({ stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 42 }) + sandbox.dispose() + }) + it('refuses to spawn before init', () => { const workspace = scratch() const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-11', mode: 'workspace-write' }) diff --git a/packages/subprocess/subprocess-local/tests/control.spec.ts b/packages/subprocess/subprocess-local/tests/control.spec.ts index f4a08a9c9e..cdc942ae28 100644 --- a/packages/subprocess/subprocess-local/tests/control.spec.ts +++ b/packages/subprocess/subprocess-local/tests/control.spec.ts @@ -5,9 +5,10 @@ import { fileURLToPath } from 'node:url' import { once } from 'node:events' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess' +import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess' import { SUBPROCESS_CONTROL_ENV } from '@deepseek-ai/dsh-subprocess/control' import { LocalSubprocessRuntime } from '../src/index.ts' +import { spawnSubprocess } from '../src/spawn.ts' const fixture = fileURLToPath(new URL('./fixtures/control-child.ts', import.meta.url)) const helper = fileURLToPath(new URL('../../subprocess/src/control.ts', import.meta.url)) @@ -16,6 +17,8 @@ let root: string | undefined let handle: SubprocessHandle | undefined afterEach(async () => { + handle?.terminate() + await handle?.waitForExit() handle?.control?.destroy() await ctx?.fiber.dispose() if (root !== undefined) await rm(root, { recursive: true, force: true }) @@ -58,18 +61,19 @@ describe('managed subprocess control pipe', () => { expect(await handle.waitForExit()).toBe(true) }) - it('returns exact binary control bytes independently of stdout and stderr', async () => { + it.each(['managed', 'fallback'] as const)('returns exact binary control bytes through %s independently of stdio', async (backend) => { root = await mkdtemp(join(tmpdir(), 'dsh-control-')) ctx = new Context() await ctx.plugin(LocalSubprocessRuntime) const input = Buffer.alloc(256 * 1024) for (let index = 0; index < input.length; index++) input[index] = index % 256 - handle = ctx.subprocess.spawn({ + const request: SubprocessSpawnSpec = { argv: [process.execPath, fixture, helper, String(input.length)], cwd: root, stdio: { stdin: 'ignore', stdout: { maxBytes: 1024 }, stderr: { maxBytes: 1024 }, control: 'pipe' }, graceMs: 1000, - }) + } + handle = backend === 'managed' ? ctx.subprocess.spawn(request) : spawnSubprocess(request) const channel = handle.control if (channel === undefined) throw new Error('requested control pipe is absent') const received = (async () => { diff --git a/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts b/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts index fe30346c3c..e2df9d213a 100644 --- a/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts +++ b/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts @@ -543,8 +543,8 @@ describe('Linux one-shot exec bootstrap', () => { }) }) - it('retries ENOEXEC through /bin/sh with the resolved file and original arguments', async () => { - const files = track(createLinuxLaunchFiles({ cwd: '/work', env: { PATH: 'bin' } })) + it.each([undefined, 'pipe'] as const)('retries ENOEXEC through /bin/sh with control %s', async (control) => { + const files = track(createLinuxLaunchFiles({ cwd: '/work', env: { PATH: 'bin' }, ...control === undefined ? {} : { control } })) const execve = vi.fn() .mockImplementationOnce(() => { throw Object.assign(new Error('exec format'), { code: 'ENOEXEC' }) }) .mockImplementationOnce(() => { throw Object.assign(new Error('shell failed'), { code: 'EIO' }) }) @@ -555,8 +555,8 @@ describe('Linux one-shot exec bootstrap', () => { internals({ execve: execve as never }), ) expect(execve.mock.calls).toEqual([ - ['/work/bin/tool', ['tool', 'literal arg'], { PATH: 'bin' }], - ['/bin/sh', ['/bin/sh', '/work/bin/tool', 'literal arg'], { PATH: 'bin' }], + ['/work/bin/tool', ['tool', 'literal arg'], { PATH: 'bin' }, ...control === undefined ? [] : [control]], + ['/bin/sh', ['/bin/sh', '/work/bin/tool', 'literal arg'], { PATH: 'bin' }, ...control === undefined ? [] : [control]], ]) expect(readLinuxStartupError(files.startupErrorPath)).toMatchObject({ type: 'error', error: { code: 'EIO', path: 'tool' }, diff --git a/packages/subprocess/subprocess-local/tests/windows-job.spec.ts b/packages/subprocess/subprocess-local/tests/windows-job.spec.ts index 19fadad5fb..9a11b5b6f7 100644 --- a/packages/subprocess/subprocess-local/tests/windows-job.spec.ts +++ b/packages/subprocess/subprocess-local/tests/windows-job.spec.ts @@ -138,6 +138,20 @@ describe('Windows Job capability', () => { }) describe('Windows parent runner contract', () => { + it('keeps the requested control endpoint separate from runner IPC and ordinary output', () => { + const child = new FakeChild() + const control = new PassThrough() + child.stdio.push(control) + const request = { ...spec, stdio: { ...spec.stdio, control: 'pipe' as const } } + const { result, spawn } = launch(child, request) + expect(result.control).toBe(control) + expect(child.sent).toEqual([{ type: 'start', cwd: 'C:\\target', env: { TARGET: 'yes' }, control: 'pipe' }]) + expect(spawn).toHaveBeenCalledWith('C:\\node.exe', expect.any(Array), expect.objectContaining({ + stdio: ['ignore', 'ignore', 'ignore', 'ipc', 'pipe', 'pipe', 2, 'overlapped'], + })) + control.destroy() + }) + it('isolates runner stdio, carries target stdio on fd 4 through fd 6, and sends cwd/env', () => { const { child, result, spawn } = launch() expect(spawn).toHaveBeenCalledWith('C:\\node.exe', [ diff --git a/packages/subprocess/win32-process/tests/process.spec.ts b/packages/subprocess/win32-process/tests/process.spec.ts index 85835a659c..7b6e5c41ba 100644 --- a/packages/subprocess/win32-process/tests/process.spec.ts +++ b/packages/subprocess/win32-process/tests/process.spec.ts @@ -70,6 +70,16 @@ function inheritedApi(overrides: Partial = {}): { describe('spawnInheritedJobProcess', () => { const token = 70n as NativePtr + it('inherits the control carrier into a restricted child before resume', () => { + const descriptorHandle = vi.fn(() => 107n as NativePtr) + const { api, events } = inheritedApi({ uvGetOsfhandle: descriptorHandle, getFileType: vi.fn(() => 3) }) + const child = spawnInheritedJobProcess(api, { command: 'node.exe', args: [], cwd: 'C:\\work', token, controlFileDescriptor: 7 }) + expect(child.pid).toBe(1234) + expect(descriptorHandle).toHaveBeenCalledExactlyOnceWith(7) + expect(events.filter(event => event === 'inherit')).toHaveLength(4) + expect(events.indexOf('assign')).toBeLessThan(events.indexOf('resume')) + }) + it('creates suspended, assigns the Job, then resumes the restricted child', () => { const { api, diff --git a/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl b/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl index ff0e383737..8d0853cf67 100644 --- a/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl +++ b/snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"inspect-subprocess-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-subprocess-api"},"content":[{"type":"tool-result","toolCallId":"inspect-subprocess-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"subprocess\",\n \"description\": \"Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).\\n\\nImplementations must honor these semantics:\\n\\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\\n- spawn returns a live handle synchronously. Target identity remains provider-private; `done` resolves with the spawned command's exit facts and may reject for spawn or provider failures.\\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\\n- SubprocessHandle.terminate (and the spec's abort signal) starts the provider's documented procedure against its managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its signalling and observability limits.\\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"subprocess\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"subprocess\"\n ],\n \"expression\": \"ctx.subprocess\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise\",\n \"description\": \"Resolve one configured executable in this provider's execution world. Absolute paths are verified; bare names use the provider's scrubbed PATH plus explicit environment overrides. Relative paths containing separators are rejected: the resolution base is undefined, so providers fail loud instead of guessing.\",\n \"parameters\": [\n {\n \"name\": \"command\",\n \"description\": \"absolute executable path or bare PATH name.\"\n },\n {\n \"name\": \"env\",\n \"description\": \"explicit environment entries used for lookup.\"\n },\n {\n \"name\": \"signal\",\n \"description\": \"aborts remote or local lookup.\"\n }\n ],\n \"returns\": \"a canonical executable path.\"\n },\n {\n \"signature\": \"abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle\",\n \"description\": \"Start one managed child process from a fully-specified spec; this seam applies no defaults.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"argv, directory, stdio dispositions, grace, cancellation, and environment.\"\n }\n ],\n \"returns\": \"the live process handle (streams/readers, signalling, outcome promise).\",\n \"throws\": [\n \"synchronously when pre-aborted or when argv, cwd, environment, or grace is invalid before handle creation.\"\n ]\n },\n {\n \"signature\": \"abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise\",\n \"description\": \"Allocate a real terminal and start one owned process session. This is the only non-pipe process primitive: implementations own terminal byte I/O, foreground groups, signals, and whole-session quiescence.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.\"\n }\n ],\n \"returns\": \"the live terminal handle after allocation succeeds.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"SubprocessCollect\",\n \"declaration\": \"export interface SubprocessCollect {\\n maxBytes: number;\\n spill?: {\\n maxBytes: number;\\n };\\n}\"\n },\n {\n \"name\": \"SubprocessCollectedOutputs\",\n \"declaration\": \"export interface SubprocessCollectedOutputs {\\n readonly stdout?: SubprocessOutputReader;\\n readonly stderr?: SubprocessOutputReader;\\n}\"\n },\n {\n \"name\": \"SubprocessHandle\",\n \"declaration\": \"export interface SubprocessHandle {\\n readonly stdin: Writable | undefined;\\n readonly stdout: Readable | undefined;\\n readonly stderr: Readable | undefined;\\n readonly collected: SubprocessCollectedOutputs;\\n readonly done: Promise;\\n terminate(): void;\\n waitForExit(signal?: AbortSignal): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessOutcome\",\n \"declaration\": \"export interface SubprocessOutcome {\\n exitCode: number | null;\\n signal: NodeJS.Signals | null;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputMode\",\n \"declaration\": \"export type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect;\"\n },\n {\n \"name\": \"SubprocessOutputRead\",\n \"declaration\": \"export interface SubprocessOutputRead {\\n text: string;\\n nextOffset: number;\\n lossy: boolean;\\n spillPath?: string;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputReader\",\n \"declaration\": \"export interface SubprocessOutputReader {\\n readFrom(fromByte: number): SubprocessOutputRead;\\n}\"\n },\n {\n \"name\": \"SubprocessSpawnSpec\",\n \"declaration\": \"export interface SubprocessSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n stdio: SubprocessStdio;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n env?: NodeJS.ProcessEnv | undefined;\\n}\"\n },\n {\n \"name\": \"SubprocessStdinMode\",\n \"declaration\": \"export type SubprocessStdinMode = 'ignore' | 'pipe' | {\\n readonly data: string;\\n};\"\n },\n {\n \"name\": \"SubprocessStdio\",\n \"declaration\": \"export interface SubprocessStdio {\\n stdin: SubprocessStdinMode;\\n stdout: SubprocessOutputMode;\\n stderr: SubprocessOutputMode;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalForeground\",\n \"declaration\": \"export interface SubprocessTerminalForeground {\\n processGroupId: number;\\n inputWaiting: boolean;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalHandle\",\n \"declaration\": \"export interface SubprocessTerminalHandle {\\n readonly pid: number;\\n readonly output: Readable;\\n readonly done: Promise;\\n write(data: string): Promise;\\n inspectForeground(): Promise;\\n signalForeground(signal: SubprocessTerminalSignal): Promise;\\n terminate(): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalSignal\",\n \"declaration\": \"export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP';\"\n },\n {\n \"name\": \"SubprocessTerminalSpawnSpec\",\n \"declaration\": \"export interface SubprocessTerminalSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n env?: Record | undefined;\\n rows: number;\\n cols: number;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n}\"\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-subprocess-api"},"content":[{"type":"tool-result","toolCallId":"inspect-subprocess-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"subprocess\",\n \"description\": \"Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).\\n\\nImplementations must honor these semantics:\\n\\n- Executable paths belong to one execution world shared with the mounted filesystem provider.\\n- spawn returns a live handle synchronously. Target identity remains provider-private; `done` resolves with the spawned command's exit facts and may reject for spawn or provider failures.\\n- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.\\n- SubprocessHandle.terminate (and the spec's abort signal) starts the provider's documented procedure against its managed range. SubprocessHandle.waitForExit observes that same range so a consumer-owned teardown ladder can hold each tier on real quiescence; each provider documents its signalling and observability limits.\\n- Disposal of the service terminates all still-running managed processes and awaits their exit.\\n- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"subprocess\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"subprocess\"\n ],\n \"expression\": \"ctx.subprocess\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"abstract resolveExecutable( command: string, env?: Readonly>, signal?: AbortSignal, ): Promise\",\n \"description\": \"Resolve one configured executable in this provider's execution world. Absolute paths are verified; bare names use the provider's scrubbed PATH plus explicit environment overrides. Relative paths containing separators are rejected: the resolution base is undefined, so providers fail loud instead of guessing.\",\n \"parameters\": [\n {\n \"name\": \"command\",\n \"description\": \"absolute executable path or bare PATH name.\"\n },\n {\n \"name\": \"env\",\n \"description\": \"explicit environment entries used for lookup.\"\n },\n {\n \"name\": \"signal\",\n \"description\": \"aborts remote or local lookup.\"\n }\n ],\n \"returns\": \"a canonical executable path.\"\n },\n {\n \"signature\": \"abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle\",\n \"description\": \"Start one managed child process from a fully-specified spec; this seam applies no defaults.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"argv, directory, stdio dispositions, grace, cancellation, and environment.\"\n }\n ],\n \"returns\": \"the live process handle (streams/readers, signalling, outcome promise).\",\n \"throws\": [\n \"synchronously when pre-aborted or when argv, cwd, environment, or grace is invalid before handle creation.\"\n ]\n },\n {\n \"signature\": \"abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise\",\n \"description\": \"Allocate a real terminal and start one owned process session. This is the only non-pipe process primitive: implementations own terminal byte I/O, foreground groups, signals, and whole-session quiescence.\",\n \"parameters\": [\n {\n \"name\": \"spec\",\n \"description\": \"fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.\"\n }\n ],\n \"returns\": \"the live terminal handle after allocation succeeds.\"\n }\n ]\n },\n \"referencedTypes\": [\n {\n \"name\": \"SubprocessCollect\",\n \"declaration\": \"export interface SubprocessCollect {\\n maxBytes: number;\\n spill?: {\\n maxBytes: number;\\n };\\n}\"\n },\n {\n \"name\": \"SubprocessCollectedOutputs\",\n \"declaration\": \"export interface SubprocessCollectedOutputs {\\n readonly stdout?: SubprocessOutputReader;\\n readonly stderr?: SubprocessOutputReader;\\n}\"\n },\n {\n \"name\": \"SubprocessHandle\",\n \"declaration\": \"export interface SubprocessHandle {\\n readonly stdin: Writable | undefined;\\n readonly stdout: Readable | undefined;\\n readonly stderr: Readable | undefined;\\n readonly control: Duplex | undefined;\\n readonly collected: SubprocessCollectedOutputs;\\n readonly done: Promise;\\n terminate(): void;\\n waitForExit(signal?: AbortSignal): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessOutcome\",\n \"declaration\": \"export interface SubprocessOutcome {\\n exitCode: number | null;\\n signal: NodeJS.Signals | null;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputMode\",\n \"declaration\": \"export type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect;\"\n },\n {\n \"name\": \"SubprocessOutputRead\",\n \"declaration\": \"export interface SubprocessOutputRead {\\n text: string;\\n nextOffset: number;\\n lossy: boolean;\\n spillPath?: string;\\n}\"\n },\n {\n \"name\": \"SubprocessOutputReader\",\n \"declaration\": \"export interface SubprocessOutputReader {\\n readFrom(fromByte: number): SubprocessOutputRead;\\n}\"\n },\n {\n \"name\": \"SubprocessSpawnSpec\",\n \"declaration\": \"export interface SubprocessSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n stdio: SubprocessStdio;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n env?: NodeJS.ProcessEnv | undefined;\\n}\"\n },\n {\n \"name\": \"SubprocessStdinMode\",\n \"declaration\": \"export type SubprocessStdinMode = 'ignore' | 'pipe' | {\\n readonly data: string;\\n};\"\n },\n {\n \"name\": \"SubprocessStdio\",\n \"declaration\": \"export interface SubprocessStdio {\\n stdin: SubprocessStdinMode;\\n stdout: SubprocessOutputMode;\\n stderr: SubprocessOutputMode;\\n control?: 'pipe';\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalForeground\",\n \"declaration\": \"export interface SubprocessTerminalForeground {\\n processGroupId: number;\\n inputWaiting: boolean;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalHandle\",\n \"declaration\": \"export interface SubprocessTerminalHandle {\\n readonly pid: number;\\n readonly output: Readable;\\n readonly done: Promise;\\n write(data: string): Promise;\\n inspectForeground(): Promise;\\n signalForeground(signal: SubprocessTerminalSignal): Promise;\\n terminate(): Promise;\\n}\"\n },\n {\n \"name\": \"SubprocessTerminalSignal\",\n \"declaration\": \"export type SubprocessTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP';\"\n },\n {\n \"name\": \"SubprocessTerminalSpawnSpec\",\n \"declaration\": \"export interface SubprocessTerminalSpawnSpec {\\n argv: readonly string[];\\n cwd: string;\\n env?: Record | undefined;\\n rows: number;\\n cols: number;\\n graceMs: number;\\n signal?: AbortSignal | undefined;\\n}\"\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146037,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788778146037,"index":0,"dt":[],"texts":["CORDIS_INSPECT_JSDOC_OK"]},{"type":"chunk","time":1788778146037,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}