@sema-agent/core
Version:
Stateless, task-oriented AI agent core
208 lines (207 loc) • 14.4 kB
TypeScript
/**
* 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;