fix(daemon): fail closed on unsafe legacy close

This commit is contained in:
OrcaWin
2026-07-20 10:02:22 -07:00
parent c45e7be78a
commit 9cb24052be
3 changed files with 62 additions and 14 deletions
+2 -2
View File
@@ -712,7 +712,7 @@
"ssh",
"runtime"
],
"coverageNotes": "Live macOS Electron tests prove exact local PTY disappearance after parked-tab close and detached-pgid descendant death after agent close. Windows 2022 Actions red/green runs prove production store.removeWorktree(id, true) retires an agent PTY and its reparented native lock-holder before deleting the worktree, plain terminal close preserves an intentionally detached child, and natural agent-root exit publishes PTY exit only after its detached Job child is absent. Deterministic tests cover explicit and spawn-recognized native Windows agent Job selection, daemon session identity propagation, fail-closed Job teardown and bridge errors, safe failure when Job ownership is unavailable, adopted-v24 destructive-shutdown refusal, provider-scoped recovered routing, coalesced authoritative shutdown inventory and cached fallback/daemon duplicate refusal, duplicate-exit suppression, sleeping legacy ownership, daemon WSL exclusion, daemon-wide shutdown, daemon and SSH routing, local/daemon pending-snapshot ownership across natural exit, stale-root descendant-signal suppression, graceful-to-immediate kill upgrades, duplicate-kill completion sharing, locale-stable bounded/fresh/coalesced process-table reads, deadline-safe successor scans, cycle-safe linear descendant traversal, target-only escalation indexing, 32-wide bulk teardown, source-scan timestamp identity, same-second PID ambiguity, ordinary runtime close ownership, unified-only hydration, split ownership, pane detach transfer, restart alias hydration, and late-hook suppression; live Linux, WSL, SSH, and remote-runtime process evidence remains pending.",
"coverageNotes": "Live macOS Electron tests prove exact local PTY disappearance after parked-tab close and detached-pgid descendant death after agent close. Windows 2022 Actions red/green runs prove production store.removeWorktree(id, true) retires an agent PTY and its reparented native lock-holder before deleting the worktree, plain terminal close preserves an intentionally detached child, and natural agent-root exit publishes PTY exit only after its detached Job child is absent. Deterministic tests cover explicit and spawn-recognized native Windows agent Job selection, daemon session identity propagation, fail-closed Job teardown and bridge errors, safe failure when Job ownership is unavailable, adopted-v24 unclassified-shutdown refusal with known-WSL compatibility, provider-scoped recovered routing, coalesced authoritative shutdown inventory and cached fallback/daemon duplicate refusal, duplicate-exit suppression, sleeping legacy ownership, daemon WSL exclusion, daemon-wide shutdown, daemon and SSH routing, local/daemon pending-snapshot ownership across natural exit, stale-root descendant-signal suppression, graceful-to-immediate kill upgrades, duplicate-kill completion sharing, locale-stable bounded/fresh/coalesced process-table reads, deadline-safe successor scans, cycle-safe linear descendant traversal, target-only escalation indexing, 32-wide bulk teardown, source-scan timestamp identity, same-second PID ambiguity, ordinary runtime close ownership, unified-only hydration, split ownership, pane detach transfer, restart alias hydration, and late-hook suppression; live Linux, WSL, SSH, and remote-runtime process evidence remains pending.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/8628",
"https://github.com/stablyai/orca/pull/8706",
@@ -805,7 +805,7 @@
{
"file": "src/main/daemon/daemon-pty-adapter.test.ts",
"assertions": [
"an adopted Windows v24 daemon rejects deadline-bearing agent, plain, and WSL shutdown before connecting or sending kill while ordinary close remains compatible"
"an adopted Windows v24 daemon rejects unclassified ordinary, sleeping, and deadline-bearing shutdown before root-only kill while preserving ordinary and deadline close for attached known WSL sessions"
]
},
{
+55 -7
View File
@@ -532,18 +532,33 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
})
describe('shutdown', () => {
it('blocks every adopted Windows v24 session kind from deadline teardown but keeps ordinary close', async () => {
it('blocks unclassified Windows v24 close while preserving known WSL close', async () => {
const ensureConnectedSpy = vi
.spyOn(DaemonClient.prototype, 'ensureConnected')
.mockResolvedValue()
const requestSpy = vi.spyOn(DaemonClient.prototype, 'request').mockResolvedValue(undefined)
const ensureConnectedWithinSpy = vi
.spyOn(DaemonClient.prototype, 'ensureConnectedWithin')
.mockResolvedValue()
const requestSpy = vi
.spyOn(DaemonClient.prototype, 'request')
.mockImplementation(async (method) =>
method === 'createOrAttach'
? {
isNew: false,
pid: 123,
shellState: 'unsupported',
snapshot: null,
wslDistro: 'Ubuntu'
}
: undefined
)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const legacy = new DaemonPtyAdapter({ socketPath, tokenPath, protocolVersion: 24 })
try {
// Why: v24 did not record whether a session was agent, plain, or WSL,
// so migration safety must reject deadline teardown for all three.
// Why: inventory-only sessions lack attached WSL metadata, while v24
// could miss native agent identity; unclassified close must fail safe.
for (const id of ['legacy-agent', 'legacy-plain', 'legacy-wsl']) {
await expect(
legacy.shutdown(id, {
@@ -551,22 +566,55 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
deadlineMs: Date.now() + 5_000
})
).rejects.toThrow('requires daemon protocol 25 or newer')
await expect(legacy.shutdown(id, { immediate: true })).rejects.toThrow(
'restart Orca before closing adopted terminals'
)
await expect(legacy.shutdown(id, { immediate: true, keepHistory: true })).rejects.toThrow(
'restart Orca before closing adopted terminals'
)
}
expect(ensureConnectedSpy).not.toHaveBeenCalled()
expect(ensureConnectedWithinSpy).not.toHaveBeenCalled()
expect(requestSpy).not.toHaveBeenCalled()
await expect(legacy.shutdown('legacy-plain', { immediate: true })).resolves.toBeUndefined()
expect(requestSpy).toHaveBeenCalledWith(
await legacy.spawn({
sessionId: 'known-legacy-wsl',
cols: 80,
rows: 24,
terminalWindowsWslDistro: 'Ubuntu'
})
await expect(
legacy.shutdown('known-legacy-wsl', { immediate: true })
).resolves.toBeUndefined()
expect(requestSpy).toHaveBeenLastCalledWith(
'kill',
{ sessionId: 'legacy-plain', immediate: true },
{ sessionId: 'known-legacy-wsl', immediate: true },
undefined
)
await legacy.spawn({
sessionId: 'known-legacy-wsl-delete',
cols: 80,
rows: 24,
terminalWindowsWslDistro: 'Ubuntu'
})
await expect(
legacy.shutdown('known-legacy-wsl-delete', {
immediate: true,
deadlineMs: Date.now() + 5_000
})
).resolves.toBeUndefined()
expect(requestSpy).toHaveBeenLastCalledWith(
'kill',
{ sessionId: 'known-legacy-wsl-delete', immediate: true },
expect.any(Number)
)
} finally {
legacy.dispose()
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
requestSpy.mockRestore()
ensureConnectedWithinSpy.mockRestore()
ensureConnectedSpy.mockRestore()
}
})
+5 -5
View File
@@ -588,13 +588,13 @@ export class DaemonPtyAdapter implements IPtyProvider {
): Promise<void> {
if (
process.platform === 'win32' &&
opts.deadlineMs !== undefined &&
this.protocolVersion < WINDOWS_AGENT_JOB_PROTOCOL_VERSION
this.protocolVersion < WINDOWS_AGENT_JOB_PROTOCOL_VERSION &&
!this.wslDistrosBySessionId.has(id)
) {
// Why: an adopted v24 daemon could not own agent descendants at spawn;
// destructive deletion must not mistake its root-only exit for a safe drain.
// Why: v24 could miss native agent identity and never Job-owned descendants;
// only an attached, known WSL session is safe for ordinary root close.
throw new DaemonProtocolError(
`Windows destructive PTY shutdown requires daemon protocol ${WINDOWS_AGENT_JOB_PROTOCOL_VERSION} or newer`
`Windows PTY shutdown requires daemon protocol ${WINDOWS_AGENT_JOB_PROTOCOL_VERSION} or newer; restart Orca before closing adopted terminals`
)
}
// Why: shutdown can be the first lazy-client operation after restart; connect