mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence Orca documented that "dispatched workers cannot spawn their own sub-workers (worker-start is coordinator-fenced)". No such check existed. What existed was a single Run-binding check in the workerStart RPC: a worker's terminal is not bound to a Run, so worker-start happened to fail. The rule was emergent, asserted by no test, and written in no doc — and it leaked. A worker could run-create its own Run, task-create, and worker-start: now bound, the check passed. Replace it with a real, configurable depth cap. Depth is derived from the caller's own active Dispatch rather than from Run binding, which is what dissolves the run-create bypass: creating a Run does not stop you being a worker. Enforcement lives in a single dispatch-row writer that owns all three INSERTs that mint a live worker — the generic claim, the supervised worker-start path (including every retry), and the remote attachment. Two of those were missed by earlier drafts of this change, so `creator` and `maxDepth` are required parameters: a new spawn path cannot compile without deciding, and a boundary test refuses the SQL anywhere else. Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments, NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails closed rather than reading as a root coordinator. The attachment pane indexes widen to the five states in which a remote worker may still be running: loss of contact is not evidence of process death, so an unverifiable worker still counts as a nesting parent. Also adds the caller-evidence assertion that workerStart was the only Run-scoped verb to skip, so a declared --from cannot name another terminal's pane and inherit its depth. Default is 1, so behaviour is unchanged unless the new setting is raised. Two limitations are deliberate and documented rather than papered over: this is a guardrail and not a security boundary, since a caller whose launch evidence is unverifiable (any ordinary restored terminal) can declare another handle; and it is enforced at supervised dispatch creation, so a settled worker whose process is still alive counts as a root again. * fix(orchestration): share caller resolution and pin worker gaps * refactor(orchestration): make the caller resolver's pane contract explicit Overloads so requireStablePane callers get a non-null string instead of casting, and rename the attestation opt-out to say what it means: the caller asserts it itself. A flag called assertEvidence:false reads as "attestation optional", which is the hole this helper exists to close. * fix(orchestration): propagate dispatch depth to federated workers * chore(cli): refresh bundled orchestration guide
This commit is contained in:
@@ -281,6 +281,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
||||
artifactsEnabled: true,
|
||||
artifactSharingEnabled: false,
|
||||
agentSkillSharingEnabled: false,
|
||||
nestedWorkerMaxDepth: 1,
|
||||
showArtifactsButton: false,
|
||||
showSkillsButton: false,
|
||||
showMobileButton: true,
|
||||
|
||||
@@ -230,6 +230,10 @@ export type GlobalSettings = {
|
||||
artifactSharingEnabled?: boolean
|
||||
/** Capability gate for agent/CLI skill publishing; manual reviewed publishing remains available. */
|
||||
agentSkillSharingEnabled?: boolean
|
||||
/** How deep dispatched workers may nest. 1 = workers cannot dispatch sub-workers.
|
||||
* Renderer-writable only: omitted from the SettingsUpdate RPC schema so a worker
|
||||
* cannot raise its own cap via `orca settings update`. */
|
||||
nestedWorkerMaxDepth?: number
|
||||
/** Only toggles the sidebar shortcut; Artifacts stay reachable from Settings. */
|
||||
showArtifactsButton?: boolean
|
||||
/** Only toggles the sidebar shortcut; Skills stay reachable from Settings. */
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
NESTED_WORKER_MAX_DEPTH_DEFAULT,
|
||||
nestedWorkerDepthExceededMessage,
|
||||
resolveNestedWorkerMaxDepth
|
||||
} from './nested-worker-depth'
|
||||
|
||||
describe('resolveNestedWorkerMaxDepth', () => {
|
||||
it('defaults to 1 when unset', () => {
|
||||
expect(resolveNestedWorkerMaxDepth(undefined)).toBe(1)
|
||||
expect(resolveNestedWorkerMaxDepth(null)).toBe(1)
|
||||
expect(resolveNestedWorkerMaxDepth({})).toBe(1)
|
||||
})
|
||||
|
||||
it('accepts whole numbers at or above 1', () => {
|
||||
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: 1 })).toBe(1)
|
||||
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: 3 })).toBe(3)
|
||||
})
|
||||
|
||||
// A malformed setting must not become a way to get unlimited nesting, so every
|
||||
// rejected shape falls back to the default rather than disabling the cap.
|
||||
it.each([
|
||||
['a numeric string', '2'],
|
||||
['a boolean', true],
|
||||
['zero', 0],
|
||||
['negative', -1],
|
||||
['fractional', 1.5],
|
||||
['NaN', Number.NaN],
|
||||
['Infinity', Number.POSITIVE_INFINITY],
|
||||
['null', null]
|
||||
])('falls back to the default for %s', (_label, value) => {
|
||||
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: value as unknown as number })).toBe(
|
||||
NESTED_WORKER_MAX_DEPTH_DEFAULT
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('depth-exceeded message', () => {
|
||||
it('names both depths and tells the worker to finish the task itself', () => {
|
||||
const message = nestedWorkerDepthExceededMessage(2, 1)
|
||||
expect(message).toContain('depth 2 (max 1)')
|
||||
expect(message).toContain('Complete this task yourself')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { GlobalSettings } from './global-settings-types'
|
||||
|
||||
/**
|
||||
* How deep dispatched workers may nest. 1 means a coordinator dispatches
|
||||
* workers and those workers may not dispatch further — the behaviour Orca
|
||||
* documented but never actually enforced.
|
||||
*/
|
||||
export const NESTED_WORKER_MAX_DEPTH_DEFAULT = 1
|
||||
|
||||
/** Root coordinators are depth 0; the first generation of workers is depth 1. */
|
||||
export const ROOT_DISPATCH_DEPTH = 0
|
||||
|
||||
export const NESTED_WORKER_DEPTH_EXCEEDED_CODE = 'nested_worker_depth_exceeded'
|
||||
|
||||
export function nestedWorkerDepthExceededMessage(childDepth: number, maxDepth: number): string {
|
||||
// Why "complete this task yourself": a refusal alone leaves the worker looping
|
||||
// on a capability it will never get.
|
||||
return (
|
||||
`Sub-worker dispatch is not permitted at depth ${childDepth} (max ${maxDepth}). ` +
|
||||
'Complete this task yourself.'
|
||||
)
|
||||
}
|
||||
|
||||
export const NESTED_WORKER_DEPTH_EXCEEDED_NEXT_STEPS: readonly string[] = [
|
||||
'Do the work in this terminal instead of dispatching a sub-worker.',
|
||||
'To allow deeper nesting, open Settings → Agents in the Orca desktop app and raise "Nested worker depth".'
|
||||
]
|
||||
|
||||
/**
|
||||
* Clamp to a usable integer. Anything that is not a whole number >= 1 falls back
|
||||
* to the default rather than disabling the fence: a malformed setting must not
|
||||
* be a way to get unlimited nesting.
|
||||
*/
|
||||
export function resolveNestedWorkerMaxDepth(
|
||||
settings: Pick<GlobalSettings, 'nestedWorkerMaxDepth'> | null | undefined
|
||||
): number {
|
||||
const raw = settings?.nestedWorkerMaxDepth
|
||||
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
|
||||
return NESTED_WORKER_MAX_DEPTH_DEFAULT
|
||||
}
|
||||
return raw
|
||||
}
|
||||
Reference in New Issue
Block a user