fix(rebase): migrate the replayed stack onto current master APIs
The linear replay carried each commit's own lineage, so this checkpoint restores the master-owned surfaces the conflicted regions clobbered and migrates branch-owned code to master's post-rebase APIs: - rebuild subprocess-local spawn.ts on master's tree-exit-observer machinery, keeping the branch's win32 childEnv key semantics and the Linux zombie-quiescence probe; the zombie test reaps its survivor directly since a confirmed-absent verdict is a permanent no-more-signals boundary - migrate pty-local test stubs to the Inbox-model Agent interface, Session.create, runnerFailureRules, and the new turn/start payload - implement the seam's resolveExecutable/spawnTerminal abstracts in the new pwsh-local and tool-fs-search test fakes - restore code-runtime, atomic-write, pwsh-local, and app-boot to master's exact content (the net-zero code-runtime churn is pruned from this history) and drop rename-detection graft debris - re-apply the PR's architecture rows and execution-world paragraph, re-record bilingual pairings, regenerate catalogs, and reconcile the lockfile
This commit is contained in:
@@ -6,7 +6,7 @@ Persistent shell backend for `ctx.pty` over `ctx.subprocess.spawnTerminal`. It s
|
||||
|
||||
## Plugin (`pty-local`)
|
||||
|
||||
The plugin injects `pty`, `sandboxPolicy`, and `subprocess`, then registers the configured backend type (`shell`). `danger-full-access` starts the shell directly without requiring a sandbox provider; confined modes require a same-world `ctx.sandbox` and wrap the exact shell argv through it, failing before spawn when none is mounted. The effective session mode is resolved at spawn. A change to a different effective mode is rejected before its `sandbox/mode` event commits while that owner has an open PTY or a spawn in progress; the fence is attached to the exact owner and therefore outlives a provider reload that retains existing sessions. Wait for creation to settle and close the sessions before changing modes, so a terminal opened with wider access cannot survive a downgrade.
|
||||
The plugin injects `pty`, `sandboxPolicy`, and `subprocess`, then registers the configured backend type (`shell`). `danger-full-access` starts the shell directly without requiring a sandbox provider; confined modes require a same-world `ctx.sandbox` and wrap the exact shell argv through it, failing before spawn when none is mounted. At spawn, one `ctx.sandboxPolicy.resolve({ session })` call supplies both the effective mode and the session workspace root; the same root is the default shell cwd when the caller omits one. A change to a different effective mode is rejected before its `sandbox/mode` event commits while that owner has an open PTY or a spawn in progress; the fence is attached to the exact owner and therefore outlives a provider reload that retains existing sessions. Wait for creation to settle and close the sessions before changing modes, so a terminal opened with wider access cannot survive a downgrade.
|
||||
|
||||
Readiness combines a foreground-verified private bash prompt marker, provider-reported foreground stdin-wait facts, silence fallback, and absolute timeout. A marker is not ready until the printable tail after the latest owned marker exactly equals the controlled `PS1`, including when the OSC marker and prompt are split across data callbacks; echoed input or output following an earlier prompt therefore cannot settle the current send. Prompt and silence evidence collected before the provider write, including while pre-write foreground inspection is pending, is discarded at the write boundary. When bash prints the marker before the terminal provider publishes its return to the foreground process group, polling retains the candidate for `handoffGraceMs` past the ordinary silence bound so a coincident handoff can win. An interactive child that inherits `PROMPT_COMMAND` therefore cannot suppress inferred-idle readiness until the absolute timeout. Unknown foreground state is never a positive exact-idle signal. A foreground group's stdin wait that existed before a send is likewise not post-write readiness: the same group must be observed outside that wait before a later wait can settle the send, while a changed foreground group is new evidence. During unpublished startup, a fallback requires observed output; zero-output silence cannot publish an empty session, and timeout rejects the spawn. Cancellation closes the unpublished shell and rejects with the caller's exact abort reason; `PtyBackendCleanupError` separately preserves a cleanup failure. The caller's signal is forwarded for terminal allocation and readiness initialization; after publication the handle owns its lifetime. Incomplete terminal-control sequences are bounded by `maxReadBytes` and discarded through their terminator after crossing that limit; malformed UTF-8 terminal output uses replacement characters, and a trailing carriage return is carried across callbacks so split CRLF becomes one newline.
|
||||
|
||||
@@ -14,19 +14,19 @@ Send cancellation marks queued input as canceled before asking the terminal hand
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Indirect consumer
|
||||
### Current file policy and indirect consumer
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Nothing directly. Through `@deepseek-ai/dsh-tool-pty`, the model may receive bounded MOTD, send deltas, scrollback pages, readiness reasons, and cleanup errors.
|
||||
The policy owner contributes capability-neutral `sandbox:policy` context. Through `@deepseek-ai/dsh-tool-pty` or another PTY consumer, the model may also receive bounded MOTD, send deltas, scrollback pages, readiness reasons, and cleanup errors.
|
||||
|
||||
#### Token effect
|
||||
|
||||
None until a consumer returns bounded backend output. Retained PTY scrollback is not placed in model history by this package.
|
||||
The current-policy clause is present while this backend is mounted. Retained PTY scrollback is not placed in model history until a consumer returns bounded output.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; the consumer owns prompts, schemas, and appended results.
|
||||
A standing-policy change appends an owner-rendered superseding runtime-context snapshot after retained history; consumer results remain append-only.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 插件(`pty-local`)
|
||||
|
||||
该插件注入 `pty`、`sandboxPolicy` 和 `subprocess`,然后注册所配置的后端类型(`shell`)。`danger-full-access` 无需沙箱提供方即可直接启动 shell;受限模式要求同一执行世界中存在 `ctx.sandbox`,并通过它包装确切的 shell argv,未挂载时会在 spawn 前失败。系统在 spawn 时解析会话的实际模式。当某个所有者存在开放的 PTY 或正在进行 spawn 时,如果配置变更会得到不同的实际模式,系统会在对应 `sandbox/mode` 事件提交前拒绝该变更。该限制绑定到确切所有者,因此即使提供方重新加载并保留现有会话,它仍然有效。更改模式前,请等待创建完成并关闭会话,避免以更宽权限打开的终端在权限降级后继续存在。
|
||||
该插件注入 `pty`、`sandboxPolicy` 和 `subprocess`,然后注册所配置的后端类型(`shell`)。`danger-full-access` 无需沙箱提供方即可直接启动 shell;受限模式要求同一执行世界中存在 `ctx.sandbox`,并通过它包装确切的 shell argv,未挂载时会在 spawn 前失败。Spawn 时,一次 `ctx.sandboxPolicy.resolve({ session })` 调用会同时给出实际模式与会话工作区根目录;调用方省略 cwd 时,同一根目录也是 shell 的默认 cwd。当某个所有者存在开放的 PTY 或正在进行 spawn 时,如果配置变更会得到不同的实际模式,系统会在对应 `sandbox/mode` 事件提交前拒绝该变更。该限制绑定到确切所有者,因此即使提供方重新加载并保留现有会话,它仍然有效。更改模式前,请等待创建完成并关闭会话,避免以更宽权限打开的终端在权限降级后继续存在。
|
||||
|
||||
就绪检测结合以下机制:由前台状态验证的私有 bash 提示符标记、提供方报告的前台 stdin 等待事实、静默回退和绝对超时。只有最新自有标记之后的可打印尾部与受控 `PS1` 完全相等,标记才算就绪;即使 OSC 标记和提示符被拆到多个数据回调中也一样。因此,较早提示符之后的回显输入或输出无法使当前 send 完成。提供方写入前收集的提示符与静默证据,包括写入前前台检查仍在等待时收集的证据,都会在写入边界丢弃。如果 bash 在终端提供方发布其重新取得前台进程组的状态前打印标记,轮询会在普通静默上限之后再保留该候选状态 `handoffGraceMs`,使恰好同时发生的前台交接有机会胜出。因此,继承 `PROMPT_COMMAND` 的交互式子进程无法一直抑制推断空闲就绪直至绝对超时。未知的前台状态绝不会作为精确空闲的正向信号。同样,一次 send 之前就已存在的前台进程组 stdin 等待并不代表写入后就绪:必须先观察到同一进程组脱离该等待,之后再次进入等待才能使该次 send 完成;前台进程组发生变化则构成新的证据。尚未发布的启动过程中,回退路径要求已经观察到输出;零输出静默不能发布空会话,超时则拒绝 spawn。取消操作会关闭尚未发布的 shell,并以调用方提供的确切中止原因拒绝;`PtyBackendCleanupError` 会单独保留清理失败。调用方的 signal 会转发给终端分配与就绪初始化;发布后,句柄负责其生命周期。未完成的终端控制序列受 `maxReadBytes` 限制;超过上限后,系统会丢弃内容直到其终止符。格式错误的 UTF-8 终端输出使用替换字符;末尾的回车会跨回调保留,使拆分的 CRLF 合并为一个换行。
|
||||
|
||||
@@ -14,19 +14,19 @@
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 间接消费方
|
||||
### 当前文件策略与间接消费方
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
没有直接可见内容。模型通过 `@deepseek-ai/dsh-tool-pty` 可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
|
||||
策略归属方会贡献与具体能力无关的 `sandbox:policy` 上下文。模型通过 `@deepseek-ai/dsh-tool-pty` 或其他 PTY 消费方还可能收到有界的 MOTD、发送增量、scrollback 页、就绪原因和清理错误。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
消费方返回有界的后端输出前没有影响。此包(package)不会把保留的 PTY scrollback 放入模型历史。
|
||||
装载该后端期间,当前策略子句会一直存在。消费方返回有界输出前,保留的 PTY scrollback 不会进入模型历史。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接使 KV Cache 失效;提示词、schema 与追加结果由消费方负责。
|
||||
常驻策略发生变化时,会在保留的历史之后追加一份由归属方渲染、取代先前状态的运行时上下文快照;消费方结果保持仅追加。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PassThrough } from 'node:stream'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { Inbox, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
||||
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
|
||||
class EmptySandbox extends SandboxProvider {
|
||||
confine(_argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv {
|
||||
return { argv: [], enforcement: 'full', denialSignatures: [], runnerFailureSignatures: [] }
|
||||
return { argv: [], enforcement: 'full', denialSignatures: [], runnerFailureRules: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class RecordingSandbox extends SandboxProvider {
|
||||
|
||||
confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv {
|
||||
this.calls.push({ argv, policy })
|
||||
return { argv: ['/sandbox', '--', ...argv], enforcement: 'full', denialSignatures: [], runnerFailureSignatures: [] }
|
||||
return { argv: ['/sandbox', '--', ...argv], enforcement: 'full', denialSignatures: [], runnerFailureRules: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,15 @@ function config(): ResolvedConfig {
|
||||
|
||||
function agent(ctx: Context): Agent {
|
||||
const id = SessionId('agent')
|
||||
const session = Session.create(id, undefined, { version: 0, id, createdAt: 0 })
|
||||
return {
|
||||
id, options: {}, session: new Session(id), status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
||||
status: 'idle',
|
||||
ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +337,7 @@ describe('pty-local plugin shape', () => {
|
||||
|
||||
const session = ctx.sessions.create(SessionId('unowned-mode'))
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('turn/start', { turn: 1 })
|
||||
}).not.toThrow()
|
||||
expect(() => { setSandboxMode(session, 'read-only') }).not.toThrow()
|
||||
})
|
||||
@@ -348,8 +354,13 @@ describe('pty-local plugin shape', () => {
|
||||
const session = ctx.sessions.create(SessionId('mode-owner'))
|
||||
const ownerFiber = await ctx.plugin(() => {})
|
||||
const owner: Agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx: ownerFiber.ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
||||
status: 'idle',
|
||||
ctx: ownerFiber.ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(owner)
|
||||
const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession())
|
||||
@@ -358,7 +369,7 @@ describe('pty-local plugin shape', () => {
|
||||
const unrelated = ctx.sessions.create(SessionId('unrelated-mode'))
|
||||
expect(() => { setSandboxMode(unrelated, 'read-only') }).not.toThrow()
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('turn/start', { turn: 1 })
|
||||
}).not.toThrow()
|
||||
|
||||
expect(() => { setSandboxMode(session, 'danger-full-access') }).not.toThrow()
|
||||
@@ -392,8 +403,13 @@ describe('pty-local plugin shape', () => {
|
||||
const session = ctx.sessions.create(SessionId('pending-mode-owner'))
|
||||
const ownerFiber = await ctx.plugin(() => {})
|
||||
const owner: Agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx: ownerFiber.ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
||||
status: 'idle',
|
||||
ctx: ownerFiber.ctx,
|
||||
send: () => {},
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(owner)
|
||||
const gate = Promise.withResolvers<undefined>()
|
||||
|
||||
Reference in New Issue
Block a user