/** * Answer a pending tool confirmation from a phone. * * The agent already parks on `onRequestPermission` and resumes when that promise * settles. This adds a second way to settle it — nothing in the gate changes, * and whichever answers first wins. A run that would have sat at an empty desk * until someone came back can now continue. * * Telegram carries it because Telegram runs the infrastructure: the bot API is * polled outbound from this machine, so there is no server to host, no inbound * port, and no per-user cost. Long polling, never a webhook — a webhook needs a * public address, which is precisely what we do not want to need. That is also * why this works identically on Linux and Windows, where the Mac app's CloudKit * route does not exist. * * SECURITY: a Telegram bot is reachable by anyone who learns its username. Every * update is checked against the configured chat id before it can decide * anything. Without that check a stranger who found the bot could approve a * destructive command on someone else's machine. See `isFromOwner`. * * PRIVACY: the message carries the command line, so Telegram sees it. Stated in * the docs rather than hidden — it is the reason this is opt-in. * * Ported from the Mac app's `TelegramApproval.swift`; the wire format, the token * matching and the ownership check are deliberately identical, so a bug found on * one side is findable on the other. */ /** What the phone sent back. Mirrors the three buttons the desktop offers. */ export type TelegramAnswer = 'run' | 'skip' | 'cancel'; import { type TelegramCredentials } from './telegramUpdates'; export type { TelegramCredentials } from './telegramUpdates'; export { nextOffset } from './telegramUpdates'; /** * The message text. * * Fenced, because a command containing underscores or asterisks would otherwise * be mangled by Markdown parsing into something that is not what will run — and * approving a command you were shown incorrectly is the one failure this whole * feature must not have. * * `reason` is what the file being written decides — "this file controls what * commands git runs". The terminal dialog shows it, and someone answering from * a phone is the one with the least context, so a message without it asks them * to approve on less than the person at the desk had. */ export declare function composeMessage(command: string, toolName: string, isDestructive: boolean, reason?: string): string; /** * Only the configured chat may decide. * * A bot's username is discoverable, so without this an unrelated Telegram user * could approve a command on someone else's machine. Telegram sends the id as a * number; it is compared as a string because that is how the user typed it. */ export declare function isFromOwner(callback: unknown, chatID: string): boolean; /** * Split `run:` into its parts. * * A callback whose token does not match the question in flight is ignored, and * that is what stops a stale button — tapped after the run moved on — from * answering a later question it was never shown. */ export declare function parseCallbackData(data: string): { answer: TelegramAnswer; token: string; } | null; /** The keyboard sent with the question. Shape pinned by a test — a renamed * callback_data field would leave three buttons that silently do nothing. */ export declare function buildKeyboard(token: string): Record; /** * Telegram's three buttons, in the terms the agent's gate speaks. * * The gate has four outcomes and `classifyPermissionOutcome` fails closed on * anything it does not recognise, so this must be exhaustive rather than * defaulted — a typo here would read as a denial, which is safe but silently * wrong, and the user would tap Run and watch nothing happen. */ export declare function outcomeForAnswer(answer: TelegramAnswer): 'allow_once' | 'reject_once' | 'reject_always'; /** * How a decision reads on the other device, once it is too late to change it. * * The withdrawn side says what happened rather than just going blank, so * someone reaching for their phone a moment late learns what they missed * instead of finding a message that silently lost its buttons. */ export declare function describePermissionOutcome(outcome: string): string; /** * Telegram's failure, phrased for someone who is setting this up. * * `chat not found` is the one people actually hit: the id belongs to a * conversation with a *different* bot, or the bot has never been messaged from * that chat at all. Repeating the API's words and adding what they mean beats * a generic failure that sends them back to the docs. */ export declare function describeApiError(status: number, json: Record | null): string; export declare class TelegramApproval { private readonly credentials; private outstanding; /** Set while a question is open; called to stop listening once it closes. */ private unlisten; /** Called once when the question could not be put at all. Not for a missing * answer — only for a failure to ask. */ private readonly onProblem?; constructor(credentials: TelegramCredentials, onProblem?: (reason: string) => void); /** * Send the question and wait. * * Resolves `null` when no answer arrived — the terminal was used instead, the * caller aborted, or Telegram could not be reached. **A null is never * approval**: the caller keeps its own gate and decides for itself. * * `reason` says what the file being written decides, when it decides * anything, so the phone shows what the terminal shows. */ ask(command: string, toolName: string, isDestructive: boolean, signal?: AbortSignal, reason?: string): Promise; /** * The terminal answered first. Close the question on the phone so nobody taps * a button that would do nothing, and say where it was decided. */ withdraw(decidedInTerminal: string): Promise; /** * Why the last call failed, in Telegram's own words. * * Kept because swallowing it made a misconfiguration indistinguishable from * silence: a wrong chat id answers `chat not found` on the very first send, * and reporting nothing left the user watching a phone that was never going * to ring. */ private lastError; private post; private sendQuestion; private edit; private handle; }