/** * Automation run arm. * * The connector-worker daemon claims connector sync/action/auth/embed jobs. For * `run_type='automation'` (device mode only — the gateway never hands an * automation run to a trusted fleet worker), the daemon now spawns the user's * local agent CLI (`claude`, `codex`, …) the same way the Mac app's * `AutomationDispatcher` does: build the prompt from the poll envelope, spawn * headless, heartbeat, then post the process exit to `/complete-automation` and * honour the server's `resume` decision. * * Ported from `packages/owletto`'s `AutomationDispatcher` (AgentSpec routing, * `SpecExecutor` subprocess supervision, and the finalize/resume loop). The * prompt and the `AgentSpec` table now live in * `@lobu/core/contracts/worker/device-automation`, so the two runtimes cannot * drift. * * Unlike connector children (which inherit a small system-env allowlist), the * spawned agent CLI runs in the user's environment (PATH, HOME) minus the * `WORKER_API_TOKEN` env var, so the child cannot act as the worker/poll loop. * Its Lobu credential is the poll envelope's per-run `agent_session` when the * server minted one (see `resolveDeviceAgentRunAccess`): the run authenticates * as the run's assigned agent, not as the daemon or the user's ambient CLI * session. Capable standalone Mac daemons fail closed when that session is * absent; legacy workers retain the pre-session fallback. */ import { type AgentKind, type AgentSpec } from '@lobu/core/contracts/worker/device-automation'; import type { AutomationPollPayload, CompleteAutomationResponse, PollResponse, WorkerExitReason } from '@lobu/core/contracts/worker/protocol'; import type { ExecutorClient } from './client.js'; import { type InteractiveSession } from './interactive-session.js'; import { type AutomationAcpAdapters } from './automation-acp.js'; /** * The slice of the daemon's `ExecutorConfig` the automation arm reads. * * Declared narrowly so a caller that only executes automations — the one-shot * `executeClaimedAutomationRun` entry point — does not have to fabricate * connector-sync fields (`batchSize`, `generateEmbeddings`) * that this arm never touches. The daemon's full `ExecutorConfig` is * structurally assignable to it. */ export interface AutomationExecutorConfig { timeoutMs?: number; heartbeatIntervalMs?: number; /** Stops a pending interactive handoff during daemon shutdown. */ shutdownSignal?: AbortSignal; /** Standalone Mac daemons must never use the device PAT for MCP writes. */ requireRunScopedSession?: boolean; /** Test-only override; production deliberately uses the 15-second default. */ terminalHeartbeatGraceMs?: number; /** * Agent to use when the Automation names no `agent_kind`. * * The device, not the server, owns this choice: `agent_kind` is optional on * the wire (`AutomationPollMetaSchema`), and which CLIs are actually * installed is a property of the machine. The Mac app has always resolved it * from the user's menubar pick; without it here, every Automation created * without an explicit kind fails on the device with "no local agent executor * configured". */ defaultAgentKind?: AgentKind; /** * Explicit per-agent binary paths (else PATH lookup). Lets an operator point * at a non-PATH CLI install, and is the injection seam the automation tests * use to drive a fake binary. */ binaryOverrides?: Partial>; /** Maintained ACP entrypoints keyed by the local agent kind they drive. */ acpAdapters?: AutomationAcpAdapters; } /** Shared liveness/cancellation control for every device-local agent run. */ export declare function monitorDeviceAgentRun(client: ExecutorClient, runId: number, cfg: AutomationExecutorConfig, label: string): { abortController: AbortController; terminalSignal: AbortSignal; shutdownRequested: () => boolean; stop: () => void; }; /** Local-CLI run result, mirrored from the Mac app's `ExecutorResult`. */ export interface ExecutorResult { output: string; error: string | null; exitCode: number | null; exitSignal: string | null; exitReason: WorkerExitReason; durationMs: number; /** Present for an ACP turn; cumulative across finalize rounds. */ transcriptJsonl?: string; } /** Wall-clock cap for a device-local agent CLI without a per-run override. */ export declare const DEFAULT_DEVICE_AGENT_TIMEOUT_MS = 600000; /** Assemble argv from the spec + execution config, mirroring `SpecExecutor`. */ export declare function buildArguments(spec: AgentSpec, prompt: string, config: AutomationPollPayload['automation']['execution_config'], mcpArgs: string[], timeoutSeconds: number): string[]; /** The run-scoped credential set used by any spawned local agent CLI. */ export interface DeviceAgentRunAccess { /** Lobu MCP wiring for the CLI's mcp config (buildMcp). */ wiring: { url: string; bearer?: string; } | undefined; /** Extra env for the child process (LOBU_API_TOKEN / LOBU_MEMORY_URL). */ env: Record; } /** * Resolve what credential the spawned CLI runs with — the boundary shared by * Automation and device-chat CLIs. When the poll envelope carries a per-run * `agent_session`, the CLI authenticates as the run's assigned agent for * exactly this run: the session token goes into the MCP wiring AND into * LOBU_API_TOKEN/LOBU_MEMORY_URL, which `lobu memory` prefers * over the device's ambient CLI session — so an unattended run never acts as * the human user or the daemon. Without a session (older server, or a run with * no usable assigned agent) fall back to the daemon's own wiring, the * pre-session dispatch path. */ export declare function resolveDeviceAgentRunAccess(session: AutomationPollPayload['context']['agent_session'], daemonWiring: { url: string; bearer?: string; } | undefined): DeviceAgentRunAccess; /** Interactive delivery requires the run kind and run-scoped access to match. */ export declare function isInteractiveSessionEligible(kind: AgentKind, session: InteractiveSession | undefined, payload: AutomationPollPayload): boolean; /** Spawn one CLI run and classify how it ended. */ export declare function runCli(spec: AgentSpec, prompt: string, config: AutomationPollPayload['automation']['execution_config'], access: DeviceAgentRunAccess, timeoutMs: number, binaryPath?: string, abortSignal?: AbortSignal, shutdownSignal?: AbortSignal, terminalHeartbeatGraceMs?: number): Promise; /** The CLI spawn + exit-report seams the resume loop drives. Injecting them is * how the loop is tested without spawning a real CLI or hitting the network — * the same seam `AutomationDispatcher` exposes via its `LocalAgentExecutor` * protocol. */ export interface AutomationRunIo { /** Spawn one CLI run (with any finalize nudge appended) and classify its exit. */ run: (finalizeNudge: string | undefined) => Promise; /** Post one exit report, retrying on retriable failures; null = unknown. */ deliver: (result: ExecutorResult, finalizeAttempt: number) => Promise; /** Best-effort local-failure report so the run does not sit `running`. */ reportError: (error: string, reason: WorkerExitReason) => Promise; } /** * Execute a device automation run: spawn the local CLI per its AgentSpec, then * post the exit report and honour the server's `resume` decision. */ export declare function executeAutomationRun(client: ExecutorClient, job: PollResponse, cfg: AutomationExecutorConfig): Promise<{ itemsCollected: number; error?: string; }>; /** Spawn → exit report → resume loop, mirroring `AutomationDispatcher.dispatch`. */ export declare function dispatchAutomationResumeLoop(io: AutomationRunIo): Promise<{ itemsCollected: number; error?: string; }>; /** Post one exit report, re-sending on retriable failures. */ export declare function deliverExitReport(client: ExecutorClient, runId: number, result: ExecutorResult, finalizeAttempt: number): Promise; //# sourceMappingURL=automation.d.ts.map