Stabilize master CI across platforms
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# @deepseek-ai/dsh-pty-local
|
||||
|
||||
Local `node-pty` backend for `ctx.pty`. It starts an interactive shell under the shared `ctx.sandboxPolicy`, strips credential-shaped ambient environment variables, retains bounded line-oriented output, detects readiness, and tears down the captured process tree rooted at the `node-pty` child.
|
||||
Local Linux/macOS `node-pty` backend for `ctx.pty`; loading it on another platform fails as unsupported. It starts an interactive shell under the shared `ctx.sandboxPolicy`, strips credential-shaped ambient environment variables, retains bounded line-oriented output, detects readiness, and tears down the captured process tree rooted at the `node-pty` child.
|
||||
|
||||
## Plugin (`pty-local`)
|
||||
|
||||
The plugin injects `pty`, `sandbox`, and `sandboxPolicy`, then registers the configured backend type (`shell`). `danger-full-access` starts the shell directly; confined modes wrap the exact shell argv through `ctx.sandbox`. 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 local-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.
|
||||
|
||||
Linux readiness combines a foreground-verified private bash prompt marker, foreground-process-group syscall inspection, silence fallback, and absolute timeout. macOS uses the verified prompt marker plus silence/timeout because it has no `/proc` syscall surface. A marker is not ready until printable prompt text arrives, including when the OSC marker and `PS1` are split across data callbacks. Unrecognized or unreadable process state is never a positive exact-idle signal. 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 even when its foreground process group is not observable yet; if that close fails, `PtyBackendCleanupError` separately preserves the cleanup failure for registry disposal. Incomplete terminal-control sequences are bounded by `maxReadBytes` and discarded through their terminator after crossing that limit; a trailing carriage return is carried across callbacks so split CRLF becomes one newline.
|
||||
Linux readiness combines a foreground-verified private bash prompt marker, foreground-process-group syscall inspection, silence fallback, and absolute timeout. macOS uses the verified prompt marker plus silence/timeout because it has no `/proc` syscall surface. A marker is not ready until printable prompt text arrives, including when the OSC marker and `PS1` are split across data callbacks; when bash prints the marker before the kernel publishes its return to the foreground process group, polling retains the candidate until bash ownership is observable. Unrecognized or unreadable process state is never a positive exact-idle signal. 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 even when its foreground process group is not observable yet; if that close fails, `PtyBackendCleanupError` separately preserves the cleanup failure for registry disposal. Incomplete terminal-control sequences are bounded by `maxReadBytes` and discarded through their terminator after crossing that limit; a trailing carriage return is carried across callbacks so split CRLF becomes one newline.
|
||||
|
||||
Send cancellation resolves the current foreground process group and delivers a real `SIGINT`; it never emulates interruption by writing `\x03`, so raw-mode programs remain cancellable. Close sends `SIGTERM` to descendants, waits, then sends `SIGKILL` to the union of captured survivors and newly scanned descendants so reparenting cannot hide a process from teardown. It verifies that every retained identity is gone or, on Linux, a non-executing zombie before stopping the shell; zombie entries are quiescent and are reaped as the shell exits. A survivor failure does not cache a permanently rejected close; a later close retries the teardown.
|
||||
|
||||
|
||||
@@ -288,11 +288,12 @@ export class LocalPtySession implements PtyBackendSession {
|
||||
if (sanitized.prompt) {
|
||||
const foregroundPgid = this.inspector.foregroundPgid(this.pid)
|
||||
if (this.shellPgid === undefined) this.shellPgid = foregroundPgid
|
||||
if (foregroundPgid !== undefined && foregroundPgid === this.shellPgid) {
|
||||
this.promptSeen = true
|
||||
this.promptTextSeen = sanitized.promptText === true
|
||||
this.lastOutputAt = Date.now()
|
||||
}
|
||||
// Bash can print PROMPT_COMMAND before the kernel publishes its return
|
||||
// to the foreground process group. Retain the marker; polling below is
|
||||
// the authority that accepts it only after bash owns the foreground.
|
||||
this.promptSeen = true
|
||||
this.promptTextSeen = sanitized.promptText === true
|
||||
this.lastOutputAt = Date.now()
|
||||
} else if (this.promptSeen && sanitized.promptText === true) {
|
||||
this.promptTextSeen = true
|
||||
}
|
||||
@@ -312,8 +313,11 @@ export class LocalPtySession implements PtyBackendSession {
|
||||
return
|
||||
}
|
||||
if (this.promptSeen && this.promptTextSeen && Date.now() - this.lastOutputAt >= this.config.pollIntervalMs) {
|
||||
this.settleActive('stdin_read')
|
||||
return
|
||||
const pgid = this.inspector.foregroundPgid(this.pid)
|
||||
if (this.shellPgid !== undefined && pgid === this.shellPgid) {
|
||||
this.settleActive('stdin_read')
|
||||
return
|
||||
}
|
||||
}
|
||||
const elapsed = Date.now() - operation.startedAt
|
||||
const startupHasOutput = !this.initializing || this.scrollback.snapshot().text.length > 0
|
||||
@@ -324,7 +328,13 @@ export class LocalPtySession implements PtyBackendSession {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (startupHasOutput && Date.now() - this.lastOutputAt >= this.config.idleSilenceMs) {
|
||||
// A complete owned marker is stronger evidence than silence, but can race
|
||||
// the kernel's foreground-PGID handoff. Once it is pending, wait for bash
|
||||
// ownership (or the absolute timeout) instead of misclassifying that race
|
||||
// as inferred idle.
|
||||
if (!(this.promptSeen && this.promptTextSeen)
|
||||
&& startupHasOutput
|
||||
&& Date.now() - this.lastOutputAt >= this.config.idleSilenceMs) {
|
||||
this.settleActive('inferred_idle')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,7 +187,12 @@ describe('LocalPtyBackend startup rollback', () => {
|
||||
kill() { exitListener?.({ exitCode: 0, signal: 15 }) },
|
||||
resize() {}, clear() {}, pause() {}, resume() {},
|
||||
} as IPty
|
||||
const backend = new LocalPtyBackend(ctx, config(), inspector, () => terminal)
|
||||
const backend = new LocalPtyBackend(
|
||||
ctx,
|
||||
config(),
|
||||
{ ...inspector, foregroundPgid: () => terminal.pid },
|
||||
() => terminal,
|
||||
)
|
||||
const session = await backend.spawn(spec(agent(ctx)))
|
||||
expect(session.motd).toBe('dsh> ')
|
||||
await session.close('test complete')
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('LocalPtySession readiness and output', () => {
|
||||
expect(session.motd).toBe('dsh> ')
|
||||
})
|
||||
|
||||
it('trusts prompt markers only while the startup shell owns the foreground group', async () => {
|
||||
it('retains a prompt marker until the startup shell regains the foreground group', async () => {
|
||||
vi.useFakeTimers()
|
||||
const terminal = new FakeTerminal()
|
||||
const inspector = new FakeInspector()
|
||||
@@ -297,13 +297,13 @@ describe('LocalPtySession readiness and output', () => {
|
||||
let settled = false
|
||||
void operation.done.then(() => { settled = true })
|
||||
inspector.pgid = 789
|
||||
terminal.emitData('\x1b]133;D;0\x07spoofed')
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
terminal.emitData('\x1b]133;D;0\x07dsh> ')
|
||||
await vi.advanceTimersByTimeAsync(60)
|
||||
expect(settled).toBe(false)
|
||||
|
||||
inspector.pgid = 456
|
||||
terminal.emitData('\x1b]133;D;0\x07dsh> ')
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(settled).toBe(true)
|
||||
expect((await operation.done).waitReason).toBe('stdin_read')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user