fix(resource-manager): distinguish unprovable ownership from proven absence

Adversarial review of the previous commit found it committed the same class of
error it was fixing: it collapsed "no agent owns this" and "this provider cannot
tell me" into one boolean `false`, and both destructive paths read that as proof.

A daemon generation below the claim protocol, an older SSH relay, or the
in-process local fallback all list no owners for a session that may well have
one. `pty.ts` already encodes the rule at :613 — "only providers that serialize
claims may make listing absence authoritative" — and the new IPC row ignored it.
So after upgrading with a legacy daemon still holding a live agent terminal,
bulk cleanup would have destroyed it: exactly #8459, one layer down.

`hasAgentOwner: boolean` is now `agentOwnership: 'present' | 'absent' | 'unknown'`,
derived via `providesAgentSessionOwnerListings`. Only `absent` authorizes
destruction, so `unknown` protects and confirms.

Second defect, found independently by four review lenses: the deferred-SSH
bindings reached the bulk selector but not `mergeSnapshotAndSessions`, because
the merge call site re-listed the binding fields instead of reusing the object.
A deferred SSH session therefore rendered `bound: false`, and its single-row kill
skipped confirmation while bulk cleanup correctly spared it. The call site now
spreads `resourceSessionBindings`, and a parity test fails if any binding field
is re-listed inline — the drift itself is now impossible to reintroduce quietly.

The e2e ownership assertion was also weak: it checked only that a boolean
arrived. It now asserts the exact arm, and that the live local provider reports
`absent` rather than `unknown`, so a degenerate all-unknown implementation fails.
This commit is contained in:
OrcaWin
2026-07-27 11:34:02 -07:00
parent 7a393d1c98
commit 53a1db4b64
16 changed files with 313 additions and 74 deletions
@@ -150,18 +150,28 @@ test.describe('Resource Manager unbound-session safety', () => {
'A live warm-reattached session was unbound after restore completed; orphan cleanup would target it'
).toBe(true)
// The ownership field must survive the real IPC boundary, not just the unit-test mock:
// a structured-clone drop or a preload contract mismatch would surface here as undefined.
const ownershipReported = await secondLaunch.page.evaluate(async (expected: string) => {
// Ownership evidence must survive the real IPC boundary, not just the unit-test mock: a
// structured-clone drop or preload contract mismatch would surface here as undefined.
// Asserting the exact arm matters — a stub returning a constant would satisfy a typeof check.
const ownership = await secondLaunch.page.evaluate(async (expected: string) => {
const sessions = await window.api.pty.listSessions()
return sessions
.filter((s) => s.id === expected)
.map((s) => typeof s.hasAgentOwner === 'boolean')
return sessions.filter((s) => s.id === expected).map((s) => s.agentOwnership)
}, ptyId)
expect(
ownershipReported,
'pty:listSessions dropped hasAgentOwner across the real IPC boundary'
).toEqual([true])
ownership,
'pty:listSessions did not report a valid agentOwnership arm across the real IPC boundary'
).toHaveLength(1)
expect(
['present', 'absent', 'unknown'],
'agentOwnership crossed IPC as an unrecognized value'
).toContain(ownership[0])
// A plain shell under the live local provider must be PROVEN unowned, not merely unknown —
// otherwise the tri-state would be reporting "unknown" for everything and proving nothing.
expect(
ownership[0],
'The live local provider reported non-authoritative ownership for its own session'
).toBe('absent')
} finally {
if (firstApp) {
await session.close(firstApp).catch(() => {})