UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

481 lines (480 loc) • 34.7 kB
/** * Untrusted-text containment (design/53 §2.A — "untrusted-data-everywhere"). * * A multi-agent code-orchestration system treats **every worker's output** (report / diff / code / module) * as untrusted data: an LLM worker can be prompt-injected or adversarial, and its output flows into other * models' prompts (the L3 judge, the leader, the next worker) where it could pose as instructions — * e.g. emitting `</system-reminder>` to escape its framing and forge a verdict (threat BUG1, verify.ts). * * The trusted side of the codebase REJECTS such sequences (runtask.ts: trusted steering / deny reasons must * not contain `</system-reminder>`). Worker text can't be rejected — it must be **contained**: this module * NEUTRALIZES break-out sequences and wraps the text in a clearly-labeled opaque fence the consuming prompt * tells the model to treat as data, not instructions. Structural and reusable across the chain, not a * per-call point-fix. * * This is defense-in-depth, NOT a guarantee: an LLM cannot reliably honor a "this is data" boundary on its * own (instructions and data share one channel). The real boundary is decorrelation + reading the objective * artifact (the diff / working tree) rather than the worker's self-report (design/53 §3, design/54 §3). */ /** * One row of {@link ENGINE_ENVELOPES}. * * WHY A TABLE EXISTS. design/319 spent its whole threat model on a single tag and three independent * texts stated the supporting premise as "this harness mints no other tag" — a MODULE-level * enumeration (`turn-attachments` really does mint only one) promoted to a HARNESS-level claim. The * engine in fact authors several model-facing envelopes, guarded by two different escape families * living in two different modules, and the two envelopes added most recently (`skills`, `scope`) * reached the model with NO guard at all — not because anyone argued they were safe, but because * nothing in the tree listed the set an author was joining. A guard family without a census is a * guard family that silently loses members; this table is that census, and the accompanying * literal-tag test keeps it honest (a new closing-tag literal in `src/` fails until it is classified * here). * * The same discipline as the `ToolSpec` completeness table: the registry is the declaration, the * test is the gate, and a NEW envelope is a two-line edit rather than an invisible omission. */ export interface EngineEnvelope { /** The tag NAME as the engine spells it, without angle brackets. */ readonly tag: string; /** * - `authority` — the engine authors it as model-facing HARNESS speech; seeing the tag is what * makes the model read the content as system information, so a forgery is an authority claim. * - `framing` — the engine wraps UNTRUSTED content in it as a labeled data frame; a forgery * escapes the frame (or closes it early) rather than claiming harness authority. * - `not-an-envelope` — a tag-shaped literal in `src/` that is not an engine-minted model-facing * envelope at all (a MODEL-output grammar the engine parses, a prompt-assembly placeholder, * rendering metadata, or copy that merely TALKS about an envelope). Listed so the census test * has a home for every literal in the tree and no reader has to re-derive the classification. */ readonly kind: "authority" | "framing" | "not-an-envelope"; /** Where the engine authors (or, for `not-an-envelope`, spells) it. */ readonly mint: string; /** How untrusted bytes reaching this envelope's own body/attributes are defused at that site. */ readonly guard: string; /** * True ⇒ neutralized inside every FENCED/inline untrusted projection ({@link delimitUntrusted}, * {@link inlineUntrusted}). Those lanes already rewrite bytes by contract, so a forged authority * envelope arriving through them is defused rather than merely fenced. */ readonly fenced: boolean; /** * True ⇒ DISCLOSED (never rewritten) when it appears in VERBATIM external data — the design/319 * trailer pipeline (reminder-disclosure.ts). Verbatim lanes are byte-frozen by the G3 veto (the * Read↔Edit `old_string` quote-back loop), so the only available judgment bit is an appended, * engine-minted note. Deliberately NOT set for tags whose spelling is common in ordinary data * (`scope` is a Maven POM element; `summary` is HTML) — a trailer that fires on every build file * teaches the model to ignore trailers. */ readonly disclosed: boolean; } /** * The census. Rows are grouped by {@link EngineEnvelope.kind}; within a group, by module. * * KNOWN CEILING (stated, not hidden): the accompanying test enumerates CLOSING-TAG LITERALS in * `src/`, so an envelope whose tag name is INTERPOLATED (`<${tag}>`) is invisible to it. Those are * enumerated here by hand — `teammate-message`, the observer digest's four event tags, and the * dynamic `<{slug}-activity>` wrapper — and each already carries its own escape at its mint site. */ export declare const ENGINE_ENVELOPES: readonly EngineEnvelope[]; /** The registry row for a tag name, or `undefined` when the tag is not in the census. */ export declare function engineEnvelope(tag: string): EngineEnvelope | undefined; /** Registry-derived: the envelopes that read as HARNESS AUTHORITY on sight. */ export declare const ENGINE_AUTHORITY_ENVELOPE_TAGS: readonly string[]; /** * Registry-derived: the tags {@link delimitUntrusted} / {@link inlineUntrusted} neutralize on top of * the always-on reminder tag. These lanes carry EXTERNAL/worker bytes into a labeled data frame and * already rewrite them by contract, so an authority envelope forged inside them is defused rather * than merely fenced — the "forgery can be neutralized, never trusted" direction. * * Deliberately NOT applied inside {@link sanitizeUntrustedText} itself: that primitive is also used * by mint sites over an ALREADY-ASSEMBLED body (the boundary/first-frame reminder wrap runs it over * a body that may legitimately contain the engine's own `<skills>` block), so widening the primitive * would have the engine defuse its OWN envelopes. Containment belongs to the fence, not the * primitive. */ export declare const FENCED_LANE_ENVELOPE_TAGS: readonly string[]; /** * The same family as {@link FENCED_LANE_ENVELOPE_TAGS}, as a MUTABLE array for every body that will be * SHELLED in a marked `<system-reminder>` — the attachment sink, the listing/diagnostics producers, * the durable-resume orphan notice, the post-tool-batch hook relay, the git frame. * * These are not fences. They render deployment/server/model-supplied strings into a body the run loop * then wraps in engine authority, so a forged envelope inside one is laundered by the wrapper. The * wrapper itself cannot blanket-neutralize the family — it also shells bodies that legitimately ARE * envelopes (TWO owners today: `buildSkillsBlock`'s `<skills>` fence and `renderTotalTokensReminder`'s * `<total_tokens>` body) — so containment is expressed as OWNERSHIP: this full set for every body that * owns nothing, minus its own tag for each one that does (single source: `ATTACHMENT_TAGS_OWNED` in * turn-attachments.ts, which the per-source `attachmentEnvelopeTags` derivation reads). * * Allocated ONCE so the sanitizer's memoized break-out regex is keyed by a stable value. */ export declare const SHELLED_BODY_ENVELOPE_TAGS: string[]; /** * Registry-derived: the tags the design/319 disclosure pipeline reports when they appear in VERBATIM * external data (see {@link scanEnvelopeShaped}). `system-reminder` is absent on purpose — the mark * pipeline scans it with a stronger (marked vs bare) verdict of its own. */ export declare const DISCLOSED_ENVELOPE_TAGS: readonly string[]; /** The result of one envelope-shaped scan of an external projection. */ export interface EnvelopeShapedScan { /** ≥1 disclosed-family envelope tag (open or close) is present. */ hit: boolean; /** The REGISTRY spellings that matched, de-duplicated, in registry order. Registry-owned strings — * never the matched bytes — so a caller may render them into trusted copy safely. */ tags: string[]; } /** * The envelope-family sibling of {@link scanReminderShaped}: detect text shaped like one of the * engine's OTHER authority envelopes inside external data. Read-only — verbatim lanes are byte * frozen (design/319 G3 veto: the Read↔Edit `old_string` quote-back loop), so the disclosure * pipeline's only move is to append an engine-minted note saying what was seen. * * Grammar is deliberately the same shape as the reminder scan (attribute / case / tag-internal * whitespace tolerant, open OR close tag), and the tag SET comes from the registry rather than a * hand-copied list, so adding an envelope to {@link ENGINE_ENVELOPES} with `disclosed: true` is the * whole change. Same accepted residual as the reminder scan: a tag the grammar does not recognize * (fragmented, Unicode-lookalike) is not a hit — defense in depth, not a guarantee. */ export declare function scanEnvelopeShaped(text: string): EnvelopeShapedScan; /** * Defuse a caller-declared occurrence of ONE specific envelope tag (opening AND closing) inside * content, CC `zZe` @8339092 verbatim: ``t.replace(new RegExp(`<(?=/?${e}(?:[>\\s/]|$))`, "gi"), "<\\")``. * * Homed here (beside {@link ENGINE_ENVELOPES}) rather than in the observer module that first needed * it: "escape one named envelope tag" is a containment primitive, and the registry's guard column * points at it. Re-exported from `agents/observer.ts` for its existing importers. * * Deviation from CC (hardening): the tag is regex-escaped before entering the RegExp — CC * interpolates raw, safe only because its slugs are `[a-zA-Z0-9_-]`; ours are too, but we don't rely * on the caller for that invariant. * * NOTE the two sanctioned escape SHAPES in this tree, and why they are not merged: this one inserts * a backslash (`<` → `<\`) because it is a byte-level CC anchor on the observer/teammate lanes; * {@link sanitizeUntrustedText} writes the entity `&lt;` in the bracket's place, CC's own spelling * for its reminder-tag escapers. Both are idempotent, both break the tag, and both are printable * ASCII that no format-character cleaning can remove; a merge would move bytes on one of the two * families for no security gain. */ export declare function escapeEnvelopeTag(tag: string, text: string): string; /** design/319 (B ticket) — what one reminder-shaped scan of an external projection found. */ export interface ReminderShapedScan { /** ≥1 reminder-shaped tag (open or close; attribute/case/whitespace tolerant) is present. */ hit: boolean; /** Some reminder-shaped TAG's own bytes contain the session's exact mark value — the strongest * verdict (a full scan decides this, never the first match: a bare decoy ahead of a marked * forgery must still be judged marked). `false` whenever `mark` is `undefined`. */ hadCurrentMark: boolean; } /** * design/319 (B ticket) — the ONE detection predicate for reminder-shaped text in EXTERNAL data * (the Read/notebook/PDF/MCP disclosure trailers and the MCP/web exact-mark defuse all judge with * THIS scan). It reads the same {@link SYSTEM_REMINDER} regex the neutralizer rewrites with, so * "what gets defused on contained lanes" and "what gets disclosed on verbatim lanes" can never * drift apart — do not hand-copy the tag pattern anywhere else (same single-source discipline as * {@link defuseFenceMarkers}). Attribute-tolerant / case-insensitive / tag-internal whitespace * tolerant by construction; a tag the regex does not recognize is NOT a hit (the residual gap * between this grammar and what a model might read as authority is an accepted, documented * residual — same class as the Unicode-lookalike note on {@link defuseFenceMarkers}). * * The scan is read-only over the FINALIZED projection (post concatenation/truncation, pre any * trusted tail) — callers must scan BEFORE any byte-changing transform (design/319 §3e four-step * order): a defused/neutralized projection no longer carries the exact mark, so scanning after * would misreport a marked forgery as the bare form (the lying-disclosure shape). */ export declare function scanReminderShaped(text: string, mark: string | undefined): ReminderShapedScan; /** * design/319 §3b (B ticket) — EXACT-MARK defuse for the MCP-success/web outlets: insert a ZWSP * inside every occurrence of the session's exact mark value so the bytes no longer spell the * current mark. This is the ONE sanctioned byte change on those lanes (their zero-byte-change * invariant's single exception): a server/page legitimately producing the 22-char session mark has * definitionally zero probability — the value lives only in this session's prompt and checkpoint — * so an occurrence is either a forgery or a leak echo, and defusing it turns a silent authority * forgery into a visible mismatch (the accepted failure direction; a legitimate round-trip echo of * a leaked mark defuses too, and the next hop then fails loudly instead of re-arming the forgery). * NEVER applied on Read/Bash/Grep (the strong quote-back lanes stay verbatim — ruled, observation * seat only). Idempotent: the rewritten text no longer contains the exact mark (ZWSP is not a * base64url character, so the insertion can also never synthesize a new occurrence). */ export declare function defuseExactMark(text: string, mark: string): string; /** * design/319 §3b — the SEGMENT form of {@link defuseExactMark} (adversarial round: a server can * split the mark across adjacent MCP text blocks; the block seam is invisible in the model-facing * concatenation, so a per-segment defuse would be a bypass). Judged over the JOINED projection and * the ZWSP insertion mapped back into the owning segment, so the post-defuse concatenation NEVER * contains the exact mark — including occurrences that straddle one or more seams (one ZWSP inside * the occurrence's span breaks it wherever the split fell). Returns the same array (`changed: * false`) when the joined projection has no occurrence; otherwise fresh segment strings whose * concatenation equals the single-string form's output byte for byte (it IS the single-string * form's implementation). * * OVERLAP-AWARE (adversarial round 2): a BORDERED mark (first character = last character — ~1/64 * random mints) admits overlapping occurrences (`mark + mark.slice(1)` carries two, one starting on * the other's final character), so enumeration advances ONE character per match, never by the mark * length — a skip-by-length walk defused the first and left the second exact while the trailer * claimed neutralization. Every enumerated occurrence gets a ZWSP inside its own span; ZWSP is not * a base64url character, so insertions can never synthesize a new occurrence and the result is * idempotent (no exact mark survives ⇒ a second pass is byte-identical). */ export declare function defuseExactMarkInSegments(segments: readonly string[], mark: string): { segments: string[]; changed: boolean; }; /** * Neutralize structural break-out sequences in UNTRUSTED text so it can't escape its framing into * model-facing instructions. Always neutralizes `<system-reminder>` / `</system-reminder>` (the codebase's * elevated-authority wrapper) by writing the entity {@link ESCAPED_LT} in place of the leading `<` (a form no * format-character or whitespace cleaning removes — see there). Pass `extraTags` to also * neutralize a CALLER's own data-framing tags — e.g. memory recall frames note text inside its own injection * tags, so an untrusted note could emit the closing tag to break out; the caller passes those tag names * (`MEMORY_INJECTION_TAGS`). Tag names must be literal (alphanumeric/hyphen) — they are code-supplied * wrapper names, never untrusted input. Idempotent for prompt assembly (a defused tag no longer matches). * * WHY THE ALWAYS-ON SET IS ONE TAG AND NOT THE WHOLE {@link ENGINE_ENVELOPES} AUTHORITY FAMILY: this is a * PRIMITIVE, and mint sites also run it over an ALREADY-ASSEMBLED body — the boundary/first-frame reminder * wrap sanitizes a body that may legitimately BE the engine's own `<skills>` block, and the memory write * scan diffs against it to decide what markup a note may contain. Widening the primitive would have the * engine defuse its own envelopes and would silently move an unrelated store's admission rule. Family-wide * containment therefore lives at the FENCE ({@link delimitUntrusted} / {@link inlineUntrusted}, via * {@link FENCED_LANE_ENVELOPE_TAGS}) and at each envelope's own mint site (the registry's guard column). */ export declare function sanitizeUntrustedText(text: string, extraTags?: string[]): string; /** * [c209-C] 收口 C1/C2 (metadata-driven, replaces the shape-anchored * `stripLeadingSystemReminders`) — cut the engine-prepended prefix off a user-lane message using the * EXACT length recorded on the message at mint time (`UserMessage.enginePrefixChars`, stamped where * the engine concatenated first-frame listing reminders / a UserPromptSubmit additionalContext * reminder / a resume continuation's trusted head AHEAD of the real user text). The first user turn * carries those bytes as engine guidance, not user speech — a consumer that re-presents user * messages under a `[user]`/`[User]:` label (the auto-mode classifier window, the compaction * summarizer input) must not let them (a) impersonate user authority or (b) evict the real * objective from a head-truncated excerpt. * * The old helper GUESSED by reminder shape, which mis-fired in both directions (收口轮): a * same-line engine mint (`<system-reminder>[deadline] …</system-reminder>`) was NOT recognized and * entered the window under `[user]` authority, while a user who legitimately opened their own * prompt with a byte-exact reminder block WAS stripped. Metadata removes the guess entirely: * - a message WITH `enginePrefixChars` is cut at exactly that offset (no regex, no shape matching); * - a message WITHOUT metadata is rendered VERBATIM — user-authored `<system-reminder>` prefixes * are never stripped, and OLD sessions (persisted before the field existed) render unchanged. * Conservative direction, deliberate: a classifier/summarizer seeing engine listings as user text * is acceptable; silently losing real user text is not. * An out-of-range length (≤0 / > text length — corrupt or foreign data) is ignored the same way. * [c209-C] R5: the parameter is `unknown` — the field can arrive as shape-poison from old/foreign * imported sessions (a string "9", null) and the typeof/integer checks below are the total guard. */ export declare function cutEnginePrefix(text: string, enginePrefixChars: unknown): { text: string; cut: boolean; }; /** An engine-trusted segment of a user-lane message: a half-open `[start, end)` UTF-16 code-unit * range — see `UserMessage.engineSegments` (engine/llm/types.ts) for the full field contract. */ export interface EngineSegment { start: number; end: number; } /** * [c209-C] R5 — the three-state result of {@link normalizeEngineSegments}. `absent` * and `malformed` are DIFFERENT states on purpose (R4 #1): a message whose `engineSegments` field * is present-but-malformed must render WHOLLY verbatim — the metadata family is suspect, so a * co-present `enginePrefixChars` must NOT get a second chance to cut (absent metadata is the only * state where the prefix sugar applies). */ export type NormalizedEngineSegments = { readonly state: "absent"; } | { readonly state: "malformed"; } | { readonly state: "valid"; readonly segments: ReadonlyArray<EngineSegment>; }; /** * [c209-C] R4 终判 (R5: three-state + exception-safe over `unknown`) — validate * `UserMessage.engineSegments` against the text it indexes into. * - `absent`: the field is `undefined` (or an empty array — it claims no engine bytes), so the * `enginePrefixChars` sugar MAY still apply. * - `valid`: EVERY range is well-formed (elements are objects whose `start`/`end` are integers, * `0 ≤ start < end ≤ text.length`, ascending and non-overlapping — `prev.end ≤ next.start`). * Returns FRESH `{start, end}` copies (canonical plain data, e.g. for the import gate). * - `malformed`: anything else — including non-array values (`null`, `{}`), non-object elements * (`[null]`), and shape-poison (`[{}]`, string offsets) from old/foreign session data. ONE bad * range invalidates the WHOLE field and consumers render the entire message verbatim, prefix * suppressed (losing real user text is worse than showing engine bytes). NEVER throws: the input * is `unknown` by contract and any exotic shape (throwing getters included) collapses to * `malformed`. * Shared single source for the mint clamp (agent-harness `createUserMessage`), the session import * gate, and every projection side (auto-mode window / compaction serializer / prompt suggestions). */ export declare function normalizeEngineSegments(text: string, segments: unknown): NormalizedEngineSegments; /** * [c209-C] R4 终判 — the SEGMENT sibling of {@link cutEnginePrefix}: remove every * engine-trusted `[start, end)` segment from a user-lane message, keeping the bytes OUTSIDE the * segments verbatim (the caller/operator-authored content that derived views may present under * `[user]`/`[User]:` authority). Unlike the prefix cut, segments can sit mid-message: the resume * continuation preserves issue order (parked steer first, wake second — the public * ResumeOutcome/ResumeRun contract), so a trusted engine frame may legally follow an untrusted * fenced one. * * [c209-C] R5 — the result distinguishes WHY nothing was cut: `malformed:true` * means the field was PRESENT but bad (per {@link normalizeEngineSegments}) — the caller must * render the whole message verbatim and MUST NOT fall through to `enginePrefixChars` (segments * exist ⇒ they own the message; malformed ⇒ the metadata family is suspect wholesale). Only * `cut:false, malformed:false` (absent metadata) allows the prefix-sugar fallback. Never throws. */ export declare function cutEngineSegments(text: string, segments: unknown): { text: string; cut: boolean; malformed: boolean; }; /** * [c209-C] R5 — the ONE flatten under which `engineSegments`/`enginePrefixChars` * offsets are DEFINED: a user-lane message qualifies only when its content is a plain string or a * SINGLE text block (whose text is that same string). Multi-block / image-bearing content returns * `undefined` and consumers render the message VERBATIM with their own local flatten — the two * projection sides flatten blocks DIFFERENTLY (the auto-mode window joins with `\n` + `[image: …]` * block markers; the compaction serializer drops non-text blocks and joins with no separator), so a * single `[start, end)` range would point at different characters on each side. The restriction * costs nothing real: every engine-assembled prefixed/segmented message (first-frame objective * concatenation, resume continuation) is minted from ONE plain string — the sole exception is a * first-frame objective that ALSO carries user images, which now falls back to verbatim (the * accepted conservative direction: engine listing bytes visible under user authority, never lost * user text). Single-source helper shared by both projections, the suggestions transcript, and * the session import gate. Accepts `unknown` (import-side data) and never throws. */ export declare function flattenableUserText(content: unknown): string | undefined; /** * Defuse this module's triple-angle fence sentinels (`<<<` / `>>>`) inside an untrusted body so the body * cannot forge {@link delimitUntrusted}'s own OPEN/CLOSE markers and break out of the fence. A worker (or * an external page via `web_fetch`, an MCP resource — the label format is open-source, so the close marker * `<<<END UNTRUSTED <label>>>>` is guessable) could otherwise emit the literal close marker followed by * top-level injection. Caps any run of 3+ identical brackets at 2 consecutive by writing the entity * ({@link ESCAPED_LT} / {@link ESCAPED_GT}) in place of every third bracket — the reader still sees the text, * no `<<<`/`>>>` triple survives, and no format-character cleaning can put one back. Idempotent: a capped run * (≤2) never re-matches `{3,}`. Defusing EITHER bracket breaks the marker; we defuse both for symmetry. * * Residual (council QUESTION, accepted): only ASCII `<`/`>` (U+003C/003E) are defused. A Unicode lookalike * (fullwidth `<`/`>`, U+FF1C/FF1E) is NOT — but a fullwidth marker is a DIFFERENT string from the real * ASCII close sentinel, so it cannot break out at the string level; at worst it visually resembles the * fence to the model. Consistent with this module's "defense-in-depth, NOT a guarantee" posture. * * Exported (design/138 S2-C, O-F11/C-F10): the memory write-time scan detects a forged fence by diffing * `defuseFenceMarkers(text) !== text` — the ONE sentinel definition serves both the defusing and the * detection, so the two can never drift apart. Do not hand-copy the `<<<`/`>>>` pattern anywhere else. */ export declare function defuseFenceMarkers(body: string): string; /** * design/177 — control-character hygiene for an untrusted BLOCK that is rendered as-is under a framing * line (shared-store document content). Two steps, in order: * ① fold `\r\n` and a lone `\r` to `\n`, so a carriage return cannot repaint a rendered line; * ② scan by code POINT and replace every C0 control except tab and newline, plus the C1 range * (U+007F–U+009F), with U+FFFD. * * REPLACE, never delete — deliberately, and this is the half a naive strip gets wrong: deletion makes a * document with an embedded escape sequence read as clean text, while a replacement character is a scar * both the model and a human reviewer can see. Surrogate pairs survive intact (the scan is by code * point, not by UTF-16 unit). * * This is the CHARACTER layer only. Structural containment (elevated-authority tags, fence sentinels) * is {@link sanitizeUntrustedText} + {@link defuseFenceMarkers}, which the same pipeline applies after * this one — a framing line is a discipline statement, tag neutralization is the structural defense, * and an untrusted block needs both. */ export declare function defuseControlChars(text: string): string; /** * Make untrusted text safe to interpolate INLINE on a trusted prompt line (not inside a {@link delimitUntrusted} * fence) — e.g. design/80 D-F echoes a chosen option label into the "The user answered:" block. Folds EVERY * line/space separator to a single space so the text cannot forge a new line — `\s` (covers CR/LF/TAB/VT/FF and * U+2028/U+2029 + the Unicode spaces) PLUS the whole C0 + DEL + C1 block (U+0000–U+001F, U+007F–U+009F), which * `\s` does NOT fully match: the C1 half carries the 8-bit CSI/OSC/ST forms (U+009B/U+009D/U+009C) a * C1-honoring terminal treats like their ESC-prefixed spellings, so leaving them through would let a * 'sanitized' value repaint the trusted line it is interpolated onto (adversarial round finding) — caps the * length, then applies the same tag-neutralization + fence-sentinel (`<<<`/`>>>`) defusing the fenced * body gets. * * The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope * family, not the reminder tag alone. A value interpolated onto a TRUSTED line is the cheapest place to * open a forged envelope, and this lane rewrites bytes by contract already, so the whole family is * defused here. * Defense-in-depth, NOT a guarantee (same posture as the rest of this module). */ export declare function inlineUntrusted(text: string, maxLen?: number): string; /** The mark a cut text ends in: {@link inlineUntrusted}'s, and a notice's machine-read rule text's * ({@link import("./permission-rule-model.js").noticeRuleText}) — one ellipsis, spelled once. */ export declare const INLINE_TRUNCATION_MARK = "\u2026"; /** The code points {@link inlineUntrusted} may return beyond its content cap: the one truncation mark. The longest result for a cap * of `maxLen` is `maxLen` plus this. */ export declare const INLINE_TRUNCATION_MARK_CODEPOINTS: number; /** The per-ENTRY ceiling inside a `ProbeCause` operand family (checkpoint-store's * structured probe account), sized like the descriptor's own `touchedPaths` entries (the same thing * on the same card). Per entry, not per cause: that is the whole point of the structured shape — * one pathological path costs only itself, where a single joined string let it consume every other * entry's room. HOMED HERE (the leaf neutralizer module) rather than with the `ProbeCause` types: * its two consumers — the shell probe that sanitizes at the entry boundary (tools/fs) and the * descriptor builder that re-sanitizes at the persist boundary (core/checkpoint-store) — sit on * opposite sides of a value-import boundary, and a value import from the shell tool back into * checkpoint-store closed an ESM evaluation cycle (checkpoint-store → tool-policy → tools/fs → * checkpoint-store, a latent TDZ). Both already import this module; one cap, no cycle. */ export declare const PROBE_CAUSE_PATH_MAX = 200; /** The bloat ceiling on a probe's one-sentence account (`RiskDescriptor.probeReason` / * `AskRequest.probeReason`); the neutralizer is the INJECTION ceiling, this is only the LENGTH one. Homed * here for the same reason as the constant above: three readers — the shell probe that MINTS the sentence * (tools/fs) and must fit every load-bearing part inside this bound, the gate, the persist boundary — and * a value import from tools/fs into checkpoint-store closes an ESM cycle. */ export declare const PROBE_REASON_MAX = 200; /** * Whether `text` is a probe's one-sentence account the engine itself could have minted: every mint passes the sentence through * {@link inlineUntrusted} at {@link PROBE_REASON_MAX}, so the longest one is the cap's content plus the truncation ellipsis * ({@link INLINE_TRUNCATION_MARK_CODEPOINTS}), counted in code points. A host that relays the sentence (`RiskDescriptor.probeReason`, * `AskRequest.probeReason`) and meets a longer one has met text the engine did not mint (a third-party producer, a generation * with another cap) — to be dropped whole, not cut, because a cut is another sentence. The bound lives here once; a host asks, it does not count. * The question is total: a value that is not a string is not a sentence the engine mints (every mint is a string), so it answers * `false` — the relay's answer to it, dropping it whole, is the same as to an over-long one. */ export declare function isMintableProbeReason(text: unknown): boolean; /** The one body bound every "reviewer note" relay passes to {@link delimitUntrusted} — the decider's * free text attached to a deny (sync `AskOutcome.reason` and the durable `ResumeOutcome` `reason` * legs alike). A note is steering, not payload: unbounded it can flood the transcript/context the * refusal lands in. 2048 code points matches the note cap the interactive card layer already * enforces on its own input, so a note that fit the card arrives whole. */ export declare const REVIEWER_NOTE_MAX_BODY = 2048; /** * Wrap untrusted text in a clearly labeled opaque fence. The consuming prompt should instruct the model to * treat everything inside as untrusted data — never as instructions. Sanitizes internally (authority-envelope * neutralization + fence-sentinel defusing, on the body AND the label), so callers may pass raw * worker/external text — and labels derived from external identifiers (hostnames, resource URIs). * * The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope * family (design/319 sibling work): a fence declares its contents to be data, but a forged * `<task-notification>` / `<user_memory>` / `<skills>` opening inside it was previously reproduced byte * for byte, so a model that skims past the fence line reads harness-shaped text with no judgment bit. * This lane rewrites bytes by contract (it is the CONTAINED half; the verbatim quote-back lanes stay * frozen and are served by the disclosure trailer instead), so the whole family is defused here. * * A caller that must DISCLOSE a `maxBody` truncation calls {@link delimitUntrustedWithClip} instead — * never a re-derived length test of its own (see that function and {@link neutralizeForFence}). */ export declare function delimitUntrusted(label: string, text: string, maxBody?: number): string; /** * The EXACT body neutralization {@link delimitUntrusted} applies, exported so a caller that has to * MEASURE the fenced body (its length in code points, whether the cap will bite) measures the string * the fence really carries instead of re-deriving the transform from its own copy of the tag list. * * Two copies of that list is precisely what drifted once the fenced lane widened past the reminder tag: * a caller pre-counting with the default (reminder-only) set under-measured the wide-set result by the * rewrite's few extra characters per widened-family tag occurrence, so a body got clipped while the truncation disclosure that * decision drove was withheld — an externally selectable window just under the cap. Idempotent (both * halves are), so a caller may hand the neutralized string straight back to the fence. */ export declare function neutralizeForFence(text: string): string; /** * {@link delimitUntrusted} plus the clip verdict, for the callers that must DISCLOSE a truncation on a * TRUSTED line outside the fence (§25 honesty: a bare ellipsis reads to the model as the source's own * text, so a silent clip is a lie about what the source said). * * `clipped` is reported by the same measurement that performed the cut. A caller re-deriving "would this * clip?" from its own transform + its own tag list is the shape that silently broke when the fence's tag * set widened; there is no second condition here to keep in sync. */ export declare function delimitUntrustedWithClip(label: string, text: string, maxBody?: number): { text: string; clipped: boolean; };