mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
feat(ssh): record the host-attested shell identity on the lease
A lease names a shell by ptyId, and ptyId alone cannot identify one: a replaced relay restarts its ids at pty-1, so the same id can name somebody else's shell. This adds the missing primitive — the incarnation the HOST attested — so a later change can tell "my shell" from "a different shell wearing its id". No behaviour changes yet; nothing reads the field. Only host-attested values are stored. The provider synthesizes a stand-in when a host reports none; that stand-in is first-write-wins and is dropped when provider state resets, so the same live shell can present a different one after a reconnect. Recording that would later read as a different shell and strand a live pane, so it is refused, and the synthesizer now shares the prefix constant with the predicate that rejects it. Leases are rebuilt field by field on load, so the normalizer had to learn the field too — adding it to the type alone drops it on every boot, which is how a fence ships silently permitting everything. The oracle reddens on exactly that.
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
nextAutomationOccurrenceAfter
|
||||
} from '../shared/automation-schedules'
|
||||
import { getAutomationLegacyRepoId } from '../shared/automation-run-identity'
|
||||
import { isRelayAttestedPtyIncarnationId } from '../shared/pty-incarnation'
|
||||
import { normalizeAutomationPrecheck } from '../shared/automation-precheck'
|
||||
import { normalizeProxyUrl } from '../shared/network-proxy'
|
||||
import { normalizeKagiSessionLink } from '../shared/browser-url'
|
||||
@@ -1619,7 +1620,13 @@ function normalizeSshRemotePtyLease(value: unknown): SshRemotePtyLease | null {
|
||||
createdAt: typeof raw.createdAt === 'number' ? raw.createdAt : now,
|
||||
updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : now,
|
||||
...(typeof raw.lastAttachedAt === 'number' ? { lastAttachedAt: raw.lastAttachedAt } : {}),
|
||||
...(typeof raw.lastDetachedAt === 'number' ? { lastDetachedAt: raw.lastDetachedAt } : {})
|
||||
...(typeof raw.lastDetachedAt === 'number' ? { lastDetachedAt: raw.lastDetachedAt } : {}),
|
||||
// Rebuilt field by field, so a lease is only as durable as this list — adding the field to the
|
||||
// type without adding it here drops it on every load. Synthesized values are refused rather
|
||||
// than carried: one written by an older build would later read as a different shell.
|
||||
...(isRelayAttestedPtyIncarnationId(raw.incarnationId)
|
||||
? { incarnationId: raw.incarnationId }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type SshPtyReceivingActivationLease
|
||||
} from './ssh-pty-notification-routing'
|
||||
import type { PtySourceReceivingActivation } from '../../shared/pty-source-receiving-activation'
|
||||
import { SYNTHESIZED_PTY_INCARNATION_PREFIX } from '../../shared/pty-incarnation'
|
||||
|
||||
export class SshPtyProviderOutputState {
|
||||
private readonly dataListeners = new Set<SshPtyDataCallback>()
|
||||
@@ -132,7 +133,7 @@ export class SshPtyProviderOutputState {
|
||||
this.rememberPtyIncarnation(relayPtyId, incarnationId)
|
||||
let resolved = this.incarnationByRelayPtyId.get(relayPtyId)
|
||||
if (!resolved) {
|
||||
resolved = `legacy:${this.providerGeneration}:${this.legacyIncarnationSerial++}:${relayPtyId}`
|
||||
resolved = `${SYNTHESIZED_PTY_INCARNATION_PREFIX}${this.providerGeneration}:${this.legacyIncarnationSerial++}:${relayPtyId}`
|
||||
this.incarnationByRelayPtyId.set(relayPtyId, resolved)
|
||||
}
|
||||
return resolved
|
||||
|
||||
@@ -223,6 +223,24 @@ describe('STA-3077: existing duplicate leases are healed, not revived', () => {
|
||||
2
|
||||
)
|
||||
})
|
||||
|
||||
// A lease is rebuilt field by field on load, so a field added to the type but not to the
|
||||
// normalizer is dropped on every boot — it would look right in memory and be gone on the next
|
||||
// start, which is how a fence ships silently permitting everything. A synthesized identity is
|
||||
// refused outright rather than carried: it is not stable across reconnects, so one persisted by
|
||||
// another build would later read as a different shell and strand a live pane.
|
||||
it('keeps a host-attested incarnation across a reload and refuses a synthesized one', async () => {
|
||||
const store = await createStore({
|
||||
sshRemotePtyLeases: [
|
||||
{ ...leaseFor('relay-pty-a', 1), createdAt: 1, incarnationId: 'inc-host-a' },
|
||||
{ ...leaseFor('relay-pty-b', 2), createdAt: 2, incarnationId: 'legacy:23:0:relay-pty-b' }
|
||||
]
|
||||
})
|
||||
|
||||
const byPtyId = new Map(store.getSshRemotePtyLeases(TARGET).map((l) => [l.ptyId, l]))
|
||||
expect(byPtyId.get('relay-pty-a')?.incarnationId).toBe('inc-host-a')
|
||||
expect(byPtyId.get('relay-pty-b')?.incarnationId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('STA-3077: reattach binds panes, it never creates them', () => {
|
||||
|
||||
@@ -3,3 +3,16 @@ export type PtyIncarnationId = string
|
||||
export function isPtyIncarnationId(value: unknown): value is PtyIncarnationId {
|
||||
return typeof value === 'string' && value.length > 0 && value.length <= 128
|
||||
}
|
||||
|
||||
/** Stand-in minted locally when a host reported no incarnation of its own. */
|
||||
export const SYNTHESIZED_PTY_INCARNATION_PREFIX = 'legacy:'
|
||||
|
||||
/**
|
||||
* True only for an incarnation the HOST minted. A synthesized one is first-write-wins and is
|
||||
* dropped when provider state resets, so the same live shell can present a different value on a
|
||||
* later reconnect. Durable identity may only be recorded from — and compared against — a value the
|
||||
* host itself attested, or a reconnect would read its own stand-in as a different shell.
|
||||
*/
|
||||
export function isRelayAttestedPtyIncarnationId(value: unknown): value is PtyIncarnationId {
|
||||
return isPtyIncarnationId(value) && !value.startsWith(SYNTHESIZED_PTY_INCARNATION_PREFIX)
|
||||
}
|
||||
|
||||
@@ -203,6 +203,14 @@ export type SshRemotePtyLease = {
|
||||
updatedAt: number
|
||||
lastAttachedAt?: number
|
||||
lastDetachedAt?: number
|
||||
/**
|
||||
* The shell this record names, as the HOST identified it. `ptyId` alone cannot: a replaced relay
|
||||
* restarts its ids at `pty-1`, so the same id can name somebody else's shell. Only a host-attested
|
||||
* value is stored — a locally synthesized stand-in is not stable across reconnects and would read
|
||||
* as a different shell. Absent on legacy rows and on hosts that report none, which stays
|
||||
* adoption-eligible and must never act as a claim that subtracts a shell.
|
||||
*/
|
||||
incarnationId?: string
|
||||
}
|
||||
|
||||
/** Main-owned relay lease needed to reclaim PTY delivery after a desktop restart. */
|
||||
|
||||
Reference in New Issue
Block a user