Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/main/runtime/orchestration/db/principal-match.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { isEquivalentPrincipal } from './principal-match'

describe('principal-match', () => {
const leaf = '11111111-1111-4111-8111-111111111111'

it('treats pane principals with the same leaf as equivalent across a tab-half remint', () => {
expect(isEquivalentPrincipal(`pane:tab-a:${leaf}`, `pane:tab-a:${leaf}`)).toBe(true)
expect(isEquivalentPrincipal(`pane:tab-a:${leaf}`, `pane:tab-b:${leaf}`)).toBe(true)
expect(
isEquivalentPrincipal(`pane:tab-a:${leaf}`, 'pane:tab-a:22222222-2222-4222-8222-222222222222')
).toBe(false)
})

it('matches session principals exactly and only exactly', () => {
expect(isEquivalentPrincipal('session:s-1', 'session:s-1')).toBe(true)
expect(isEquivalentPrincipal('session:s-1', 'session:s-2')).toBe(false)
})

it('requires an exact match for unparseable values', () => {
expect(isEquivalentPrincipal('legacy-value', 'legacy-value')).toBe(true)
expect(isEquivalentPrincipal('legacy-value', 'other-value')).toBe(false)
expect(isEquivalentPrincipal('unknown:payload', 'unknown:payload')).toBe(true)
expect(isEquivalentPrincipal('unknown:payload', 'unknown:other')).toBe(false)
})

it('NEVER bridges pane and session principals, in either direction', () => {
// The invariant, not an edge case: a structured pane key's tab half embeds the session id in
// plain text, so a caller who learns a session id can fabricate this pane key. Only the
// random leaf is a credential; matching it to the session principal would hand the attacker
// the coordinator's Run binding.
const realSessionId = 'session-alpha-1'
const fabricated = `pane:structured-agent-session-${realSessionId}:${leaf}`
expect(isEquivalentPrincipal(fabricated, `session:${realSessionId}`)).toBe(false)
expect(isEquivalentPrincipal(`session:${realSessionId}`, fabricated)).toBe(false)
})
})
27 changes: 27 additions & 0 deletions src/main/runtime/orchestration/db/principal-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { parseOrchestrationPrincipal } from '../../../../shared/orchestration-principal'
import { isEquivalentPaneKey } from './pane-key-match'

/**
* Equivalence over serialized `OrchestrationPrincipal` strings.
*
* INVARIANT: cross-kind is NEVER equivalent, in either direction. A structured pane key's tab half
* embeds the session id in plain text (`structured-agent-session-<sessionId>:<leaf>`), so deriving
* `session:` from `pane:` here would let anyone who learns a session id fabricate a "matching"
* pane key and reach the coordinator's Run binding through caller-supplied-pane-key paths. That
* derivation is legal exactly once — PR 1's one-time server-side backfill/dual-write over pane
* keys the host itself wrote (`principalFromPaneKey`) — and never at request/match time: the
* random leaf is the only real credential. The backfill rule does NOT generalize to matching.
*/
export function isEquivalentPrincipal(a: string, b: string): boolean {
if (a === b) {
return true
}
const aParsed = parseOrchestrationPrincipal(a)
const bParsed = parseOrchestrationPrincipal(b)
// Session principals match exactly (handled above); unparseable or cross-kind never match.
if (aParsed?.kind !== 'pane' || bParsed?.kind !== 'pane') {
return false
}
// Leaf-UUID rule preserved so break-out remints keep matching.
return isEquivalentPaneKey(aParsed.paneKey, bParsed.paneKey)
}
37 changes: 20 additions & 17 deletions src/main/runtime/orchestration/db/runs/run-binding.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { principalFromPaneKey } from '../../../../../shared/orchestration-principal'
import type { RunRow } from '../../types'
import { OrchestrationError } from '../../orchestration-error'
import { LEGACY_CONTRACT_VERSION } from '../contract-constants'
import { isEquivalentPaneKey } from '../pane-key-match'
import { isEquivalentPrincipal } from '../principal-match'
import type { OrchestrationDb } from '../orchestration-db'
import { runCoordinatorBinding, type RunCoordinatorParam } from './run-coordinator-binding'

export function bindRun(
this: OrchestrationDb,
params: {
runId: string
coordinatorHandle: string
coordinatorPaneKey: string
takeoverLegacy?: boolean
legacyCoordinatorAuthority?: {
runId: string
Expand All @@ -19,8 +18,9 @@ export function bindRun(
paneKey: string
consumerGeneration: number
}
}
} & RunCoordinatorParam
): RunRow | undefined {
const coordinator = runCoordinatorBinding(params)
this.db.exec('BEGIN IMMEDIATE')
try {
const run = this.getRunRaw(params.runId)
Expand All @@ -29,15 +29,16 @@ export function bindRun(
return undefined
}
const sameBinding =
run.coordinator_pane_key !== null &&
isEquivalentPaneKey(run.coordinator_pane_key, params.coordinatorPaneKey)
run.coordinator_principal !== null &&
isEquivalentPrincipal(run.coordinator_principal, coordinator.principalId)
const adoption = this.getLegacyAdoption()
const adoptedRun = adoption?.adopted_run_id === params.runId
const legacyAuthority = params.legacyCoordinatorAuthority
const legacyPrincipalId = legacyAuthority?.principalId
const legacyPrincipal = legacyPrincipalId
? this.getLegacyCompatibilityPrincipal(legacyPrincipalId)
: undefined
// Why: a null handle or pane key never proves — a session binding yields false, matching today.
const provenLegacyBinding = Boolean(
adoptedRun &&
legacyAuthority &&
Expand All @@ -49,8 +50,9 @@ export function bindRun(
legacyPrincipal.status === 'committed' &&
legacyPrincipal.terminal_handle === legacyAuthority.terminalHandle &&
isEquivalentPaneKey(legacyPrincipal.pane_key, legacyAuthority.paneKey) &&
params.coordinatorHandle === legacyAuthority.terminalHandle &&
isEquivalentPaneKey(params.coordinatorPaneKey, legacyAuthority.paneKey)
coordinator.terminalHandle === legacyAuthority.terminalHandle &&
coordinator.paneKey !== null &&
isEquivalentPaneKey(coordinator.paneKey, legacyAuthority.paneKey)
)
if (legacyAuthority && !provenLegacyBinding) {
throw new OrchestrationError(
Expand Down Expand Up @@ -81,15 +83,16 @@ export function bindRun(
const takeoverAlreadyApplied = Boolean(
params.takeoverLegacy &&
sameBinding &&
run.coordinator_handle === params.coordinatorHandle &&
coordinator.terminalHandle !== null &&
run.coordinator_handle === coordinator.terminalHandle &&
coordinatorPrincipal?.status !== 'committed'
)
const replacesLegacyCoordinator = Boolean(
adoptedRun &&
!provenLegacyBinding &&
retainedCoordinatorHandle &&
(params.takeoverLegacy ||
retainedCoordinatorHandle !== params.coordinatorHandle ||
retainedCoordinatorHandle !== coordinator.terminalHandle ||
!sameBinding)
)
if (params.takeoverLegacy && !adoptedRun) {
Expand All @@ -110,10 +113,9 @@ export function bindRun(
}
)
}
const incomingPrincipal = principalFromPaneKey(params.coordinatorPaneKey)
this.unbindOtherRunsForPane(params.coordinatorPaneKey, params.runId)
this.unbindOtherRunsForPrincipal(coordinator.principalId, params.runId)
for (const handle of new Set(
[run.coordinator_handle, params.coordinatorHandle].filter((value): value is string =>
[run.coordinator_handle, coordinator.terminalHandle].filter((value): value is string =>
Boolean(value)
)
)) {
Expand All @@ -123,14 +125,15 @@ export function bindRun(
if (
(params.takeoverLegacy && !takeoverAlreadyApplied) ||
!sameBinding ||
run.coordinator_handle !== params.coordinatorHandle
run.coordinator_handle !== coordinator.terminalHandle
) {
if (adoptedRun && (params.takeoverLegacy || !activeLegacyAssignment)) {
if (
coordinatorPrincipal?.status === 'committed' &&
(params.takeoverLegacy ||
coordinatorPrincipal.terminal_handle !== params.coordinatorHandle ||
!isEquivalentPaneKey(coordinatorPrincipal.pane_key, params.coordinatorPaneKey))
coordinatorPrincipal.terminal_handle !== coordinator.terminalHandle ||
coordinator.paneKey === null ||
!isEquivalentPaneKey(coordinatorPrincipal.pane_key, coordinator.paneKey))
) {
this.setLegacyCompatibilityPrincipalStatus(coordinatorPrincipal.id, 'revoked')
}
Expand All @@ -143,7 +146,7 @@ export function bindRun(
updated_at = datetime('now')
WHERE id = ?`
)
.run(params.coordinatorHandle, params.coordinatorPaneKey, incomingPrincipal, params.runId)
.run(coordinator.terminalHandle, coordinator.paneKey, coordinator.principalId, params.runId)
this.fenceOutstandingDelivery(params.runId)
if (params.takeoverLegacy || replacesLegacyCoordinator) {
this.promoteLegacyCoordinatorMailForTakeover(params.runId, retainedCoordinatorHandle)
Expand Down
23 changes: 23 additions & 0 deletions src/main/runtime/orchestration/db/runs/run-coordinator-binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { principalFromPaneKey } from '../../../../../shared/orchestration-principal'
import type { RunCoordinatorBinding } from '../../types'

/** Either the resolver's opaque binding or the legacy handle+pane shape existing callers pass. */
export type RunCoordinatorParam =
| { coordinator: RunCoordinatorBinding }
| { coordinatorHandle: string; coordinatorPaneKey: string }

/** Legacy shape normalizes through the same classification as PR 1's dual-write derivation. */
export function runCoordinatorBinding(params: RunCoordinatorParam): RunCoordinatorBinding {
if ('coordinator' in params) {
return params.coordinator
}
const principalId = principalFromPaneKey(params.coordinatorPaneKey)
if (!principalId) {
throw new Error('A run coordinator binding requires a non-empty pane key.')
}
return {
principalId,
terminalHandle: params.coordinatorHandle,
paneKey: params.coordinatorPaneKey
}
}
22 changes: 10 additions & 12 deletions src/main/runtime/orchestration/db/runs/run-create.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { principalFromPaneKey } from '../../../../../shared/orchestration-principal'
import type { RunRow } from '../../types'
import { generateId } from '../generated-id'
import type { OrchestrationDb } from '../orchestration-db'
import { runCoordinatorBinding, type RunCoordinatorParam } from './run-coordinator-binding'

// ── Runs ──

export function createRun(
this: OrchestrationDb,
params: {
objective: string
coordinatorHandle: string
coordinatorPaneKey: string
}
params: { objective: string } & RunCoordinatorParam
): RunRow {
const id = generateId('run')
const coordinatorPrincipal = principalFromPaneKey(params.coordinatorPaneKey)
const coordinator = runCoordinatorBinding(params)
this.db.exec('BEGIN IMMEDIATE')
try {
this.unbindOtherRunsForPane(params.coordinatorPaneKey)
this.unbindOtherRunsForPrincipal(coordinator.principalId)
this.db
.prepare(
`INSERT INTO runs (
Expand All @@ -28,11 +24,13 @@ export function createRun(
.run(
id,
params.objective,
params.coordinatorHandle,
params.coordinatorPaneKey,
coordinatorPrincipal
coordinator.terminalHandle,
coordinator.paneKey,
coordinator.principalId
)
this.rememberRunCoordinatorHandle(id, params.coordinatorHandle)
if (coordinator.terminalHandle !== null) {
this.rememberRunCoordinatorHandle(id, coordinator.terminalHandle)
}
this.db.exec('COMMIT')
} catch (error) {
this.db.exec('ROLLBACK')
Expand Down
64 changes: 59 additions & 5 deletions src/main/runtime/orchestration/db/runs/run-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
RUN_PANE_KEY_MATCH_SUFFIX_SQL,
paneKeyMatchSuffix
} from '../pane-key-match'
import { isEquivalentPrincipal } from '../principal-match'
import { parseOrchestrationPrincipal } from '../../../../../shared/orchestration-principal'
import { exposeRunTimestamps } from '../utc-timestamp'
import { encodeRunListCursor, decodeRunListCursor } from '../run-list-cursor'
import type { RunListPage } from '../run-list-page'
Expand All @@ -22,6 +24,20 @@ const RUNS_BOUND_TO_PANE_SQL = `SELECT ${RUN_COLUMN_LIST} FROM runs
WHERE coordinator_pane_key IS NOT NULL AND legacy = 0
AND ${RUN_PANE_KEY_MATCH_SUFFIX_SQL} = ?
ORDER BY rowid`
// Why: exact principal lookup uses the principal index; pane principals also need the existing
// pane-leaf index so a tab-half remint does not scan historical runs. The JS equivalence check
// remains authoritative for cross-kind and malformed values.
const RUN_COLUMN_LIST_WITH_MATCH_ROWID = `${RUN_COLUMN_LIST}, rowid AS principal_match_rowid`
const RUNS_BOUND_TO_PRINCIPAL_EXACT_WITH_ROWID_SQL = `SELECT ${RUN_COLUMN_LIST_WITH_MATCH_ROWID} FROM runs
WHERE coordinator_principal = ? AND legacy = 0
ORDER BY rowid`
const RUNS_BOUND_TO_PRINCIPAL_PANE_SUFFIX_WITH_ROWID_SQL = `SELECT ${RUN_COLUMN_LIST_WITH_MATCH_ROWID} FROM runs
WHERE legacy = 0 AND coordinator_principal LIKE 'pane:%'
AND coordinator_pane_key IS NOT NULL
AND ${RUN_PANE_KEY_MATCH_SUFFIX_SQL} = ?
ORDER BY rowid`

type PrincipalRunCandidate = RunRow & { principal_match_rowid: number }

export function getRun(this: OrchestrationDb, id: string): RunRow | undefined {
const run = this.getRunRaw(id)
Expand Down Expand Up @@ -118,16 +134,50 @@ export function runsBoundToPane(this: OrchestrationDb, paneKey: string): RunRow[
)
}

export function getCurrentRunForPrincipal(
this: OrchestrationDb,
principalId: string
): RunRow | undefined {
const run = this.runsBoundToPrincipal(principalId)[0]
return run ? exposeRunTimestamps(run) : undefined
}

export function runsBoundToPrincipal(this: OrchestrationDb, principalId: string): RunRow[] {
const parsed = parseOrchestrationPrincipal(principalId)
const candidates = (
parsed?.kind === 'pane'
? [
...this.db.prepare(RUNS_BOUND_TO_PRINCIPAL_EXACT_WITH_ROWID_SQL).all(principalId),
...this.db
.prepare(RUNS_BOUND_TO_PRINCIPAL_PANE_SUFFIX_WITH_ROWID_SQL)
.all(paneKeyMatchSuffix(parsed.paneKey))
]
: this.db.prepare(RUNS_BOUND_TO_PRINCIPAL_EXACT_WITH_ROWID_SQL).all(principalId)
) as PrincipalRunCandidate[]
const unique = new Map<string, PrincipalRunCandidate>()
for (const candidate of candidates) {
unique.set(candidate.id, candidate)
}
return [...unique.values()]
.sort((a, b) => a.principal_match_rowid - b.principal_match_rowid)
.map(({ principal_match_rowid: _rowid, ...run }) => run)
.filter(
(run) =>
run.coordinator_principal !== null &&
isEquivalentPrincipal(run.coordinator_principal, principalId)
)
}

export function getRunRaw(this: OrchestrationDb, id: string): RunRow | undefined {
return this.db.prepare(RUN_BY_ID_SQL).get(id) as RunRow | undefined
}

export function unbindOtherRunsForPane(
export function unbindOtherRunsForPrincipal(
this: OrchestrationDb,
paneKey: string,
principalId: string,
exceptRunId?: string
): void {
for (const run of this.runsBoundToPane(paneKey)) {
for (const run of this.runsBoundToPrincipal(principalId)) {
if (run.id !== exceptRunId) {
if (run.coordinator_handle) {
this.routeAllUnreadDirectMessagesToRunMailbox(run.id, run.coordinator_handle)
Expand Down Expand Up @@ -164,8 +214,10 @@ export type RunLookupMethods = {
listRuns: typeof listRuns
getCurrentRunForPane: typeof getCurrentRunForPane
runsBoundToPane: typeof runsBoundToPane
getCurrentRunForPrincipal: typeof getCurrentRunForPrincipal
runsBoundToPrincipal: typeof runsBoundToPrincipal
getRunRaw: typeof getRunRaw
unbindOtherRunsForPane: typeof unbindOtherRunsForPane
unbindOtherRunsForPrincipal: typeof unbindOtherRunsForPrincipal
requireRun: typeof requireRun
fenceOutstandingDelivery: typeof fenceOutstandingDelivery
}
Expand All @@ -178,8 +230,10 @@ export function attachRunLookup(ctor: { prototype: object }): void {
listRuns,
getCurrentRunForPane,
runsBoundToPane,
getCurrentRunForPrincipal,
runsBoundToPrincipal,
getRunRaw,
unbindOtherRunsForPane,
unbindOtherRunsForPrincipal,
requireRun,
fenceOutstandingDelivery
})
Expand Down
Loading