mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/ua-b-finish
This commit is contained in:
@@ -25,7 +25,14 @@ describe('Electron runtime package contract', () => {
|
||||
}
|
||||
|
||||
it('keeps root postinstall as the single Electron binary install owner', () => {
|
||||
expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs')
|
||||
// Why not an exact match: the invariant is that the root postinstall owns the Electron
|
||||
// binary install, not that nothing else may run after it. Pinning the whole string made
|
||||
// any unrelated chained step (a lint-plugin sync, say) a CI failure for every open PR.
|
||||
const postinstall = packageJson.scripts.postinstall
|
||||
const steps = postinstall.split('&&').map((step) => step.trim())
|
||||
expect(steps[0]).toBe('node config/scripts/rebuild-native-deps.mjs')
|
||||
// No later step may take over the Electron install the first step owns.
|
||||
expect(steps.slice(1).join(' ')).not.toMatch(/electron/i)
|
||||
expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 56m">
|
||||
<title>downloads: 56m</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 58m">
|
||||
<title>downloads: 58m</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
|
||||
<text x="37" y="14">downloads</text>
|
||||
<text x="90" y="15" fill="#010101" fill-opacity=".3">56m</text>
|
||||
<text x="90" y="14">56m</text>
|
||||
<text x="90" y="15" fill="#010101" fill-opacity=".3">58m</text>
|
||||
<text x="90" y="14">58m</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 935 B After Width: | Height: | Size: 935 B |
+1
-1
@@ -97,7 +97,7 @@
|
||||
"build": "pnpm run build:desktop && pnpm run build:native",
|
||||
"build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer",
|
||||
"build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer",
|
||||
"postinstall": "node config/scripts/rebuild-native-deps.mjs && node config/scripts/sync-anti-slop-plugin.mjs",
|
||||
"postinstall": "node config/scripts/rebuild-native-deps.mjs",
|
||||
"rebuild:electron": "node config/scripts/rebuild-native-deps.mjs",
|
||||
"reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs",
|
||||
"reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs",
|
||||
|
||||
@@ -54,15 +54,17 @@ export class OrcaRuntimeWithBindPtyIncarnationHandle extends OrcaRuntimeWithBuil
|
||||
for (const [ptyId, retained] of this.handleByPtyIncarnation) {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
const leaves = this.getLeavesForPty(ptyId)
|
||||
if (
|
||||
!pty ||
|
||||
pty.incarnationId !== retained.incarnationId ||
|
||||
leaves.length !== 1 ||
|
||||
this.handleByPtyId.has(ptyId)
|
||||
) {
|
||||
// Why: a handle issued before the host reported the incarnation is un-fenced, so
|
||||
// learning it is not a replacement; only a known-to-different incarnation is.
|
||||
const incarnationReplaced =
|
||||
retained.incarnationId !== null &&
|
||||
pty !== undefined &&
|
||||
pty.incarnationId !== retained.incarnationId
|
||||
if (!pty || incarnationReplaced || leaves.length !== 1 || this.handleByPtyId.has(ptyId)) {
|
||||
this.invalidatePtyIncarnationHandle(ptyId)
|
||||
continue
|
||||
}
|
||||
retained.incarnationId = pty.incarnationId
|
||||
this.bindPtyIncarnationHandle(retained, leaves[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,40 @@ describe('runtime terminal handle incarnation fencing', () => {
|
||||
await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ handle, status: 'running' })
|
||||
})
|
||||
|
||||
it('keeps a listed handle when graph sync learns the incarnation after issue', async () => {
|
||||
// Daemon-hosted PTYs are recorded from first output before the spawn commit reports an
|
||||
// incarnation, so the handle is issued un-fenced and must survive learning it.
|
||||
const { runtime } = makeRuntime()
|
||||
runtime.registerPty(PTY_ID, WORKTREE_ID, 'target', { tabId: TAB_ID, leafId: LEAF_ID })
|
||||
syncGraph(runtime)
|
||||
const [listed] = (await runtime.listTerminals()).terminals
|
||||
|
||||
register(runtime, 'incarnation-learned')
|
||||
syncGraph(runtime)
|
||||
|
||||
await expect(runtime.readTerminal(listed.handle)).resolves.toMatchObject({
|
||||
handle: listed.handle,
|
||||
status: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('stales a listed handle when graph sync sees a replaced incarnation', async () => {
|
||||
const { runtime } = makeRuntime()
|
||||
register(runtime, 'incarnation-old')
|
||||
syncGraph(runtime)
|
||||
const [listed] = (await runtime.listTerminals()).terminals
|
||||
|
||||
// Rotate the record directly so reconcile is the only fence exercised.
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test reaches the runtime's protected pty record map to bypass the registerPty fence.
|
||||
const internals = runtime as unknown as {
|
||||
ptysById: Map<string, { incarnationId: string | null }>
|
||||
}
|
||||
internals.ptysById.get(PTY_ID)!.incarnationId = 'incarnation-new'
|
||||
syncGraph(runtime)
|
||||
|
||||
await expect(runtime.readTerminal(listed.handle)).rejects.toThrow('terminal_handle_stale')
|
||||
})
|
||||
|
||||
it('invalidates a direct handle when a reused PTY id gets a new incarnation', async () => {
|
||||
const { runtime, writes } = makeRuntime()
|
||||
const staleHandle = runtime.preAllocateHandleForPty(PTY_ID)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import { isTerminalLeafId, parsePaneKey } from '../../shared/stable-pane-id'
|
||||
import { structuredAgentSessionPaneKey } from '../../shared/structured-agent-session-projection'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../shared/structured-agent-session-projection'
|
||||
import { selectExactWorkerProviderSession } from './orchestration/worker-provider-session'
|
||||
import { structuredWorkerChildIdentityEnv } from './structured-worker-child-identity-env'
|
||||
import {
|
||||
@@ -85,12 +88,57 @@ describe('structured worker identity', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("accepts a persisted pane key for its own session and rejects another session's", () => {
|
||||
it('accepts only the registered pane key for its session', () => {
|
||||
const handle = mintStructuredWorkerHandle()
|
||||
const paneKey = mintStructuredWorkerPaneKey(SESSION_ID)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(true)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(paneKey, 'another-session-id')).toBe(false)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession('not-a-pane-key', SESSION_ID)).toBe(false)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(null, SESSION_ID)).toBe(false)
|
||||
structuredWorkerIdentities.register({
|
||||
handle,
|
||||
sessionId: SESSION_ID,
|
||||
agent: 'claude',
|
||||
paneKey,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(SESSION_ID),
|
||||
worktreeId: 'wt_1',
|
||||
hostScope: { kind: 'local', hostId: 'local' }
|
||||
})
|
||||
try {
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(true)
|
||||
expect(
|
||||
structuredWorkerPaneKeyBelongsToSession(mintStructuredWorkerPaneKey(SESSION_ID), SESSION_ID)
|
||||
).toBe(false)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(paneKey, 'another-session-id')).toBe(false)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession('not-a-pane-key', SESSION_ID)).toBe(false)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(null, SESSION_ID)).toBe(false)
|
||||
} finally {
|
||||
structuredWorkerIdentities.forget(handle)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects the deterministic public status key even for a registered worker', () => {
|
||||
const handle = mintStructuredWorkerHandle()
|
||||
const paneKey = mintStructuredWorkerPaneKey(SESSION_ID)
|
||||
structuredWorkerIdentities.register({
|
||||
handle,
|
||||
sessionId: SESSION_ID,
|
||||
agent: 'claude',
|
||||
paneKey,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(SESSION_ID),
|
||||
worktreeId: 'wt_1',
|
||||
hostScope: { kind: 'local', hostId: 'local' }
|
||||
})
|
||||
try {
|
||||
const statusPaneKey = structuredAgentSessionPaneKey(
|
||||
structuredAgentSessionTabId(SESSION_ID),
|
||||
SESSION_ID
|
||||
)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(statusPaneKey, SESSION_ID)).toBe(false)
|
||||
} finally {
|
||||
structuredWorkerIdentities.forget(handle)
|
||||
}
|
||||
})
|
||||
|
||||
it('fails closed when the session has no registry record', () => {
|
||||
const paneKey = mintStructuredWorkerPaneKey(SESSION_ID)
|
||||
expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(false)
|
||||
})
|
||||
|
||||
it('derives a pane key whose leaf passes the terminal leaf check', () => {
|
||||
@@ -169,6 +217,21 @@ describe('structured worker identity registry', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('refuses to rehydrate the deterministic public status key as a worker credential', () => {
|
||||
expect(
|
||||
registry.rehydrate({
|
||||
terminal_handle: mintStructuredWorkerHandle(),
|
||||
pane_key: structuredAgentSessionPaneKey(
|
||||
structuredAgentSessionTabId(SESSION_ID),
|
||||
SESSION_ID
|
||||
),
|
||||
process_incarnation: structuredWorkerProcessIncarnation(SESSION_ID),
|
||||
worktree_id: 'wt_1',
|
||||
host_scope: JSON.stringify({ kind: 'local', hostId: 'local' })
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('forgets both indexes', () => {
|
||||
const handle = mintStructuredWorkerHandle()
|
||||
registry.register({
|
||||
|
||||
@@ -20,7 +20,10 @@ import type {
|
||||
AgentSessionRecord
|
||||
} from '../../shared/agent-session-record'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../shared/structured-agent-session-projection'
|
||||
import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../shared/stable-pane-id'
|
||||
import {
|
||||
parseWorkerTerminalHostScope,
|
||||
@@ -67,13 +70,30 @@ export function mintStructuredWorkerPaneKey(sessionId: string): string {
|
||||
return makePaneKey(structuredAgentSessionTabId(sessionId), randomUUID())
|
||||
}
|
||||
|
||||
/** Integrity check for a persisted pane key: same session's tab, and a real terminal leaf. */
|
||||
/** Credential check: only the pane key registered for this session can prove its identity. */
|
||||
export function structuredWorkerPaneKeyBelongsToSession(
|
||||
paneKey: string | null | undefined,
|
||||
sessionId: string
|
||||
): boolean {
|
||||
const registered = structuredWorkerIdentities.getBySessionId(sessionId)
|
||||
const parsed = paneKey ? parsePaneKey(paneKey) : null
|
||||
return Boolean(
|
||||
registered &&
|
||||
registered.paneKey === paneKey &&
|
||||
parsed &&
|
||||
parsed.tabId === structuredAgentSessionTabId(sessionId)
|
||||
)
|
||||
}
|
||||
|
||||
/** Bootstrap validation for a durable row before its key can enter the registry. */
|
||||
function persistedStructuredWorkerPaneKeyIsValid(
|
||||
paneKey: string | null | undefined,
|
||||
sessionId: string
|
||||
): paneKey is string {
|
||||
const parsed = paneKey ? parsePaneKey(paneKey) : null
|
||||
return Boolean(
|
||||
paneKey &&
|
||||
paneKey !== structuredAgentSessionPaneKey(structuredAgentSessionTabId(sessionId), sessionId) &&
|
||||
parsed &&
|
||||
parsed.tabId === structuredAgentSessionTabId(sessionId) &&
|
||||
isTerminalLeafId(parsed.leafId)
|
||||
@@ -176,9 +196,8 @@ export class StructuredWorkerIdentityRegistry {
|
||||
!hostScope ||
|
||||
!row.worktree_id ||
|
||||
!isStructuredWorkerHandle(row.terminal_handle) ||
|
||||
// The leaf is random, so the row IS the only source for it; verify only that it is a real
|
||||
// leaf under this session's tab rather than trying to re-derive it.
|
||||
!structuredWorkerPaneKeyBelongsToSession(row.pane_key, sessionId)
|
||||
// The durable row bootstraps the registry after restart, so validate it before registration.
|
||||
!persistedStructuredWorkerPaneKeyIsValid(row.pane_key, sessionId)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
@@ -187,7 +206,7 @@ export class StructuredWorkerIdentityRegistry {
|
||||
sessionId,
|
||||
// The row does not carry the provider; callers that need it read the durable record.
|
||||
agent: null,
|
||||
paneKey: row.pane_key as string,
|
||||
paneKey: row.pane_key,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(sessionId),
|
||||
worktreeId: row.worktree_id,
|
||||
hostScope
|
||||
|
||||
Reference in New Issue
Block a user