/** * owner-approval.ts, the one thing that clears an outward-effect refusal, and * why untrusted content cannot produce one. * * ── The failure this replaces ───────────────────────────────────────────── * * The refusal used to tell the owner: reply "send it now" and it will be * resent. The owner replied, and it refused again with the same message. Two things * were wrong at once and either alone would have been a defect: * * - Nothing in production ever minted an `OwnerApproval`. `grantOwnerApproval` * had test callers only, so the advice named a mechanism with no * implementation behind it. Advice that cannot work is worse than no advice: * it spends the owner's trust and teaches them the boundary is broken. * - Had a typed phrase minted one, that would have been the WORSE outcome. A * security boundary cleared by three words of chat text is cleared by * anything that can get those words into the chat, and steering the * conversation toward producing a particular sentence is exactly what the * content this boundary guards against is good at. The gate would have been * theatre. * * ── What an approval is, therefore ──────────────────────────────────────── * * A record that a HUMAN answered a prompt, out of band from the conversation, * about ONE specific action carrying ONE specific payload. Four properties, and * each closes a way the previous design could be ridden: * * 1. **Owner-direct only.** `grantOwnerApproval` refuses every other surface, * so no page, mailbox, channel or document can produce one no matter what * it says. This was already true and is kept. * 2. **Bound to the content.** An approval carries a fingerprint of the exact * fields it approved. Without this, an approval is a standing permit for an * action id: the owner approves one benign `email.send`, and the next * `email.send`, whose body the injected content wrote, rides it. Matching * on the action id alone is matching on the verb, not on the deed. * 3. **Short-lived.** An approval that never expires is a key left in a lock. * The window is minutes, because the gesture and the send are adjacent in * time by construction. * 4. **Single use.** Taken from the store when spent, so one answered prompt * authorizes one action and not a loop of them. * * ── What this deliberately does NOT accept as an approval ───────────────── * * - A phrase in the conversation, however specific. * - `confirm: true` on a tool call. That flag says the CALL was formed * deliberately; it is set by the model, and a model reading injected text is * precisely the thing being defended against. It remains a useful gate in * front of every outward effect and it is not this one. * - Anything derived from message or page content, including a sender address * or a header. * * The gesture that mints one is a prompt the human answers, see the surface's * approval broker. That is why `grantOwnerApproval` takes `surface` from the * CODE PATH rather than from any argument a tool call can carry. */ /** * How long an answered prompt stays spendable. * * Minutes, not hours: the owner answers and the action follows immediately, so * a long window buys nothing and leaves a spendable approval sitting in memory * across everything the process does next. */ export declare const OWNER_APPROVAL_TTL_MS: number; /** Only the owner, speaking directly to the runtime, can authorize work. */ export type ApprovalSurface = 'owner-direct' | 'web-page' | 'email' | 'channel-message' | 'document'; /** * An owner approval for one outward effect, carrying one payload. * * It can only be created from a surface with command authority, which is why * the factory takes the surface and refuses everything else. Page text cannot * manufacture one of these no matter what it says. */ export interface OwnerApproval { readonly action: string; readonly grantedAt: string; readonly expiresAt: string; readonly surface: 'owner-direct'; /** * Fingerprint of the exact fields approved, or `null` when the approving * surface could not enumerate them. * * `null` is the weaker form and is treated as such: it clears only a refusal * that was itself made without content, never a content-derivation finding. * An approval given without seeing the payload cannot authorize a payload. */ readonly contentFingerprint: string | null; } /** * A stable digest of the fields about to leave the machine. * * Field names are included and the record is sorted, so moving a body into the * subject changes the fingerprint. Values are compared after the same * whitespace normalization the taint check uses, so a reflowed line does not * invalidate an approval the owner has just given for the same message, but * any change to the words does. */ export declare function fingerprintOutwardContent(fields: Readonly> | undefined): string | null; export declare function grantOwnerApproval(input: { readonly action: string; /** * The surface the approving gesture arrived on. Supplied by the CODE PATH * that handled the gesture, never read from a tool argument or from content. */ readonly surface: ApprovalSurface; /** The exact fields the owner was shown. Omitting them yields the weak form. */ readonly content?: Readonly> | undefined; readonly ttlMs?: number | undefined; readonly now?: () => Date; }): OwnerApproval | null; /** Why an approval did not clear a refusal, so a message can say which. */ export type ApprovalMismatch = 'none' | 'different-action' | 'expired' | 'different-content' | 'no-content-binding'; /** * Does this approval authorize THIS action with THIS payload? * * `contentInQuestion` is the field record the caller is about to send. When it * is present, the approval must be bound to the same payload, an approval for * a different message is not an approval for this one, and that is the whole * point of the binding. */ export declare function checkOwnerApproval(input: { readonly approval: OwnerApproval | null | undefined; readonly action: string; readonly contentInQuestion?: Readonly> | undefined; /** True when the refusal being cleared is a content-derivation finding. */ readonly clearingContentTaint: boolean; readonly now?: () => Date; }): { readonly authorized: boolean; readonly mismatch: ApprovalMismatch; }; /** * Where a surface keeps approvals between the gesture and the action. * * Single use is enforced here rather than by convention: `take` removes what it * returns, so one answered prompt cannot authorize a loop. Expired entries are * swept on every access, so an abandoned prompt does not leave a spendable * approval in memory for the life of the process. */ export declare class OwnerApprovalStore { private readonly now; private readonly approvals; /** Bounded: a surface that mints without spending must not grow without limit. */ private static readonly MAX_PENDING; constructor(now?: () => Date); /** * Record an approval the owner has just given. * * Returns null, and stores nothing, for any surface without command * authority, so a caller that threads the wrong surface fails closed. */ grant(input: { readonly action: string; readonly surface: ApprovalSurface; readonly content?: Readonly> | undefined; readonly ttlMs?: number | undefined; }): OwnerApproval | null; /** * Spend the approval matching this action and payload, if one is held. * * Removes what it returns. A caller that takes an approval and then does not * perform the action has spent it, which is the safe direction. */ take(input: { readonly action: string; readonly content?: Readonly> | undefined; }): OwnerApproval | null; /** Whether anything is currently spendable, for a status line. */ pendingCount(): number; private sweep; } //# sourceMappingURL=owner-approval.d.ts.map