Files
hapi/shared/src/runnerCapabilities.ts
T
wushenghua 41179575a5 fix(hub,web): block team spawns on runners without the agentTeam capability
Old runners ignore the team payload, so the lead was spawned without
HAPI_TEAM_*: no team tools, no team prompt, no repo memory — it then
improvised (reverse-engineering the HAPI API, poking the project) instead
of replying. Gate team spawns in SyncEngine.spawnSession, share the
capability predicate via runnerCapabilities, and mark machines without it
disabled in team mode with an upgrade hint.
2026-09-15 12:05:11 +08:00

95 lines
3.6 KiB
TypeScript

import { RPC_METHODS } from './rpcMethods'
/**
* Capabilities the current runner generation advertises to the hub.
*
* `piExistingSessionResume` gates the Pi native-history resume path in
* hub/src/sync/syncEngine.ts: the runner must be able to hand back an
* existing native Pi process. The runner declares it at registration
* (HTTP POST /machines) and again on every socket connect, because the hub
* merges registration-time runner state only for brand-new machines and the
* socket heartbeat owns the persisted runner_state afterwards — without the
* socket-side advertisement, a runner upgraded in place would never get its
* new capabilities observed by the hub.
*
* `agentTeam` gates the Agent Team feature (team spawning / peer messaging).
* Old runners omit it; the hub must treat absence as unsupported and keep
* those machines fully usable for regular single sessions.
*/
import { getBuiltinAgentConfigDescriptors } from './agentConfig'
export const RUNNER_CAPABILITIES = {
codexSharedRuntime: true as const,
piExistingSessionResume: true as const,
agentConfigs: getBuiltinAgentConfigDescriptors(),
agentTeam: true as const
}
export type RunnerCapabilities = typeof RUNNER_CAPABILITIES
/**
* Machine-scoped capabilities runners advertise on connect.
* Hub features that hard-depend on a machine RPC must list that capability
* in {@link REQUIRED_MACHINE_CAPABILITIES} so skew surfaces as a banner
* instead of a silent fail-closed product bug.
*/
/** Runner-state capability check shared by hub gates and web pickers. */
export function runnerSupportsAgentTeam(
runnerState: { capabilities?: { agentTeam?: true } } | null | undefined
): boolean {
return runnerState?.capabilities?.agentTeam === true
}
export const MACHINE_CAPABILITIES = {
AgentAvailability: RPC_METHODS.AgentAvailability,
CursorChatStoreStatus: RPC_METHODS.CursorChatStoreStatus,
StopRunner: RPC_METHODS.StopRunner,
} as const
export type MachineCapability =
(typeof MACHINE_CAPABILITIES)[keyof typeof MACHINE_CAPABILITIES]
/** Capabilities this CLI generation registers on the machine socket. */
export const CURRENT_MACHINE_CAPABILITIES: readonly MachineCapability[] = [
MACHINE_CAPABILITIES.AgentAvailability,
MACHINE_CAPABILITIES.CursorChatStoreStatus,
MACHINE_CAPABILITIES.StopRunner,
]
/**
* Capabilities the hub requires on every connected runner for features it
* hard-depends on. Missing entries → operator-visible skew banner (+ optional
* stop-runner ensure when a newer binary is already on disk).
*/
export const REQUIRED_MACHINE_CAPABILITIES: readonly MachineCapability[] = [
MACHINE_CAPABILITIES.AgentAvailability,
MACHINE_CAPABILITIES.CursorChatStoreStatus,
]
export function missingRequiredCapabilities(
advertised: readonly string[] | null | undefined,
): MachineCapability[] {
const set = new Set(advertised ?? [])
return REQUIRED_MACHINE_CAPABILITIES.filter((cap) => !set.has(cap))
}
export function isMachineCapabilitySkewed(
advertised: readonly string[] | null | undefined,
): boolean {
return missingRequiredCapabilities(advertised).length > 0
}
/** True when the running process started from a different CLI binary/mtime than what's installed now. */
export function cliBinaryUpdatedOnDisk(metadata: {
startedCliMtimeMs?: number | null
installedCliMtimeMs?: number | null
} | null | undefined): boolean {
const started = metadata?.startedCliMtimeMs
const installed = metadata?.installedCliMtimeMs
return typeof started === 'number'
&& typeof installed === 'number'
&& Number.isFinite(started)
&& Number.isFinite(installed)
&& started !== installed
}