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;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
|
# pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
|
||||||
README.md: fe0e5e9dec07fad745d6bea28da9009e517c7d85
|
README.md: 2e934298ceff75440357b0742770010c8b1c3904
|
||||||
README.zh.md: 195f3963328035e9c6c382dd924cd04ee9e8c642
|
README.zh.md: 14f4867ce0f9eaae2a1dea98dbd7d401c98d4535
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
English | [中文](README.zh.md)
|
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
|
```ts ignore-check
|
||||||
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
|
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](README.md) | 中文
|
[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
|
```ts ignore-check
|
||||||
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
|
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 { PtyBackendCleanupError } from '@deepseek-ai/dsh-pty'
|
||||||
import type { PtyBackend, PtyBackendSpawnSpec } 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 { 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 { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||||
import { type Config, type ResolvedConfig, validateConfig } from './config.ts'
|
import { type Config, type ResolvedConfig, validateConfig } from './config.ts'
|
||||||
import { LocalPtySession } from './session.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 argv = [config.shellPath, ...config.shellArgs]
|
||||||
const mode: SandboxMode = effectiveSandboxMode(spec.owner.session.events) ?? ctx.sandboxPolicy.defaultMode
|
if (policy.mode === 'danger-full-access') return argv
|
||||||
if (mode === 'danger-full-access') return argv
|
|
||||||
const sandbox = ctx.get('sandbox')
|
const sandbox = ctx.get('sandbox')
|
||||||
if (sandbox === undefined) {
|
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, {
|
// Re-state the discriminant because object spread does not preserve its narrowed type.
|
||||||
mode: mode,
|
return sandbox.confine(argv, { ...policy, mode: policy.mode }).argv
|
||||||
workspaceRoot: ctx.sandboxPolicy.workspaceRoot,
|
|
||||||
}).argv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(pty-initialize-race-home): Fold this outer abort race into
|
// 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> {
|
async spawn(spec: PtyBackendSpawnSpec): Promise<LocalPtySession> {
|
||||||
spec.signal?.throwIfAborted()
|
spec.signal?.throwIfAborted()
|
||||||
ensureSandboxModeFence(this.ctx, spec.owner)
|
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')
|
if (argv[0] === undefined) throw new Error('pty-local: sandbox returned empty argv')
|
||||||
const terminal = await this.spawnTerminal({
|
const terminal = await this.spawnTerminal({
|
||||||
argv,
|
argv,
|
||||||
cwd: spec.cwd ?? this.ctx.sandboxPolicy.workspaceRoot,
|
cwd: spec.cwd ?? policy.workspaceRoot,
|
||||||
env: childEnvironment(spec),
|
env: childEnvironment(spec),
|
||||||
rows: this.config.rows,
|
rows: this.config.rows,
|
||||||
cols: this.config.cols,
|
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 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 {
|
return {
|
||||||
id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
@@ -213,6 +213,42 @@ describe('LocalPtyBackend startup rollback', () => {
|
|||||||
})
|
})
|
||||||
expect(spawned?.env?.PTY_TEST_SECRET).toBeUndefined()
|
expect(spawned?.env?.PTY_TEST_SECRET).toBeUndefined()
|
||||||
expect(initialized).toHaveBeenCalledWith(undefined)
|
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 () => {
|
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}`)
|
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
|
||||||
else await rm(stagedBuild, { recursive: true, force: true })
|
else await rm(stagedBuild, { recursive: true, force: true })
|
||||||
if (target.platform !== 'linux') return
|
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')
|
const destination = join(stagedBuild, 'Release', 'pty.node')
|
||||||
if (this.cli.dryRun) {
|
if (this.cli.dryRun) {
|
||||||
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user