Resolves #17157
Refs #16737
The identity-binding third signal is now typed end-to-end: the plane client stamps planeBlockerCode: 'viewer-binding-unavailable' at the two refusal sites that KNOW they are binding-class (the identity-oracle's no-canonical-identity and identity-mismatch refusals in connectProven — "the one refusal that protects every admission decision downstream"), the thrown session-lost errors CARRY the stamp verbatim (a shared sessionLostError builder copies, never derives), the presence adapter passes a recognized code through as capability.reasonCode behind its own closed set (PRESENCE_CAPABILITY_REASON_CODES — an unrecognized stamp is dropped, never admitted), and the cockpit DTO delivers it intact. A post-admission viewer-binding failure — binding drift or a failed reacquisition — is now RENDERABLE as "binding unavailable" from a typed field — never inferred from prose, and never mistakable for an empty fleet (every roster row still answers unknown); the initial-admission refusal stays terminal by design (devFleetServer exits before any capability DTO exists) and fabricates nothing. Ambiguous failures (transport, timeout, 5xx) deliberately stay unstamped: ambiguity never classifies, per the fleetMailboxMirrorAdapter anti-misclassification precedent (bare-Unauthorized warning) this design follows.
Evidence: L2 (scoped unit receipts across all three chain hops — client refusal/throw sites via the scripted-plane harness, adapter passthrough via the reader seam, cockpit DTO via the pure map) → L2 sufficient for the close-target ACs (every AC adapter/client/DTO-side and specable). Residual: C3's render-side consumption of the typed field (the pane state itself), Residual-Owner: #16737.
Deltas from ticket
One narrowing, stated: the ticket's Fix step 1 mentioned enumerating plane-side admission-denial texts as a second stamp class; the enumeration found the presence path's binding-class failures all route through the identity-oracle refusals (who_is_online is roster-wide — no per-subject admission scope like the mirror's CAN_READ_INBOX_OF), so the two connectProven sites are the complete stamp set for this chain. No tool-text matching was added anywhere — which is the stricter reading of the ticket's own anti-misclassification requirement.
Post-merge body correction (reviewer phase-wording catch, @neo-gpt): the renderability claim above originally said "a broken viewer binding" unqualified, overclaiming by one phase — what this PR types is the post-admission phase only; the initial binding refusal was and remains terminal before any capability DTO exists. No code change.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/planeMailboxClient.spec.mjs test/playwright/unit/ai/services/fleet/fleetPresenceStateAdapter.spec.mjs → 45 passed (2.8s).
npm run test-unit -- test/playwright/unit/ai/services/fleet/fleetCockpitStatus.spec.mjs → 21 passed (2.4s).
- New coverage: mismatch refusal
toEqual now VERIFIES the stamp; the changed-identity reconnect test asserts the thrown error carries planeBlockerCode; ambiguity-never-classifies (500-class → no stamp); binding-classified read → reasonCode + full roster of unknown rows (never an empty fleet); unclassified failure → no reasonCode field; unrecognized stamp → dropped (closed-set guard); degraded presence capability with reasonCode reaches the cockpit DTO intact.
- Surface
ai/services/fleet (plane client + presence axis + cockpit map): the three spec files above are the existing coverage, extended; no app/UI surface touched.
Post-Merge Validation
Branch note: sibling PR #17154 (leg 1, in verification) touches the same adapter on adjacent lines (envelope capturedAt line vs the added reasonCode spread; different let/const blocks) — zero-to-trivial merge in either landing order; whichever lands second rebases mechanically.
Authored by Clio (Claude Fable 5, Claude Code). Session 1deebbe1-b7e6-4f76-b39d-9cfcbe342596.
Resolves #17157
Refs #16737
The identity-binding third signal is now typed end-to-end: the plane client stamps
planeBlockerCode: 'viewer-binding-unavailable'at the two refusal sites that KNOW they are binding-class (the identity-oracle's no-canonical-identity and identity-mismatch refusals inconnectProven— "the one refusal that protects every admission decision downstream"), the thrown session-lost errors CARRY the stamp verbatim (a sharedsessionLostErrorbuilder copies, never derives), the presence adapter passes a recognized code through ascapability.reasonCodebehind its own closed set (PRESENCE_CAPABILITY_REASON_CODES— an unrecognized stamp is dropped, never admitted), and the cockpit DTO delivers it intact. A post-admission viewer-binding failure — binding drift or a failed reacquisition — is now RENDERABLE as "binding unavailable" from a typed field — never inferred from prose, and never mistakable for an empty fleet (every roster row still answersunknown); the initial-admission refusal stays terminal by design (devFleetServerexits before any capability DTO exists) and fabricates nothing. Ambiguous failures (transport, timeout, 5xx) deliberately stay unstamped: ambiguity never classifies, per thefleetMailboxMirrorAdapteranti-misclassification precedent (bare-Unauthorizedwarning) this design follows.Evidence: L2 (scoped unit receipts across all three chain hops — client refusal/throw sites via the scripted-plane harness, adapter passthrough via the reader seam, cockpit DTO via the pure map) → L2 sufficient for the close-target ACs (every AC adapter/client/DTO-side and specable). Residual: C3's render-side consumption of the typed field (the pane state itself), Residual-Owner: #16737.
Deltas from ticket
One narrowing, stated: the ticket's Fix step 1 mentioned enumerating plane-side admission-denial texts as a second stamp class; the enumeration found the presence path's binding-class failures all route through the identity-oracle refusals (who_is_online is roster-wide — no per-subject admission scope like the mirror's
CAN_READ_INBOX_OF), so the twoconnectProvensites are the complete stamp set for this chain. No tool-text matching was added anywhere — which is the stricter reading of the ticket's own anti-misclassification requirement.Post-merge body correction (reviewer phase-wording catch, @neo-gpt): the renderability claim above originally said "a broken viewer binding" unqualified, overclaiming by one phase — what this PR types is the post-admission phase only; the initial binding refusal was and remains terminal before any capability DTO exists. No code change.
Test Evidence
npm run test-unit -- test/playwright/unit/ai/planeMailboxClient.spec.mjs test/playwright/unit/ai/services/fleet/fleetPresenceStateAdapter.spec.mjs→ 45 passed (2.8s).npm run test-unit -- test/playwright/unit/ai/services/fleet/fleetCockpitStatus.spec.mjs→ 21 passed (2.4s).toEqualnow VERIFIES the stamp; the changed-identity reconnect test asserts the thrown error carriesplaneBlockerCode; ambiguity-never-classifies (500-class → no stamp); binding-classified read →reasonCode+ full roster ofunknownrows (never an empty fleet); unclassified failure → noreasonCodefield; unrecognized stamp → dropped (closed-set guard); degraded presence capability withreasonCodereaches the cockpit DTO intact.ai/services/fleet(plane client + presence axis + cockpit map): the three spec files above are the existing coverage, extended; no app/UI surface touched.Post-Merge Validation
Branch note: sibling PR #17154 (leg 1, in verification) touches the same adapter on adjacent lines (envelope
capturedAtline vs the addedreasonCodespread; different let/const blocks) — zero-to-trivial merge in either landing order; whichever lands second rebases mechanically.Authored by Clio (Claude Fable 5, Claude Code). Session 1deebbe1-b7e6-4f76-b39d-9cfcbe342596.