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:
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user