export declare function isAutoApproveEnabled(): boolean; export declare function isTimeoutDefaultAllow(): boolean; export declare function announceTimeoutDefaultIfAllow(): void; /** Stop the C1 hourly reminder. Used by graceful shutdown + tests. */ export declare function stopTimeoutDefaultReminder(): void; export interface RunContext { threadId: string; platform: string; userId: string; channelId: string; /** A2A-L1: name of the agent this run is executing under (claude-code / * opencode / codex / …). Adapters fill this when calling * approvalBus.registerRun so bus.handleA2A can reject self-calls and * audit can record the chain. Optional for legacy callers; the * self-call check just no-ops when absent. */ callerAgent?: string; /** A2A-L1: this run's own call_depth. 0 for user-originated runs; * bumped to caller.callDepth+1 when an A2A invocation spawns a new * agent run. Used to enforce AGIM_A2A_MAX_DEPTH. */ callDepth?: number; /** A2A-L1: id of the inline-jobs row that's tracking this run, when * the run was spawned from another agent's mcp__agim__call_agent * call. Optional for top-level runs. */ parentJobId?: number; /** SEC: this run is executing under plan-mode (user did /plan on, so the * parent agent runs read-only). Adapters set this when registering a run * whose opts.planMode is true. bus.handleA2A propagates it to the callee * so a plan-mode run can't delegate a write task to another agent that * would launch with full write access — bypassing the user's intent. * Mirrors a2a.ts / agim-dispatcher.ts (callerPlanMode) which already do * this for their own spawn paths. */ callerPlanMode?: boolean; /** Filesystem cwd used by the running agent subprocess. Extension-based * agents (PI, future custom-tool CLIs) use it when Agim proxies native * write/exec tools on their behalf, so approved filesystem operations land * in the same workspace the agent is reasoning about. */ cwd?: string; /** Root invocation trace used to bind goal evidence lineage. */ traceId?: string; /** Web chat activity accordion — forwarded into handleA2A → callAgentByName * so CLI agents (claude-code etc.) can surface "Waiting for subagent". */ onToolActivity?: import('./types.js').AgentSendOpts['onToolActivity']; } /** Wire-format decision sent back to the sidecar. `autoAllowFurther` is * internal-only — `sendDecision` strips it before serializing. */ export type Decision = { behavior: 'allow'; updatedInput?: Record; autoAllowFurther?: boolean; } | { behavior: 'deny'; message?: string; }; export interface ApprovalNotification { runId: string; reqId: string; toolName: string; input: Record; toolUseId: string; ctx: RunContext; /** Present iff this request matched a per-thread auto-allow rule. The * notifier should render a softer prompt ("⏱ 自动放行中, 5s 内回 n 可拒绝") * instead of the regular y/n one — the bus will allow on timer expiry, * unless the user actively denies first. */ autoAllow?: { graceMs: number; }; /** Security-sensitive state transitions (for example exiting Plan Mode) * require a one-time human decision. Auto-approve, remembered allow rules, * allow-on-timeout, and "allow all" must not bypass this prompt — unless * `allowSessionPin` is also set (see below). */ requiresExplicitDecision?: boolean; /** When paired with `requiresExplicitDecision`, still bypass global * auto-approve / allow-on-timeout, but let the operator click * "Auto for similar" to seed a per-thread remembered rule (and honor * that rule on later matching requests via the grace path). */ allowSessionPin?: boolean; } /** Notifier 只负责"通知 IM 推卡片/消息"。不要在这里返回决策;决策走 resolvePending。 */ export type ApprovalNotifier = (n: ApprovalNotification) => Promise; /** Snapshot returned by {@link ApprovalBus.resolvePending} so the router can * send the user a confirmation receipt ("✅ 已批准并启用自动放行 …" or * "❌ 已拒绝并撤销自动放行 …") without re-scanning bus internals. */ export interface ResolvedInfo { reqId: string; runId: string; threadId: string; platform: string; toolName: string; /** Same fingerprint the bus uses for the auto-allow rule key. */ fingerprint: string; /** True iff the resolved pending had been created in auto-allow grace * mode — i.e. matched an existing rule. Lets the router distinguish * "user denied a normal prompt" from "user denied an auto-allow prompt * (which also revoked the rule)". */ wasAutoAllow: boolean; } /** Why an approval was resolved. Used by ResolutionListener consumers (e.g. * approval-router) to decide whether to edit a UI card to "expired" — only * non-`user` causes need cleanup, since the user-driven path (button click / * text reply) edits the card itself before the listener fires. */ export type ResolutionCause = 'user' | 'timeout' | 'sidecar-disconnect' | 'run-terminated' | 'shutdown' | 'notifier-error'; /** Fired exactly once per pending when it transitions from pending → resolved. * Mirrors the wire decision sent to sidecar/dispatch but adds context (cause, * threadId, platform) so listeners can route side-effects without scanning * bus internals. Synchronous; listener errors are logged + swallowed. */ export interface ResolutionEvent { reqId: string; runId: string; threadId: string; platform: string; toolName: string; fingerprint: string; decision: Decision; wasAutoAllow: boolean; cause: ResolutionCause; } export type ResolutionListener = (e: ResolutionEvent) => void; /** Composite key that prevents cross-platform/channel thread collisions. */ export declare function threadKey(platform: string, channelId: string, threadId: string): string; export interface ApprovalBusOptions { approvalTimeoutMs?: number; /** Window between sending the auto-allow notice and auto-resolving as * allow (when the user doesn't actively deny in the meantime). */ autoAllowGraceMs?: number; } export declare class ApprovalBus { private server; private socketPath; /** Public so renderers (approval-router plain-text prompt, future card * variants) can show the actual budget instead of hardcoded "5 分钟". */ readonly approvalTimeoutMs: number; readonly autoAllowGraceMs: number; private runContexts; private pendingById; private pendingByThread; private connections; private notifier; private resolutionListener; /** compositeKey → set of `${toolName}::${prefix}` keys the user has marked * as auto-allow within this conversation. Cleared by clearAutoAllowForThread * (called from session.resetConversation) and on stop(). Key format is * `platform:channelId:threadId` to isolate across platforms/channels. */ private autoAllowByThread; /** Per-thread opt-out for readonly auto-allow. Default: enabled (= empty * set means everyone is opted in). Toggled via /approval readonly on|off. * In-memory only — survives no restarts; restart returns to default-on, * matching the env-var default. */ private readonlyAutoAllowDisabledThreads; /** * Lifetime counters surfaced via {@link getMetrics}. Help ops detect * leaks (pending growing unbounded), spikes (totalRequests rate), and * approval skew (deny:allow ratio). Reset on stop() so the gauge for a * fresh process starts at zero. */ private metricsSnapshot; constructor(opts?: ApprovalBusOptions); /** 注入"通知 IM 推送"的回调。messenger 层启动时调一次。 */ setNotifier(n: ApprovalNotifier | null): void; /** Subscribe to resolution events. Replaces any previous listener. * approval-router uses this to keep its UI cards in sync with bus-side * cancellations (timeout / sidecar disconnect / run terminated). The * user-driven path (button or y/n text) already edits its own card; the * listener still fires there with cause='user' so consumers can dedup. */ setResolutionListener(l: ResolutionListener | null): void; /** 启动 unix socket 服务。返回最终使用的 socket 路径。 */ start(socketPath?: string): Promise; stop(): Promise; registerRun(runId: string, ctx: RunContext): void; /** 进程结束时调。pending 全 deny,runContext 清掉。 */ unregisterRun(runId: string): void; /** Check if there are pending approvals for the given composite key. * `key` must be a composite produced by `threadKey(platform, channelId, * threadId)` — that's what register stores under. A bare threadId will * miss in multi-platform deployments. */ hasPendingFor(key: string): boolean; /** True iff a notifier has been installed (i.e. messenger layer has wired * the bus into IM). Callers that have a fallback path (e.g. the opencode * HTTP adapter) check this before registerSyntheticPending so they can * short-circuit when the bus is dormant — mostly relevant in tests and in * non-IM call paths (web, scheduler). */ hasNotifier(): boolean; /** * 由 messenger.onMessage 拦截层调用。把 thread 队列头部的 pending 用 * 给定决策 resolve 掉。返回被 resolve 的 pending 描述(platform / tool / * fingerprint / 是否处于 auto-allow grace 模式);router 用这些信息发回执。 * 没有 pending 时返回 null。 * * Auto-allow side-effect: a user-initiated deny against a pending that * was running in auto-allow mode revokes the matching rule (the user is * signaling "stop auto-approving this"). Revocation is intentionally * scoped to this user-path so sidecar disconnects / shutdown / run- * terminated denies don't accidentally clear rules the user still wants. */ /** Resolve the FIFO head for the given composite key. `key` must be a * composite produced by `threadKey(platform, channelId, threadId)`. The * text-reply path (cli's tryHandleApprovalReply) is the canonical caller; * button callbacks should use `resolvePendingByReqId` instead. */ resolvePending(key: string, decision: Decision, actorUserId?: string): ResolvedInfo | null; /** * Resolve a specific pending approval identified by reqId. Used by button * callbacks and the dashboard where the UI element carries the exact reqId, * avoiding the FIFO head-of-thread ambiguity of resolvePending(). */ resolvePendingByReqId(reqId: string, decision: Decision, actorUserId?: string): ResolvedInfo | null; /** Cancel a specific request from a non-user lifecycle event. Unlike * resolvePendingByReqId this preserves the cause so card renderers expire * stale buttons instead of treating an abort as a human click. */ cancelPendingByReqId(reqId: string, message: string, cause?: Exclude): boolean; private _resolveTarget; /** Drop every auto-allow rule registered for this thread. Called from * session.resetConversation so `/new` truly returns to "ask every time". * `key` must be a composite from `threadKey(platform, channelId, threadId)`. */ clearAutoAllowForThread(key: string): void; /** Test/diagnostic helper — current rule keys for a thread. */ getAutoAllowKeys(key: string): string[]; /** Whether readonly auto-allow is active for this thread. False if either * the global env flag is off OR the user explicitly opted this thread out. */ isReadonlyAutoAllowEnabled(key: string): boolean; /** Per-thread toggle. Returns the new effective state (after applying the * global env constraint — if the env disabled it, returning true is * impossible). */ setReadonlyAutoAllowEnabled(key: string, enabled: boolean): boolean; /** True iff the tool name is in the readonly allowlist. Surface for tests * and the /approval command's status output. */ isReadonlyTool(toolName: string): boolean; /** Snapshot of the readonly allowlist. Returns a copy so callers can't * mutate the bus's internal set. */ getReadonlyTools(): string[]; /** 测试用:当前 socket 路径。 */ getSocketPath(): string | null; /** * Operational metrics snapshot used by /api/metrics (M14). `pending` is * a live count; the totals are lifetime counters that monotonically * increase until stop(). The three result buckets (allowed / denied / * timedOut) are mutually exclusive and sum to totalResolved — see * cancelPending for the bucketing rule. Cheap to call — no allocations * beyond the returned object. */ getMetrics(): { pending: number; totalRequests: number; totalResolved: number; totalAllowed: number; totalDenied: number; totalTimedOut: number; totalReadonlyAutoAllowed: number; }; /** * Snapshot of every currently-pending approval, sanitized for surface * to the operator dashboard. Used by the web `/api/approvals` endpoint * (and any future ops tooling). Returns a stable JSON shape — input * is included verbatim so the UI can render the same preview the * IM-side card would; sockets / dispatch closures / timer handles are * intentionally omitted. * * Sorted oldest-first so the queue head is at index 0 (matches the * head-of-thread queue semantics used by resolvePending). */ listPending(): Array<{ reqId: string; runId: string; threadId: string; platform: string; toolName: string; input: Record; fingerprint: string; autoAllow: boolean; registeredAt: number; ageMs: number; }>; private handleConnection; private handleLine; /** * Handle an A2A call RPC sent by the MCP sidecar. Wire format: * * { * v: 1, type: 'a2a', reqId, runId, * payload: { agent: string, prompt: string, timeoutMs?: number } * } * * Response (sent via {@link sendA2AResult}): * * { v: 1, type: 'a2a.result', reqId, * ok: true, result, jobId, agent, durationMs } * | { v: 1, type: 'a2a.result', reqId, * ok: false, error } * * Validation order: * 1. reqId/runId/payload shape — wire-level * 2. runId → RunContext (no _im_context fallback for A2A: a forged * context could let a compromised callee escalate to a different * user's workspace, and the cost is real money) * 3. agent + prompt presence * 4. self-call check (caller agent vs target agent) * 5. depth check (handled inside a2a.callAgentByName) */ private handleA2A; private sendA2AResult; /** * Handle a reminder RPC sent by the MCP sidecar (mcp-approval-server.ts). * Same socket as approvals; different `type`. Two ways to identify the * IM thread: * * 1. runId → RunContext lookup (preferred — used by claude-code via * --mcp-config env, and opencode stdio via per-spawn extraEnv) * 2. _im_context inline (fallback — used by opencode http where the * MCP server is shared across sessions and per-spawn env can't be * injected; the agent itself asserts the context) * * Path 2 is **single-user only** by design: a multi-tenant deployment * would let one user's agent forge reminders for another. Adapters that * want to use path 2 must opt in by sending `_im_context`. We log it * loudly so operators can audit. */ private handleReminder; private sendReminderResult; /** * Handle a memo RPC from the MCP sidecar — same socket as approvals and * reminders, different `type`. Mirror of {@link handleReminder}. Both * runId-resolved context and the inline `_im_context` fallback are * supported, with the same single-user-only caveat for path 2. * * Wire history: pre-0.2.42 this was `type: 'location'` carrying geo-only * memos; 0.2.42 generalized to 5W1H memos and renamed the wire type. */ private handleMemo; private sendMemoResult; /** * v1.3 — agent-initiated push (`mcp__agim__push_message`). Wire format: * { v: 1, type: 'push', runId, reqId, payload: { text, toThread?, kind? } } * Response: { v: 1, type: 'push.result', reqId, ok, result?, error? } * * Context resolution mirrors handleReminder / handleMemo: runId → ctx * with an inline _im_context fallback for single-user MCP setups. * Per-user rate limit + cross-thread gating are enforced in push-rpc. */ private handlePush; private sendPushResult; /** * P0 #5 — agent-driven structured ask. Wire format: * { v:1, type:'ask', runId, reqId, payload:{question, choices, timeoutSec?, allowTextReply?} } * Response: { v:1, type:'ask.result', reqId, ok, result?, error?, reason? } * * Like push, runId resolution is REQUIRED — no _im_context fallback, * because the ask delivers a question to a specific IM user thread * and accepting an agent-asserted thread would let any caller spoof * a different user's context. */ private handleAsk; private sendAskResult; /** * Stage 3 / P0 #3 — skills loader RPC. agim-owned skills are * injected as a tier-1 summary into every prompt; this RPC lets the * agent pull the tier-2 body on-demand. Same _im_context-free * posture as push: skills are agim-global, no per-user scoping. * { v:1, type:'skill', reqId, payload:{op, name?} } * { v:1, type:'skill.result', reqId, ok, result?, error? } */ private handleSkill; private sendSkillResult; /** * Read-only Agim native tool bridge for extension-based agents such as PI. * * This is intentionally a small whitelist, not a generic "call any native * tool" escape hatch. The payload name is resolved in native-tool-rpc.ts * (currently web_search/web_fetch aliases only), and the call must be tied * to a live runId so a stray process cannot reuse the socket after cleanup. */ private handleNativeTool; private sendNativeToolResult; /** * v1.5 — long-term memory ops (memory_query / save / list / delete). * Same dispatch pattern as handleReminder / handlePush. Context resolved * from runId; inline _im_context fallback supported for single-user MCP. */ private handleMemory; private sendMemoryResult; /** v1.2.63 — long_task / complete_goal RPC handler. Mirrors handleMemo's * shape exactly; goals.ts state lives per-thread so we tie identity to * the live RunContext (no _im_context fallback — same reasoning as * memory ops). */ private handleGoal; private sendGoalResult; private handleApproval; /** * Register an approval request that did NOT come from the unix-socket * sidecar. Used by the opencode HTTP bridge (P2): SSE event from opencode * → bridge calls this with a `dispatch` callback that POSTs the decision * back to opencode's REST API. * * Behavior is identical to the socket path — same notifier, same timeout, * same auto-allow rules — just the delivery channel differs. The * `dispatch` is invoked with the final Decision exactly once, on: * - user reply via {@link resolvePending} * - timeout (deny in normal mode, allow in auto-allow mode) * - {@link unregisterRun} (deny: "run terminated") * - {@link stop} (deny: "approval-bus shutting down") * * dispatch errors are logged and swallowed — the bus must not crash on * a misbehaving callback. * * Idempotent on duplicate reqId: returns silently without firing notify * (matches the socket path's "duplicate reqId" handling, minus the wire * deny since the synthetic caller has no socket to deny on). * * Throws synchronously only when the caller's bus state is invalid (no * notifier installed). The caller should avoid registering the synthetic * pending in that case and fall back to its own deny path. */ registerSyntheticPending(input: { runId: string; reqId: string; toolName: string; input: Record; /** Optional — used by Claude's MCP path; defaults to '' for synthetic. */ toolUseId?: string; /** Require a one-time human decision; bypasses every automatic allow path. */ requiresExplicitDecision?: boolean; /** Allow "Auto for similar" even when `requiresExplicitDecision` is set. */ allowSessionPin?: boolean; /** Override the pending timer (e.g. goal confirmation uses its own budget). */ timeoutMs?: number; ctx: RunContext; dispatch: (decision: Decision) => void; }): Promise; /** * Shared register-and-notify pipeline used by both the socket path and the * synthetic path. Builds the PendingApproval, wires the timer, fires the * notifier, and ensures the timer is cleared if the notifier itself throws. */ private _registerPending; /** v1.3.7 (F8) — deny + drop the oldest unresolved pending approval so the * maps stay under MAX_PENDING_APPROVALS. Insertion order of a Map is * iteration order, and pendingById is inserted in registration order, so * the first unresolved entry is the oldest. */ private evictOldestPending; private cancelPending; private addAutoAllowRule; private removeAutoAllowRule; private removePending; private sendDecision; } /** * Translate a raw fingerprint string (e.g. `cmd:git+status`) into the * human-readable form shown in IM receipts and `/approval` listings * (e.g. `命令 git status`). The inverse of inputFingerprint()'s internal * encoding — keep the scheme list in sync if you add new fingerprint * variants. * * Unknown / legacy schemes pass through unchanged so the function is * forward-compatible with future fingerprint additions and backward- * compatible with rules created before the semantic split (no scheme). */ export declare function humanizeFingerprint(fingerprint: string): string; /** * Compute the auto-allow fingerprint — the "kind" of call that, when * approved with `all`, covers future similar calls. * * Semantic per tool (rather than a uniform prefix slice) — fixes two * footguns of the old prefix-based approach: * 1. `Bash git status` and `git stash` collided at 5 chars but split at * 10 — neither setting matched user intuition. We extract the first * two tokens (`cmd:git+status` vs `cmd:git+stash`) so the family is * narrow and predictable. * 2. `Read /tmp/a.txt` and `Read /tmp/b.txt` got distinct fingerprints * at any prefix length below the filename, forcing users to re-`all` * every file. We key on dirname (`dir:/tmp`) so one approval covers * the whole directory. * * Prefixes (`cmd:` / `dir:` / `host:` / `path:` / `pat:` / `query:` / * `url:` / `raw:`) keep the key space disambiguated — rules from * different schemes can never accidentally alias each other, and `raw:` * preserves backward compatibility for unknown tools. * * EXPORTED for unit tests. Not part of the bus's public API; production * call sites all live inside this module. */ export declare function inputFingerprint(toolName: string, input: Record): string; /** 进程级单例。agim 启动时 await approvalBus.start() 一次。 */ export declare const approvalBus: ApprovalBus; //# sourceMappingURL=approval-bus.d.ts.map