export declare function isTimeoutDefaultAllow(): boolean;
/** Print a prominent stdout banner + emit a structured warn log + write
 *  an audit-event row when the timeout-default policy is `allow` at
 *  startup. Idempotent — subsequent calls log only the structured warn,
 *  not the banner (to keep the journal readable on restarts). Also
 *  starts an hourly reminder timer so a long-running deployment that
 *  forgot to revert sees the warning re-surface in `journalctl --since
 *  '-1h'`. */
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 IMHUB_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__imhub__call_agent
     *  call. Optional for top-level runs. */
    parentJobId?: number;
}
/** Wire-format decision sent back to the sidecar. `autoAllowFurther` is
 *  internal-only — `sendDecision` strips it before serializing. */
export type Decision = {
    behavior: 'allow';
    updatedInput?: Record<string, unknown>;
    autoAllowFurther?: boolean;
} | {
    behavior: 'deny';
    message?: string;
};
export interface ApprovalNotification {
    runId: string;
    reqId: string;
    toolName: string;
    input: Record<string, unknown>;
    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;
    };
}
/** Notifier 只负责"通知 IM 推卡片/消息"。不要在这里返回决策；决策走 resolvePending。 */
export type ApprovalNotifier = (n: ApprovalNotification) => Promise<void>;
/** 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<string>;
    stop(): Promise<void>;
    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): 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): ResolvedInfo | null;
    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<string, unknown>;
        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__imhub__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;
    /**
     * 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<string, unknown>;
        /** Optional — used by Claude's MCP path; defaults to '' for synthetic. */
        toolUseId?: string;
        ctx: RunContext;
        dispatch: (decision: Decision) => void;
    }): Promise<void>;
    /**
     * 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;
    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, unknown>): string;
/** 进程级单例。agim 启动时 await approvalBus.start() 一次。 */
export declare const approvalBus: ApprovalBus;
//# sourceMappingURL=approval-bus.d.ts.map