fix(pty-local): restore the session-resolved workspace policy

The linear replay resurrected the July spawn path: mode folded inline
and both confinement and the default cwd fenced to the deployment
workspaceRoot. Master (and the pre-rebase merge result) resolve the
policy once per spawn — resolve({ session }) — so session.header.cwd
is both the workspace-write boundary and the default shell cwd, as the
README already states. Restores the single resolve, threads the policy
through spawnArgv and the cwd fallback, re-expresses master's
session-root test on the terminal spec, and pins the recorded confine
policy in the explicit-cwd test.

Also from the same review round: the Python SDK exe build stages
pty.node from subprocess-local (node-pty's home since the relocation),
and fs-local's README counts the seam's eleven primitives.
This commit is contained in:
Tianyi Cui
2026-08-08 21:28:00 +08:00
parent d070129622
commit fbceb8dcd5
6 changed files with 52 additions and 18 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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'
+9 -11
View File
@@ -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<string, string> {
}
}
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<LocalPtySession> {
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,
+38 -2
View File
@@ -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<SubprocessTerminalHandle> => {
spawned = spec
return terminal
}
const initialized = vi.fn<() => Promise<void>>().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 () => {
+1 -1
View File
@@ -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}`)