/** * ScannerLauncher — the supervisor wiring component. * * For each ExternalScannerSpec it: * 1. Mints an ephemeral scanner_id. * 2. Registers it in the shared registry so the intake can resolve it. * 3. Writes a launch-config JSON that the scaffold reads on startup. * 4. Builds a LongRunningSpec with the correct env + stable marker. * 5. Constructs + starts a LongRunningSupervisor. * 6. On the supervisor's "restarting" event: unregisters the old id, mints * a fresh one, re-registers, and rewrites the spec env so the relaunched * child carries the new id (zombie fence). */ import { EventEmitter } from "node:events"; import type { Logger } from "../../utils/logger.js"; import type { ExternalFieldDefinition } from "../../scanners/scanner-definition.js"; import type { LongRunningSpec, LongRunningStatus } from "../../runtime/supervisor/long-running-supervisor.js"; import type { ScannerSupervisionRow } from "../../scanners/supervision.js"; import type { ScannerRegistry } from "./scanner-registry.js"; import type { AcceptanceRecord } from "./acceptance-record.js"; import type { LivenessStore } from "./liveness-store.js"; export interface ExternalScannerSpec { name: string; /** Absolute path to the scanner's working directory (added to sys.path). */ path: string; /** Function name inside the module that the scaffold calls. */ entrypoint: string; /** Tick cadence in integer seconds (read directly from recipe `interval_seconds`). */ intervalSeconds: number; /** Optional per-tick timeout in seconds. */ timeoutSeconds?: number; /** Per-signal `data{}` schema (recipe `signal_data_schema`). */ fields: Record; /** Author-tunable knobs (recipe `inputs`) passed as the scan() first arg. Optional. */ inputs?: Record; /** Required fallback signal TTL in integer seconds (recipe `default_signal_validity_seconds`). */ defaultSignalValiditySeconds: number; /** Bound on the persisted state series (recipe `state_history_max_count`); 0/unset = disabled. */ stateHistoryMaxCount?: number; } /** * The behavioral contract the launcher requires of a supervisor — an * EventEmitter with start/stop methods and a mutable `spec`. The default * implementation is LongRunningSupervisor; injected via the `createSupervisor` * dep for unit tests. * * Naming (PR #155 review): renamed from `SupervisorLike`, which read as * "resembles a supervisor but isn't" — misleading, since this IS the surface * the launcher depends on. Two alternatives were considered and rejected: * - `SupervisorInterface` — names the type after its TS mechanism (a * technicality), not its role; same noise as an `I` prefix, which this * codebase deliberately avoids (cf. LivenessStore, AcceptanceRecord). * - `ScannerSupervisor` — reads like a concrete class/role, but this is the * abstract contract, not the implementation (that's LongRunningSupervisor). * `SupervisorContract` names the behavioral agreement itself. */ export interface SupervisorContract extends EventEmitter { /** Stable id of this supervisor instance — the join key to its free-text logs and lifecycle events. */ readonly supervisorId: string; spec: LongRunningSpec; start(): void; stop(): Promise; on(event: "event", cb: (e: unknown) => void): this; /** The supervisor's own crash-loop verdict — the source of truth for {@link ScannerLauncher.describeSupervision}. */ getStatus(): LongRunningStatus; } export interface ScannerLauncherDeps { runtimeId: string; wallet: string; scanners: ExternalScannerSpec[]; registry: ScannerRegistry; /** Unused by the launcher directly; carried for symmetry and tests. */ record: AcceptanceRecord; liveness: LivenessStore; /** Base URL the scaffold POSTs to, e.g. http://127.0.0.1:. */ intakeUrl: string; /** Absolute path to the directory that contains the `scaffold/` package. */ scaffoldPackageDir: string; /** * Directory where per-scanner launch-config JSONs are written: * /-/scanner-launch/.launch.json * (empty wallet → a literal trailing hyphen: `-`). * baseStateDir precedence (see resolveSenpiBaseStateDir): plugin stateDir > * stored STATE_DIR > SENPI_STATE_DIR > OpenClaw /senpi-state > * getDefaultStateDir (~/.openclaw/senpi-state, or process.cwd() if no home). * Always derived — never set by hand. */ launchConfigDir: string; logger: Logger; /** * MCP creds injected into the scaffold child so ctx.senpi_mcp can reach the * server. When unset, the launcher falls back to process.env.SENPI_API_KEY / * process.env.SENPI_MCP_URL ("regardless of how the key was supplied"). * Explicit deps win over the env. Absent/empty values are OMITTED from * spec.env — never written as "" — so the Python client stays loud at first * use rather than masked by a fabricated empty cred. */ senpiApiKey?: string; senpiMcpUrl?: string; /** Default: makeId("scanner"). Inject for deterministic ids in tests. */ mintScannerId?: () => string; /** Default: new LongRunningSupervisor(spec, logger). Inject to avoid forking. */ createSupervisor?: (spec: LongRunningSpec, logger: Logger) => SupervisorContract; /** Optional hook called for every supervisor event (used by E2E to capture pid). */ onSupervisorEvent?: (e: { type: string; pid?: number; [key: string]: unknown; }) => void; /** Extra LongRunningSpec fields (backoff/silence knobs) for tests. */ supervisorOptions?: Partial; } export declare class ScannerLauncher { private readonly deps; private readonly active; /** * Set to true once `_buildAndRegister()` has run (either from `start()` or * from `wireRuntime`). Prevents double-registration when `start()` is * called after eager wiring. */ private _initialized; constructor(rawDeps: ScannerLauncherDeps); /** * Mint IDs, register scanners, write launch configs, build supervisors * (but do NOT call supervisor.start()). Called by start() the first time, * or eagerly by wireRuntime so the registry is populated before the API * starts listening. * * Synchronous — all FS calls (mkdirSync/writeFileSync) are sync. Marked * with a leading underscore to signal "internal / framework use only". * Public so that wireRuntime (in a sibling module) can call it without * TypeScript's private access restriction. */ _buildAndRegister(): void; start(): Promise; /** * Snapshot the per-scanner supervision facts for the scanners module's health * derivation (B1 truthful scanner health). One row per active scanner, keyed by * the configured NAME (stable across relaunch). `supervisorStatus` is read live * from the supervisor's own crash-loop verdict — the source of truth — so a * degraded/crash-looping scanner can never be reported as healthy. `lastAliveAt` * is read from the shared liveness clock for the CURRENT scanner id (post-remint * safe). `degradedReason` is surfaced only while the supervisor is degraded. */ describeSupervision(): ScannerSupervisionRow[]; stop(): Promise; } //# sourceMappingURL=scanner-launcher.d.ts.map