/** * content-taint.ts, does THIS outward action's content derive from something * a stranger wrote? * * ── Why the question has to be this precise ─────────────────────────────── * * The coarse question, "has this process read anything untrusted?", is * useless in a daemon. A daemon reads mail and pages continuously, so the * coarse answer is permanently yes, and a boundary that is permanently * tripped gets replaced by a disclosure nobody reads. That is exactly what * happened: the daemon ended up *reporting* untrusted exposure on a send * receipt while a product with a human attached *refused* the send. The * unattended surface was the most permissive one, which is backwards, an * unattended daemon is where a prompt injection pays off best, because there * is nobody to notice. * * So the question asked here is narrow and answerable: does the CONTENT of * this specific outward action derive from untrusted input? A scheduled report * that queries a database and mails a summary derives from nothing a stranger * wrote and proceeds. A send whose recipient, subject or body carries text that * came out of a page or a mailbox is refused, on every surface, daemon * included. * * ── How derivation is detected ──────────────────────────────────────────── * * By overlap with the untrusted text actually read, not by provenance * bookkeeping the caller could forget to thread. Two signals, both conservative * in the direction of refusing: * * - a shared run of `MIN_SHARED_WORDS` normalized words, which catches a * quoted or lightly-reworded instruction; * - a shared literal span of `MIN_SHARED_CHARS` characters, which catches a * url, an address, an account number or a token copied verbatim, the * payloads that do not look like prose. * * Both thresholds are deliberately above the length of ordinary shared * phrasing ("thanks", "let me know", a greeting), because a check that fires on * every polite sentence would be turned off within a week. * * What this does NOT claim: that a sufficiently clever paraphrase is caught. It * is not a classifier and cannot be. It catches derivation that leaves textual * evidence, which is what copying an instruction out of a message looks like. * The remaining defence for the rest is that untrusted content carries no * authority to begin with, and that outward actions still require confirmation. */ /** A shared run of this many normalized words counts as derivation. */ export declare const MIN_SHARED_WORDS = 8; /** A shared literal span of this many characters counts as derivation. */ export declare const MIN_SHARED_CHARS = 40; /** Untrusted text retained for comparison, with where it came from. */ export interface TaintSource { readonly surface: string; readonly origin: string; readonly text: string; } export interface TaintOptions { /** * Fields tested by EXACT CONTAINMENT rather than by the length thresholds. * * A recipient address is short and high-signal: `accounts-payable@vendor.example` * is 3 words and 31 characters, under both thresholds, so an injection that * only redirects where mail goes would pass a length test entirely. Length is * the wrong instrument for a field where the whole value IS the payload. */ readonly exactMatchFields?: readonly string[] | undefined; /** * Recipients that are allowed even when they appear in untrusted text. * * Exactly one case: replying to where a message actually came from. The * address must be established from DELIVERY EVIDENCE, the envelope sender, * and never from a `From:` header, which the sender writes. Without this, * every legitimate auto-reply is refused, because the address it replies to * is by definition present in the message it answers. */ readonly replyToEnvelopeSenders?: readonly string[] | undefined; /** * Strip quoted regions from these fields before checking. * * A reply that quotes the message it answers repeats it verbatim by design. * Quoting is not derivation of an INSTRUCTION; it is context. Whether that * is safe is a judgement the owner should make knowingly, see the module * header for what stays refused. */ readonly stripQuotedFields?: readonly string[] | undefined; } export interface TaintFinding { /** Which outward field carried it, 'body', 'subject', 'to', … */ readonly field: string; readonly surface: string; readonly origin: string; /** * The overlapping text, truncated. Included so a refusal can SHOW the * operator what matched rather than asserting a match they cannot check. */ readonly excerpt: string; readonly kind: 'shared-words' | 'shared-span'; } /** * Find every field of an outward action whose content derives from untrusted * input. * * `fields` is a record of the caller's own field names to their values, so a * refusal names the field an operator would recognise ("body", "subject") * rather than an index. */ /** * Remove quoted regions from a reply body. * * Two conventions cover nearly all real mail: `>`-prefixed lines, and an * attribution line ("On , wrote:") after which everything is * the quoted original. Anything not matched stays in and is still checked. */ export declare function stripQuotedRegions(body: string): string; export declare function findContentTaint(fields: Readonly>, sources: readonly TaintSource[], options?: TaintOptions): readonly TaintFinding[]; /** * The refusal an operator reads. * * Names the field, the surface and the origin, and shows the overlapping text, * because "refused: untrusted content" with no evidence is indistinguishable * from a bug and gets worked around. */ export declare function describeContentTaint(action: string, findings: readonly TaintFinding[]): string; //# sourceMappingURL=content-taint.d.ts.map