/** * The conversation-first spawn gate, at the shared surface spawn boundary. * * Every channel surface adapter converges on ONE construction site for its * adapter context (DaemonSurfaceActionHelper.buildSurfaceAdapterContext), and * this module is what that site installs in place of a bare spawn. Putting the * rule here rather than in an adapter is deliberate: a gate added to ntfy * alone would leave Telegram, Slack, and Home Assistant behaving exactly as * they did before. * * The rule: an inbound message gets a conversational reply. If it reads as a * work request, the gate PROPOSES a workstream over the channel it arrived on * and starts nothing. Agreement (daemon/work-proposal-reply.ts) is what starts * the work. * * Not gated: * - goodvibes-tui and other local surfaces, the operator typed it while * sitting in front of the terminal. They never build a surface adapter * context, so they never reach this module. * - Pre-authorized work, schedules, triggers, on-exit chains, an agreed * proposal, and the explicit `retry ` control command. Those go through * the raw trySpawnAgent, never this wrapper. * - Generic webhooks, machine automation, authorized at registration. */ import type { AutomationRouteBinding } from '../automation/routes.js'; import type { RouteBindingManager } from '../channels/index.js'; import type { SharedSessionBroker } from '../control-plane/index.js'; import type { AgentManager, AgentRecord } from '../tools/agent/index.js'; import { type ConversationGateConfigReader } from '../agents/conversation-gate.js'; import type { WorkProposalRecord, WorkProposalStore } from '../agents/work-proposal-store.js'; import type { SurfaceNoticeDelivery } from './types.js'; /** * The identity of the inbound message currently being handled. * * The adapter context is built once per inbound message, so a cell holding * this is scoped to exactly that message. Every adapter runs its ingress * policy check before it spawns anything, which is what makes that check the * one place the originating channel is still known, by the time the spawn * call happens the adapter has thrown the identity away. */ export interface SurfaceIngressOrigin { readonly surface: string; readonly text?: string | undefined; readonly userId?: string | undefined; readonly channelId?: string | undefined; readonly threadId?: string | undefined; } export type SpawnInput = Parameters[0]; export interface ConversationGateDeps { readonly configManager: ConversationGateConfigReader; readonly routeBindings: Pick; readonly sessionBroker: Pick; readonly trySpawnAgent: (input: SpawnInput, logLabel?: string, sessionId?: string) => AgentRecord | Response; readonly queueSurfaceReplyFromBinding: (binding: AutomationRouteBinding | undefined, input: { readonly agentId: string; readonly task: string; readonly agentTask?: string | undefined; readonly workflowChainId?: string | undefined; readonly sessionId?: string | undefined; }) => void; readonly workProposals?: WorkProposalStore | undefined; readonly deliverSurfaceNotice?: ((binding: AutomationRouteBinding | undefined, text: string) => Promise) | undefined; } /** * Gate one surface spawn. * * A proposal is returned as a `Response`, which every adapter already * early-returns on, so no adapter needs gate-specific code and a new adapter * cannot forget to participate. */ export declare function gateSurfaceSpawn(deps: ConversationGateDeps, origin: SurfaceIngressOrigin | null, input: SpawnInput, logLabel?: string, sessionId?: string): AgentRecord | Response; /** * The route binding for the channel a message arrived on. Prefers a binding * already attached to the session (the adapter just upserted it), then falls * back to resolving one from the surface identity. */ export declare function resolveOriginBinding(deps: Pick, origin: SurfaceIngressOrigin | null, sessionId?: string): AutomationRouteBinding | undefined; /** * Put the proposal on the owner's channel and report whether it got there. * * The result is the caller's business, not this function's: a proposal whose * notice never arrived must not stay answerable, because the owner has not * seen it and their NEXT message, whatever it is about, would otherwise be * matchable against it. Discarding this outcome is exactly the defect that * let an unseen proposal be "accepted". */ export declare function deliverProposalNotice(deps: Pick, binding: AutomationRouteBinding | undefined, message: string): Promise; /** * Start work the owner just agreed to. Goes through the RAW spawn path: the * agreement IS the authorization, so re-gating it here would ask twice. */ export declare function startAgreedWork(deps: ConversationGateDeps, proposal: WorkProposalRecord, note?: string): Promise; //# sourceMappingURL=surface-conversation-gate.d.ts.map