UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

208 lines (207 loc) • 14.4 kB
/** * design/319 (B ticket) — detect-and-disclose TRAILER for reminder-shaped text in external data, * plus the MCP/web exact-mark defuse arm, in ONE ordered pipeline. * * The mark (A ticket, reminder-mint.ts) makes a forged `<system-reminder>` byte-distinguishable; * this module is the adjacency half: when a verbatim outlet (Read text/notebook/PDF text, the MCP * success arm) serves external bytes that LOOK like the engine's reminder tag, a trusted, marked * reminder is appended AFTER the data saying exactly what was found — the data bytes themselves * stay untouched on the strong quote-back lanes (Read↔Edit `old_string`; G3 is the veto gate). * * FOUR-STEP ORDER (design/319 §3e, single-sourced here so no outlet can re-derive it wrong): * ① the caller finalizes the external projection (decode/concat/truncate done, trusted tails * NOT yet appended) and hands it in as ordered text segments; * ② the WHOLE projection is scanned ({@link scanReminderShaped} — full scan, never first-match: * a bare decoy ahead of a marked forgery is judged marked) and the verdict recorded; * ③ (MCP/web outlets only) exact-mark bytes are defused ({@link defuseExactMark}) — detection * MUST precede this step: the defused text no longer carries the exact mark, so a reordered * pipeline would misreport a marked forgery as the bare form (a disclosure that lies); * ④ the trailer is appended, marked-form copy ALWAYS winning over the bare copy. * * ENVELOPE FAMILY (the envelope-census batch). `<system-reminder>` is not the only authority envelope * the engine mints — untrusted-text.ts `ENGINE_ENVELOPES` is the census, and the rows flagged * `disclosed` are scanned here too ({@link scanEnvelopeShaped}, step ② beside its sibling). They carry * no mark, so their disclosure sentence is POSITIONAL rather than byte-level ("an authentic one is * minted by the engine as its own message and never arrives inside file/command/server bytes"), which * is what lets the whole family be covered with ZERO byte change on any lane. The two families share * one TRAILER but keep SEPARATE throttle windows (see the ④ note): sharing the window would let an * envelope-only projection suppress a later reminder-only one, which is a behavior change to a * shipped arm rather than a shared budget. With no envelope hit, every arm below renders * byte-identically to the design/319 shipped copy — across a sequence, not just within one call. * * THROTTLE (bare form only): a code agent reading engine-family source trips the bare form * constantly (this repo's own tree carries the tag literal in dozens of files) — real disclosures, * not misfires, but they must not tax every page of a long read. The bare copy therefore dedups * per throttle key (the file path / the MCP server:tool) inside a 60s window (the gh-rate-limit * hint's window precedent). The MARKED copy is NEVER throttled: on the non-defusing Read lanes it * is the only in-band warning a leaked/forged mark gets, and consuming its budget with source * reads would silence exactly the rare event the trailer exists for (design/319 r2-F2). * * OBSERVATION SEAT: every append/defuse/suppression bumps a per-run counter keyed * `<outlet>.<form>` — surfaced as `TaskResult.stats.mechanisms.reminderDisclosures` — the G9② * trigger-rate reading that the D-4/D-6 re-rulings (defuse/trailer widening to Read/Bash/Grep) * are waiting on. * * A `mark` of `undefined` disables the whole pipeline (segments returned untouched, no trailer): * a library-direct mount that threaded no mark has no system-prompt declaration either, so a * trailer speaking about "this session's mark" would reference a contract that does not exist — * same byte-compat posture as every markless arm of the A ticket. */ /** Per-run mutable trigger counters, keyed `<outlet>.<form>` (e.g. `read.bare`, `mcp.defused`, * `read.bare_throttled`). Minted once per prepared task leg and threaded to every outlet; folded * into `TaskResult.stats.mechanisms.reminderDisclosures` when any key is non-zero. */ export type ReminderDisclosureCounts = Record<string, number>; /** Bump one observation counter (no-op without a counts seat — library-direct mounts). */ export declare function bumpReminderDisclosureCount(counts: ReminderDisclosureCounts | undefined, key: string): void; /** * OBSERVATION-ONLY seat for a BARE MARK ECHO — the session's exact mark VALUE appearing in external * bytes that carry no reminder-shaped TAG. {@link scanReminderShaped}'s grammar (the single detection * predicate, shared with the neutralizer so disclosure and defusal can never drift apart) judges * TAGS, so a naked 22-character mark reaches a verbatim/fenced outlet with `hit: false` — no `marked` * verdict is reachable, and before this seat no counter was either. * * The DEFENSE half of that shape is ruled covered and stays untouched here: a bare value carries no * authority FORM (the fenced arms neutralize every reminder-shaped tag, so the value is inert data), * and a mark VALUE with no tag around it asks the model nothing (the declaration speaks of reminder * TAGS, not of a bare value, and orders the value never written anywhere). What was * missing is purely this module's own stated seat: a lane whose leak/echo rate cannot be COUNTED * cannot later be argued about (the trigger-rate reading the trailer/defuse widening re-rulings * wait on). So this port counts, never rewrites and never appends — zero model-facing byte change * on every caller. * * Fires only where the defuse does not: on a defusing outlet any mark occurrence already lands as * `<outlet>.defused` + `<outlet>.marked`, so `<outlet>.mark_echo` reads unambiguously as "an arm * that serves this outlet's bytes without defusing saw the mark" — the Read-family verbatim lanes, * and the fence-but-never-defuse FAILURE arms (an MCP tool call's server-signaled, protocol and raw * rejections, WebFetch's non-2xx result, WebSearch's backend error) whose success twins disclose. * * CALL-SITE RULE (settled after three adversarial rounds spent moving the observation point between * successive bounds — truncate vs fence, fence-bound vs raw, and a fragment that never entered the * fence at all): **observe ONCE, at the arm's single composition or exit point, on the WHOLE string * that arm hands to the model.** Never on a fragment, never before a bound the arm itself applies. * The enumeration of "which pieces are external, and which bound has landed on each" was the defect; * a composed string has no enumeration to get wrong. * * CONTRACT: the count is an UPPER BOUND on model exposure, never an under-count. Bounds an arm does * not own — a downstream, outlet-independent clipper on the assembled tool result — may still drop * part of what was observed, so a mark surviving only into a discarded tail is counted anyway. That * asymmetry is chosen: a seat that misses a real leak is worthless; one that occasionally * over-reports is merely conservative. */ export declare function observeReminderMarkEcho(input: { text: string; mark: string | undefined; outlet: ReminderDisclosureOutlet; counts?: ReminderDisclosureCounts; }): boolean; /** Bare-form dedup window per throttle key (the gh-rate-limit 60s precedent — see module header). */ export declare const BARE_REMINDER_DISCLOSURE_WINDOW_MS = 60000; /** The outlets that run this pipeline. Read/Bash/Grep clean output deliberately do NOT appear: * Bash/Grep carry no trailer at all (ruled — for the `<system-reminder>` family the mark covers * their impersonation half; the observation seat is the widening data), and no outlet here ever * defuses Read-family bytes. * * SCOPE CORRECTION (the envelope-census batch): that ruling's premise held for ONE envelope. The * engine mints several model-facing authority envelopes (untrusted-text.ts `ENGINE_ENVELOPES`), and * the others carry no mark at all, so on the Bash/Grep lanes a forged `<task-notification>` still * has neither a mark test nor a trailer. The ruling itself is unchanged here (widening Bash/Grep is * a separate, model-facing decision with its own trigger-rate evidence bar); what changed is that * the outlets which DO carry a trailer now report the whole family, not just the reminder tag. */ export type ReminderDisclosureOutlet = "read" | "notebook" | "pdf" | "mcp" | "webFetch" | "webSearch"; export interface ReminderDisclosureInput { /** The finalized external projection, as ordered text segments (one per model-facing text * block; a single-string outlet passes `[text]`). Detection judges the seam-joined view so a * tag synthesized across a block seam is still caught (over-detection is the safe direction). */ segments: readonly string[]; /** The session's reminder provenance mark; `undefined` disables the pipeline (see header). */ mark: string | undefined; outlet: ReminderDisclosureOutlet; /** True ONLY on the MCP-success/web outlets (§3b) — Read/notebook/PDF must pass false: their * lanes are zero-byte-change by invariant (G3 veto). */ defuseExactMark: boolean; /** Bare-form throttle seat: `windows` maps key → next-allowed epoch ms (per tool closure ≈ per * task). Absent ⇒ the bare form is never suppressed (single-shot outlets). */ throttle?: { key: string; windows: Map<string, number>; now?: number; }; counts?: ReminderDisclosureCounts; } export interface ReminderDisclosureOutcome { /** The segments to serve — byte-identical to the input segments unless `defused`. */ segments: string[]; /** A full trusted reminder block (self-marked) to append AFTER the data, or `undefined`. */ trailer: string | undefined; /** `marked`/`bare` are the `<system-reminder>` verdicts (mark present vs absent). `envelope` is the * ENVELOPE-FAMILY-only verdict: no reminder-shaped text at all, but text shaped like one of the * engine's other authority envelopes. When BOTH are present the reminder verdict names the form * (it is the stronger one) and the envelope sentence is appended to its copy. */ form: "bare" | "marked" | "envelope" | undefined; /** True iff the exact-mark defuse rewrote bytes. Judged over the JOINED projection (adversarial * round: a mark split across a block seam is invisible as a seam to the model and must be * defused too — {@link defuseExactMarkInSegments}), so on a defusing outlet this is `true` * exactly when the joined projection carried the mark, and the post-defuse concatenation never * does. */ defused: boolean; /** True iff a trailer that was due got suppressed by a throttle window. Each family owns its own * window, so a mixed projection can emit one family's copy while reporting the other's * suppression — `throttled: true` alongside a defined `trailer` is that state, not a contradiction. */ throttled: boolean; } /** * What a planned disclosure will say, as a value: which sentences the trailer carries. The Read carrier * renders it with {@link renderReminderDisclosureTrailer} (the bytes the pipeline has always appended); * another carrier renders the same verdict in its own words. Undefined `reminder` and `envelopeTags` ⇒ no * trailer. */ export interface ReminderDisclosureVerdict { /** The `<system-reminder>`-family sentence: the marked copy (with whether the defuse arm rewrote bytes) * or the bare copy. */ reminder?: { form: "marked"; defused: boolean; } | { form: "bare"; }; /** The envelope-family sentence, with the REGISTRY tag names it names (never matched bytes). */ envelopeTags?: readonly string[]; } /** The state a planned disclosure changes — applied by {@link commitReminderDisclosureEffects}, never by * the plan: the counter keys to bump (one entry per bump, in pipeline order) and the throttle windows to * advance (key → next-allowed epoch ms). */ export interface ReminderDisclosureEffects { counts: readonly string[]; windows: ReadonlyArray<readonly [string, number]>; } /** {@link planReminderDisclosure}'s answer: the outcome the pipeline reports, the verdict a carrier renders, * and the effects a delivery commits. */ export interface ReminderDisclosurePlan { outcome: Omit<ReminderDisclosureOutcome, "trailer">; verdict: ReminderDisclosureVerdict; effects: ReminderDisclosureEffects; } /** * Plan the four-step pipeline over one finalized external projection WITHOUT changing anything: the * throttle windows are read (a due bare/envelope sentence is suppressed by a live window) but never * advanced, and the counters are not bumped — both land in `effects`, which the caller commits when the * projection is actually delivered ({@link commitReminderDisclosureEffects}). Two plans over the same key * before either commits both see the window open; that is the defined meaning of "effects ride the * delivery". */ export declare function planReminderDisclosure(input: ReminderDisclosureInput): ReminderDisclosurePlan; /** Apply a plan's effects: bump each counter key, advance each throttle window. `windows` is the map the plan * read (the outlet's per-closure throttle seat); either seat may be absent (library-direct mounts). */ export declare function commitReminderDisclosureEffects(effects: ReminderDisclosureEffects, seats: { counts?: ReminderDisclosureCounts; windows?: Map<string, number>; }): void; /** The trailer a verdict renders to on the tool-result carrier — a full trusted reminder block (self-marked), * or undefined when the verdict carries no sentence. Byte-identical to the pipeline's historical trailer. */ export declare function renderReminderDisclosureTrailer(verdict: ReminderDisclosureVerdict, mark: string | undefined): string | undefined; /** * Run the four-step pipeline over one finalized external projection and commit its effects at once — the * single-shot outlets (MCP, web) whose projection is delivered in the same breath. Plan + commit + render: * the windows and counters move exactly as they always did. */ export declare function discloseReminderShaped(input: ReminderDisclosureInput): ReminderDisclosureOutcome;