diff --git a/packages/fs/fs-local/README.i18n.yaml b/packages/fs/fs-local/README.i18n.yaml index e1870cbf41..250b94dacf 100644 --- a/packages/fs/fs-local/README.i18n.yaml +++ b/packages/fs/fs-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/fs/fs-local/README.md -README.md: fe0e5e9dec07fad745d6bea28da9009e517c7d85 -README.zh.md: 195f3963328035e9c6c382dd924cd04ee9e8c642 +README.md: 2e934298ceff75440357b0742770010c8b1c3904 +README.zh.md: 14f4867ce0f9eaae2a1dea98dbd7d401c98d4535 diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index fe0e5e9dec..2e934298ce 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The **local-filesystem implementation** of the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)). Backs the twelve `FileSystem` primitives with the host filesystem; loading it as a plugin populates `ctx.fs`. +The **local-filesystem implementation** of the `ctx.fs` provider seam ([`@deepseek-ai/dsh-fs`](../fs)). Backs the eleven `FileSystem` primitives with the host filesystem; loading it as a plugin populates `ctx.fs`. ```ts ignore-check import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local' diff --git a/packages/fs/fs-local/README.zh.md b/packages/fs/fs-local/README.zh.md index 195f396332..14f4867ce0 100644 --- a/packages/fs/fs-local/README.zh.md +++ b/packages/fs/fs-local/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -`ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))的**本地文件系统实现**。它使用宿主文件系统支持十二个 `FileSystem` 原语;将其作为插件加载会填充 `ctx.fs`。 +`ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))的**本地文件系统实现**。它使用宿主文件系统支持十一个 `FileSystem` 原语;将其作为插件加载会填充 `ctx.fs`。 ```ts ignore-check import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local' diff --git a/packages/pty/pty-local/src/index.ts b/packages/pty/pty-local/src/index.ts index b1e0e6edbd..fa2237c959 100644 --- a/packages/pty/pty-local/src/index.ts +++ b/packages/pty/pty-local/src/index.ts @@ -10,7 +10,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import { PtyBackendCleanupError } from '@deepseek-ai/dsh-pty' import type { PtyBackend, PtyBackendSpawnSpec } from '@deepseek-ai/dsh-pty' import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess' -import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' +import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox' import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' import { type Config, type ResolvedConfig, validateConfig } from './config.ts' import { LocalPtySession } from './session.ts' @@ -68,18 +68,15 @@ function childEnvironment(spec: PtyBackendSpawnSpec): Record { } } -function spawnArgv(ctx: Context, config: ResolvedConfig, spec: PtyBackendSpawnSpec): string[] { +function spawnArgv(ctx: Context, config: ResolvedConfig, policy: SandboxExecutionPolicy): string[] { const argv = [config.shellPath, ...config.shellArgs] - const mode: SandboxMode = effectiveSandboxMode(spec.owner.session.events) ?? ctx.sandboxPolicy.defaultMode - if (mode === 'danger-full-access') return argv + if (policy.mode === 'danger-full-access') return argv const sandbox = ctx.get('sandbox') if (sandbox === undefined) { - throw new Error(`pty-local: sandbox mode "${mode}" requires a ctx.sandbox provider in the execution world`) + throw new Error(`pty-local: sandbox mode "${policy.mode}" requires a ctx.sandbox provider in the execution world`) } - return sandbox.confine(argv, { - mode: mode, - workspaceRoot: ctx.sandboxPolicy.workspaceRoot, - }).argv + // Re-state the discriminant because object spread does not preserve its narrowed type. + return sandbox.confine(argv, { ...policy, mode: policy.mode }).argv } // TODO(pty-initialize-race-home): Fold this outer abort race into @@ -122,11 +119,12 @@ export class LocalPtyBackend implements PtyBackend { async spawn(spec: PtyBackendSpawnSpec): Promise { spec.signal?.throwIfAborted() ensureSandboxModeFence(this.ctx, spec.owner) - const argv = spawnArgv(this.ctx, this.config, spec) + const policy = this.ctx.sandboxPolicy.resolve({ session: spec.owner.session }) + const argv = spawnArgv(this.ctx, this.config, policy) if (argv[0] === undefined) throw new Error('pty-local: sandbox returned empty argv') const terminal = await this.spawnTerminal({ argv, - cwd: spec.cwd ?? this.ctx.sandboxPolicy.workspaceRoot, + cwd: spec.cwd ?? policy.workspaceRoot, env: childEnvironment(spec), rows: this.config.rows, cols: this.config.cols, diff --git a/packages/pty/pty-local/tests/index.spec.ts b/packages/pty/pty-local/tests/index.spec.ts index 9f10a2cde8..d61eb58da5 100644 --- a/packages/pty/pty-local/tests/index.spec.ts +++ b/packages/pty/pty-local/tests/index.spec.ts @@ -44,9 +44,9 @@ function config(): ResolvedConfig { } } -function agent(ctx: Context): Agent { +function agent(ctx: Context, cwd?: string): Agent { const id = SessionId('agent') - const session = Session.create(id, undefined, { version: 0, id, createdAt: 0 }) + const session = Session.create(id, undefined, { version: 0, id, createdAt: 0, ...cwd === undefined ? {} : { cwd } }) return { id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }), status: 'idle', @@ -213,6 +213,42 @@ describe('LocalPtyBackend startup rollback', () => { }) expect(spawned?.env?.PTY_TEST_SECRET).toBeUndefined() expect(initialized).toHaveBeenCalledWith(undefined) + expect((ctx.sandbox as RecordingSandbox).calls).toEqual([{ + argv: ['/bin/bash', '-i'], + policy: { mode: 'workspace-write', workspaceRoot: '/workspace' }, + }]) + }) + + it('resolves session mode and root together before wrapping the shell', async () => { + const ctx = new Context() + await ctx.plugin(RecordingSandbox) + await ctx.plugin(SandboxPolicyService, { mode: 'read-only', workspaceRoot: '/deployment-fallback' }) + const terminal = terminalHandle() + let spawned: SubprocessTerminalSpawnSpec | undefined + const spawnTerminal = async (spec: SubprocessTerminalSpawnSpec): Promise => { + spawned = spec + return terminal + } + const initialized = vi.fn<() => Promise>().mockResolvedValue(undefined) + const session = { initialize: initialized } as unknown as LocalPtySession + const backend = new LocalPtyBackend( + ctx, + { ...config(), shellArgs: ['-i'] }, + spawnTerminal, + () => session, + ) + const owner = agent(ctx, '/session-workspace') + setSandboxMode(owner.session, 'workspace-write') + expect(await backend.spawn(spec(owner))).toBe(session) + + expect(spawned).toMatchObject({ + argv: ['/sandbox', '--', '/bin/bash', '-i'], + cwd: '/session-workspace', + }) + expect((ctx.sandbox as RecordingSandbox).calls).toEqual([{ + argv: ['/bin/bash', '-i'], + policy: { mode: 'workspace-write', workspaceRoot: '/session-workspace' }, + }]) }) it('rejects a confined spawn without a sandbox provider', async () => { diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index db72f4c52e..536342dc89 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -326,7 +326,7 @@ class SingleExeBuild { if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`) else await rm(stagedBuild, { recursive: true, force: true }) if (target.platform !== 'linux') return - const source = join(root, 'packages', 'pty', 'pty-local', 'node_modules', 'node-pty', 'build', 'Release', 'pty.node') + const source = join(root, 'packages', 'subprocess', 'subprocess-local', 'node_modules', 'node-pty', 'build', 'Release', 'pty.node') const destination = join(stagedBuild, 'Release', 'pty.node') if (this.cli.dryRun) { console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)