@sema-agent/core 5.54.0 → 5.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/send-message-tool.js +48 -2
  7. package/dist/agents/subagent.js +250 -89
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/auto-compaction.d.ts +17 -4
  11. package/dist/core/auto-compaction.js +3 -0
  12. package/dist/core/context-edit.d.ts +55 -6
  13. package/dist/core/context-edit.js +12 -1
  14. package/dist/core/governance-codes.js +14 -0
  15. package/dist/core/hooks.d.ts +293 -11
  16. package/dist/core/hooks.js +159 -12
  17. package/dist/core/human-input-projection.d.ts +20 -2
  18. package/dist/core/human-input-projection.js +9 -0
  19. package/dist/core/lsp-diagnostics.d.ts +19 -17
  20. package/dist/core/lsp-diagnostics.js +11 -5
  21. package/dist/core/mcp.d.ts +46 -0
  22. package/dist/core/mcp.js +132 -6
  23. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  24. package/dist/core/memory-engine/consolidation.js +342 -0
  25. package/dist/core/memory-engine/dual-root.js +3 -0
  26. package/dist/core/memory-engine/engine.d.ts +237 -4
  27. package/dist/core/memory-engine/engine.js +1111 -4
  28. package/dist/core/memory-engine/export-bundle.js +9 -0
  29. package/dist/core/memory-engine/file-backend.js +27 -1
  30. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  31. package/dist/core/memory-engine/frontmatter.js +111 -0
  32. package/dist/core/memory-engine/index.d.ts +4 -2
  33. package/dist/core/memory-engine/index.js +3 -1
  34. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  35. package/dist/core/memory-engine/sync-client.js +26 -0
  36. package/dist/core/memory-engine/tools.d.ts +9 -0
  37. package/dist/core/memory-engine/tools.js +57 -13
  38. package/dist/core/memory-engine/types.d.ts +99 -0
  39. package/dist/core/memory-recall.js +4 -3
  40. package/dist/core/memory.d.ts +33 -3
  41. package/dist/core/memory.js +6 -4
  42. package/dist/core/permission-rules.d.ts +30 -0
  43. package/dist/core/permission-rules.js +71 -8
  44. package/dist/core/reminder-disclosure.d.ts +29 -4
  45. package/dist/core/reminder-disclosure.js +60 -12
  46. package/dist/core/runner/prepare-memory.js +7 -2
  47. package/dist/core/runner/prepare-task.d.ts +39 -1
  48. package/dist/core/runner/prepare-task.js +63 -35
  49. package/dist/core/runner/runtask.d.ts +8 -1
  50. package/dist/core/runner/runtask.js +170 -31
  51. package/dist/core/runner/session-rule-policy.js +5 -3
  52. package/dist/core/runner/synthetic-tools.js +4 -2
  53. package/dist/core/runner/turn-attachments.d.ts +16 -6
  54. package/dist/core/runner/turn-attachments.js +34 -20
  55. package/dist/core/session-reconcile.d.ts +32 -0
  56. package/dist/core/session-reconcile.js +15 -0
  57. package/dist/core/task-notification.d.ts +34 -7
  58. package/dist/core/task-notification.js +11 -1
  59. package/dist/core/task-registry-agent.d.ts +20 -3
  60. package/dist/core/task-registry-agent.js +31 -2
  61. package/dist/core/tool-policy.d.ts +23 -0
  62. package/dist/core/tool-policy.js +29 -13
  63. package/dist/core/types.d.ts +126 -17
  64. package/dist/core/untrusted-egress.js +12 -2
  65. package/dist/core/untrusted-text.d.ts +189 -3
  66. package/dist/core/untrusted-text.js +424 -6
  67. package/dist/engine/compaction/compaction.d.ts +77 -7
  68. package/dist/engine/compaction/compaction.js +98 -9
  69. package/dist/engine/compaction/utils.d.ts +4 -0
  70. package/dist/engine/compaction/utils.js +6 -0
  71. package/dist/engine/harness/agent-harness.d.ts +84 -0
  72. package/dist/engine/harness/agent-harness.js +88 -12
  73. package/dist/engine/harness/messages.d.ts +4 -2
  74. package/dist/engine/harness/messages.js +7 -2
  75. package/dist/engine/harness/types.d.ts +11 -5
  76. package/dist/engine/loop/types.d.ts +14 -0
  77. package/dist/engine/session/import-validate.js +10 -0
  78. package/dist/engine/session/session.js +2 -2
  79. package/dist/index.d.ts +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/orchestration/run-spec.js +8 -1
  82. package/dist/prompts/default.d.ts +22 -6
  83. package/dist/tools/fs/index.d.ts +3 -1
  84. package/package.json +1 -1
  85. package/test/export-surface.snapshot.json +28 -1
@@ -657,8 +657,9 @@ export interface ToolExecuteContext {
657
657
  /**
658
658
  * Subagent transcript persistence — the RESOLVED delegation entry caps for this run
659
659
  * ({@link RunnerDeps.delegationEntryCaps} after prepare's loud validation; both members always
660
- * present). Runner-filled trusted seat, never a model argument — the Agent tool's background lane
661
- * reads it at its registration point. Undefined outside a Runner task (the lane then applies the
660
+ * present). Runner-filled trusted seat, never a model argument — the Agent tool's spawn gate reads
661
+ * it on EVERY local lane (background, background-fork, sync fork, sync spawn), each at its own
662
+ * launch point. Undefined outside a Runner task (the lane then applies the
662
663
  * exported defaults itself, so a directly-driven tool is bounded too).
663
664
  */
664
665
  delegationEntryCaps?: {
@@ -2543,9 +2544,12 @@ export interface TaskSpec {
2543
2544
  * Override auto-compaction thresholds for this task's session. Compaction (LLM summary, persisted
2544
2545
  * to the session) runs in two places sharing these settings: at every within-task turn boundary
2545
2546
  * (design/64 §25 (A) — the long-single-task case) and once at task end (the between-task case).
2546
- * Defaults: enabled, reserveTokens 16384, keepRecentTokens 0 (design/169-C, CC full-compaction
2547
- * form: the summary replaces the compacted region entirely by default set `keepRecentTokens`
2548
- * explicitly to keep a verbatim recent tail; 20000 was the pre-169 default) — repaired
2547
+ * Defaults: enabled, reserveTokens 16384, keepRecentTokens 0 (design/169-C; 20000 was the
2548
+ * pre-169 default). A zero budget is NOT "no verbatim tail": the cut-point search has a
2549
+ * structural floor under the budget, so the default already leaves the last exchange verbatim
2550
+ * after the summary (a trailing tool-result batch keeps its emitting assistant with it) — the
2551
+ * same one-group posture CC's reactive compaction ships. Set `keepRecentTokens` explicitly only
2552
+ * to keep a WIDER tail than that. Repaired
2549
2553
  * automatically when
2550
2554
  * pathological for the model's window: threshold ≤ 0, keepRecent ≥ threshold, or a threshold above
2551
2555
  * the clearStale point (0.7×window — the prefix-cache "death band", design/64 §26.4) is clamped
@@ -3342,12 +3346,27 @@ export interface TaskResult {
3342
3346
  }>;
3343
3347
  /** design/319 (B ticket, G9② observation seat) — reminder-disclosure trigger counts for the
3344
3348
  * leg, keyed `<outlet>.<form>`: outlets `read` / `notebook` / `pdf` / `mcp` / `webFetch` /
3345
- * `webSearch`; forms `bare` (bare-form trailer appended), `marked` (marked-form trailer —
3346
- * never throttled), `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
3347
- * `defused` (an MCP/web segment's exact-mark bytes were rewritten the lane's one sanctioned
3348
- * byte change, always paired with a `marked` disclosure). Present only when ≥1 key is
3349
- * non-zero. This is the D-4/D-6 re-ruling data (defuse/trailer widening to Read/Bash/Grep):
3350
- * a reading, never a gate. */
3349
+ * `webSearch`; forms
3350
+ * · `bare` (bare-form trailer appended reminder-shaped text without this session's mark),
3351
+ * · `marked` (marked-form trailernever throttled),
3352
+ * · `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
3353
+ * · `defused` (an MCP/web segment's exact-mark bytes were rewritten the lane's one sanctioned
3354
+ * byte change, always paired with a `marked` disclosure),
3355
+ * · `envelope` (text shaped like one of the engine's OTHER authority envelopes — the DISCLOSED
3356
+ * subset is `task-notification` / `new-diagnostics` / `user_memory` / `skills`; `scope` is
3357
+ * fenced but not disclosed, since `<scope>…</scope>` is also an ordinary build-file element.
3358
+ * That family carries no mark, so its sentence is positional rather than byte-testable. It
3359
+ * rides ON the reminder copy when both families hit, so `envelope` can be bumped alongside
3360
+ * `marked`/`bare` on one result),
3361
+ * · `envelope_throttled` (an envelope sentence suppressed by its OWN 60s window — the two
3362
+ * families throttle independently, so one may emit while the other reports suppression),
3363
+ * · `mark_echo` (observation ONLY, never a disclosure: the session's mark VALUE appeared in
3364
+ * external bytes with no reminder-shaped tag around it, so the tag-grammar detector cannot
3365
+ * reach it. Nothing is rewritten and no trailer is appended; it is an upper bound on model
3366
+ * exposure for the lanes that do not defuse).
3367
+ * Present only when ≥1 key is non-zero. The map is OPEN by type — fold by key rather than
3368
+ * switching on a closed set. This is the D-4/D-6 re-ruling data (defuse/trailer widening to
3369
+ * Read/Bash/Grep): a reading, never a gate. */
3351
3370
  reminderDisclosures?: Record<string, number>;
3352
3371
  };
3353
3372
  /**
@@ -3667,6 +3686,36 @@ export type TaskEvent = ({
3667
3686
  * carry no string discriminator under either name.
3668
3687
  */
3669
3688
  errorCode?: string;
3689
+ /**
3690
+ * WHICH call the gate was holding, when this frame is a park-contamination frame — the tool call
3691
+ * id of the gated call. Three conditions, ALL required: `errorCode === "gate.parked"`, the run is
3692
+ * tearing down behind a committed park, and that park binds a tool call at all.
3693
+ *
3694
+ * **WHERE IT COMES FROM — never lifted from the result**, and deliberately unlike its neighbour
3695
+ * {@link errorCode}: that one IS the result's own `details.code`, i.e. tool-authored, because a
3696
+ * tool classifying its own failure is bounded self-description. This field is an assertion about a
3697
+ * DIFFERENT call, so a tool able to author it could point an approval UI at a call nobody is
3698
+ * holding. It is therefore engine-minted — read from the run's committed park holder and handed to
3699
+ * the frame projection as a parameter, the same posture (and the same reason) as {@link settledBy}.
3700
+ * `errorCode` only participates in selecting WHETHER the id is placed; it never supplies the
3701
+ * value, and on its own it is not enough — the run must also be aborting behind a real park, which
3702
+ * is why a tool declaring `gate.parked` on its own failure cannot conjure this field.
3703
+ *
3704
+ * Why it is on the frame: a durable gate parks ONE call and the loop then short-circuits every
3705
+ * sibling in the batch with an identical "Operation aborted" body, so `gate.parked` alone says
3706
+ * "something parked this batch" without saying WHAT. A consumer wanting to render "waiting on
3707
+ * <the gated call>" beside the collateral frames otherwise has to re-derive the pair from batch
3708
+ * adjacency and timing — an inference that is wrong exactly when it matters (a batch with more
3709
+ * than one ask candidate, a reordered stream).
3710
+ *
3711
+ * ONLY EVER STRUCTURALLY PROVEN, never guessed: the id is read off the committed checkpoint's
3712
+ * pending tool call, so it is the same value the checkpoint parks on and the same value the
3713
+ * decide/resume lane answers about. It is therefore ABSENT — not approximated — for the park
3714
+ * kinds that hold no call (a resource-limit slice, a plan review), and absent for every abort
3715
+ * that is not a gate park at all (user interrupt, timeout, walltime): those frames are
3716
+ * byte-unchanged. Absence means "core cannot prove a causal call", never "there wasn't one".
3717
+ */
3718
+ gatedCallId?: string;
3670
3719
  /**
3671
3720
  * WHAT ENDED THE APPROVAL this call was waiting on, when this frame closes a gated call — `"human"`
3672
3721
  * / `"timeout"` / `"aborted"`, the vocabulary of
@@ -4817,6 +4866,24 @@ export interface EngineNotice {
4817
4866
  * #257 contract's "accepted = enqueued, not consumed" sentence;
4818
4867
  * `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
4819
4868
  * (the terminal sweep is a single site).
4869
+ * **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
4870
+ * to empty both queues before agent_end could count them, so the one loss path an operator most
4871
+ * needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
4872
+ * settled at the run tail instead of at agent_end, because the tail first carries the still-
4873
+ * undelivered input onto the committed checkpoint's parked-steer queue (the resume delivers it)
4874
+ * — what is announced is the remainder that could NOT be carried, so a fully-migrated park says
4875
+ * nothing at all and a queue-full / no-longer-pending row still says exactly what was lost.
4876
+ *
4877
+ * - `"task.injection_priority_unimplemented"` (#389 伴生, D-3) — a notification was injected with
4878
+ * `priority: "now"`. The three values are CC's names (`now`/`next`/`later`) but this engine
4879
+ * delivers all three identically at the next turn boundary: there is no arm that aborts the
4880
+ * running turn, so a caller writing `"now"` and expecting an interruption gets a plain queued
4881
+ * delivery. The frame IS delivered (this is disclosure, not a refusal — dropping an accepted
4882
+ * notification would be the worse error); what is announced is that the knob's promise is not
4883
+ * honored, so an operator can stop building on it. Once per run (the injection funnel is a
4884
+ * single site, and a busy lane must not narrate the same gap once per frame);
4885
+ * `detail: { priority, taskId? }`. An UNKNOWN priority value is a different fact with a
4886
+ * different posture — `TaskStream.notify` refuses it typed (`notify.invalid_payload`).
4820
4887
  *
4821
4888
  * - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
4822
4889
  * this session's memory crossed into the one-way externally-exposed state (a tool classified
@@ -4889,6 +4956,27 @@ export interface EngineNotice {
4889
4956
  * tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates);
4890
4957
  * `detail: { handle }`.
4891
4958
  *
4959
+ * - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
4960
+ * session count crossed the consolidation thresholds (time gate open ∧ enough distinct
4961
+ * sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
4962
+ * the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
4963
+ * automatically; `detail: { scope, sessionsSince, sessionId? }`.
4964
+ * - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
4965
+ * `completed`: products landed, superseded targets left the default read face (retained as
4966
+ * evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
4967
+ * counts and engine-minted ids only, zero content.
4968
+ * - `"memory.consolidation_conflict"` (design/339 §6.2) — a plan parked CONFLICT: at least one
4969
+ * target did not reach its planned state (a concurrent write, a patch the store declined, or a
4970
+ * target the re-judgment could no longer act on — the plan's audit rows carry the per-target
4971
+ * reason). The REFUSED targets were not overwritten; patches the plan had already applied
4972
+ * stand (discard rolls nothing back — the receipt's applied-vs-conflict counts say how many).
4973
+ * Affected intents stay pending and wait for the host valve
4974
+ * (`resolveConsolidationPlan` retry/discard); `detail: { planId, scope }`.
4975
+ * - `"memory.consolidation_refused"` (design/339 §6.2) — the notice dialect of a consolidation
4976
+ * verb's coded structured refusal (the verb itself throws
4977
+ * {@link import("../core/memory-engine/consolidation.js").ConsolidationRefusedError});
4978
+ * `detail: { refusalCode, scope? }`.
4979
+ *
4892
4980
  * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
4893
4981
  * transient network failure being retried). Those are per-attempt liveness frames with their own
4894
4982
  * frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
@@ -5110,6 +5198,23 @@ export interface RunnerDeps {
5110
5198
  * never truthiness).
5111
5199
  */
5112
5200
  memoryProvenance?: "off" | "carry";
5201
+ /**
5202
+ * design/339 §6.1 — the v3 memory-consolidation switch, threaded verbatim to the engine seat
5203
+ * ({@link import("../core/memory-engine/engine.js").MemoryEngineOptions.consolidation} — same
5204
+ * transport as {@link memoryProvenance}). ABSENT = OFF (the shipped default): no gate state is
5205
+ * ever written, no recommendation is ever minted, and the consolidation verbs on an engine the
5206
+ * host constructs over the same store refuse coded. PRESENT = enabled: the engine counts
5207
+ * distinct terminal-harvest sessions per scope and surfaces the advisory
5208
+ * `memory.consolidation_recommended` notice when the configured thresholds cross — EXECUTION
5209
+ * stays host-owned (the four verbs are engine host API; nothing in a task can trigger a run).
5210
+ * `provenance: "off"` beside this refuses loudly at prepare
5211
+ * (`config.memory_consolidation_provenance_off` — the fold law must be able to mint);
5212
+ * `multiNode: true` without a lease refuses (`config.memory_consolidation_lease_required`);
5213
+ * every other bad value refuses under `config.memory_consolidation` (#123). DEPLOYMENT seat
5214
+ * ONLY (no TaskSpec twin, not in the governed workflow whitelist), same law as
5215
+ * {@link memoryProvenance}.
5216
+ */
5217
+ memoryConsolidation?: import("../core/memory-engine/consolidation.js").MemoryConsolidationOptions;
5113
5218
  /**
5114
5219
  * design/199 件A — the DEPLOYMENT's read-face declaration
5115
5220
  * ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
@@ -5415,12 +5520,16 @@ export interface RunnerDeps {
5415
5520
  * Subagent transcript persistence — the delegation ENTRY caps (CC parity values: 20 concurrent /
5416
5521
  * 200 cumulative per session tree; defaults exported as `DELEGATION_MAX_CONCURRENT_DEFAULT` /
5417
5522
  * `DELEGATION_MAX_PER_SESSION_DEFAULT`). Key = `(scope, rootSessionId)`, full depth (grandchildren
5418
- * share the tree's pool). `maxConcurrent` bounds running/pending a* handles in this process's
5419
- * registry (parked does not burn a slot a suspension is not concurrency; a revival claim counts
5420
- * like a spawn); `maxCumulativePerSession` bounds the RETAINED-WINDOW cumulative count (registry-
5421
- * retained + store-retained rows — a reaped row returns its quota; deliberately NOT CC's lifetime-
5422
- * monotonic session counter, which would require a persistent counting surface this economic bound
5423
- * does not justify registered divergence). Refusals are coded (`delegation.concurrency_cap` /
5523
+ * share the tree's pool). `maxConcurrent` bounds the tree's RUNNING children of every local lane:
5524
+ * running/pending a* handles in this process's registry PLUS the synchronous and forked children in
5525
+ * flight in this process (parked does not burn a slot a suspension is not concurrency; a revival
5526
+ * claim counts like a spawn); `maxCumulativePerSession` bounds the RETAINED-WINDOW cumulative count
5527
+ * (registry-retained + store-retained rows, plus those same in-flight local children a reaped row
5528
+ * returns its quota, and a synchronous child retains nothing so it leaves the window when it
5529
+ * settles; deliberately NOT CC's lifetime-monotonic session counter, which would require a
5530
+ * persistent counting surface this economic bound does not justify — registered divergence). One
5531
+ * consequence, stated rather than left to be discovered: a tree that only ever spawns synchronously
5532
+ * is bounded by the concurrency cap alone. Refusals are coded (`delegation.concurrency_cap` /
5424
5533
  * `delegation.session_cap`) with the current value and this knob's name in the text.
5425
5534
  *
5426
5535
  * BAD VALUES REFUSE LOUDLY at prepare (`config.delegation_entry_caps`, the #123 posture): a
@@ -1,4 +1,4 @@
1
- import { delimitUntrusted } from "./untrusted-text.js";
1
+ import { delimitUntrusted, neutralizeForFence } from "./untrusted-text.js";
2
2
  import { scrubSecrets, SECRET_PASSES, runRedactionPasses } from "./arg-summary.js";
3
3
  import { sliceHeadSafe } from "./surrogate-safe-slice.js";
4
4
  export { summarizeRedactions } from "./arg-summary.js";
@@ -98,5 +98,15 @@ export function boundedRedactedSummary(value, max) {
98
98
  return boundedString(value, max, redactSecrets);
99
99
  }
100
100
  export function untrustedEgressForHuman(value, opts) {
101
- return delimitUntrusted(opts.label, boundedString(value, opts.max, redactHostLeaks), opts.max);
101
+ const source = boundedString(value, Number.POSITIVE_INFINITY, redactHostLeaks);
102
+ let budget = opts.max;
103
+ let bounded = boundedString(source, budget, (s) => s);
104
+ for (let i = 0; i < 4 && budget > 0; i++) {
105
+ const overflow = [...neutralizeForFence(bounded)].length - opts.max;
106
+ if (overflow <= 0)
107
+ break;
108
+ budget = Math.max(0, budget - overflow);
109
+ bounded = boundedString(source, budget, (s) => s);
110
+ }
111
+ return delimitUntrusted(opts.label, bounded, opts.max);
102
112
  }
@@ -16,6 +16,144 @@
16
16
  * own (instructions and data share one channel). The real boundary is decorrelation + reading the objective
17
17
  * artifact (the diff / working tree) rather than the worker's self-report (design/53 §3, design/54 §3).
18
18
  */
19
+ /**
20
+ * One row of {@link ENGINE_ENVELOPES}.
21
+ *
22
+ * WHY A TABLE EXISTS. design/319 spent its whole threat model on a single tag and three independent
23
+ * texts stated the supporting premise as "this harness mints no other tag" — a MODULE-level
24
+ * enumeration (`turn-attachments` really does mint only one) promoted to a HARNESS-level claim. The
25
+ * engine in fact authors several model-facing envelopes, guarded by two different escape families
26
+ * living in two different modules, and the two envelopes added most recently (`skills`, `scope`)
27
+ * reached the model with NO guard at all — not because anyone argued they were safe, but because
28
+ * nothing in the tree listed the set an author was joining. A guard family without a census is a
29
+ * guard family that silently loses members; this table is that census, and the accompanying
30
+ * literal-tag test keeps it honest (a new closing-tag literal in `src/` fails until it is classified
31
+ * here).
32
+ *
33
+ * The same discipline as the `ToolSpec` completeness table: the registry is the declaration, the
34
+ * test is the gate, and a NEW envelope is a two-line edit rather than an invisible omission.
35
+ */
36
+ export interface EngineEnvelope {
37
+ /** The tag NAME as the engine spells it, without angle brackets. */
38
+ readonly tag: string;
39
+ /**
40
+ * - `authority` — the engine authors it as model-facing HARNESS speech; seeing the tag is what
41
+ * makes the model read the content as system information, so a forgery is an authority claim.
42
+ * - `framing` — the engine wraps UNTRUSTED content in it as a labeled data frame; a forgery
43
+ * escapes the frame (or closes it early) rather than claiming harness authority.
44
+ * - `not-an-envelope` — a tag-shaped literal in `src/` that is not an engine-minted model-facing
45
+ * envelope at all (a MODEL-output grammar the engine parses, a prompt-assembly placeholder,
46
+ * rendering metadata, or copy that merely TALKS about an envelope). Listed so the census test
47
+ * has a home for every literal in the tree and no reader has to re-derive the classification.
48
+ */
49
+ readonly kind: "authority" | "framing" | "not-an-envelope";
50
+ /** Where the engine authors (or, for `not-an-envelope`, spells) it. */
51
+ readonly mint: string;
52
+ /** How untrusted bytes reaching this envelope's own body/attributes are defused at that site. */
53
+ readonly guard: string;
54
+ /**
55
+ * True ⇒ neutralized inside every FENCED/inline untrusted projection ({@link delimitUntrusted},
56
+ * {@link inlineUntrusted}). Those lanes already rewrite bytes by contract, so a forged authority
57
+ * envelope arriving through them is defused rather than merely fenced.
58
+ */
59
+ readonly fenced: boolean;
60
+ /**
61
+ * True ⇒ DISCLOSED (never rewritten) when it appears in VERBATIM external data — the design/319
62
+ * trailer pipeline (reminder-disclosure.ts). Verbatim lanes are byte-frozen by the G3 veto (the
63
+ * Read↔Edit `old_string` quote-back loop), so the only available judgment bit is an appended,
64
+ * engine-minted note. Deliberately NOT set for tags whose spelling is common in ordinary data
65
+ * (`scope` is a Maven POM element; `summary` is HTML) — a trailer that fires on every build file
66
+ * teaches the model to ignore trailers.
67
+ */
68
+ readonly disclosed: boolean;
69
+ }
70
+ /**
71
+ * The census. Rows are grouped by {@link EngineEnvelope.kind}; within a group, by module.
72
+ *
73
+ * KNOWN CEILING (stated, not hidden): the accompanying test enumerates CLOSING-TAG LITERALS in
74
+ * `src/`, so an envelope whose tag name is INTERPOLATED (`<${tag}>`) is invisible to it. Those are
75
+ * enumerated here by hand — `teammate-message`, the observer digest's four event tags, and the
76
+ * dynamic `<{slug}-activity>` wrapper — and each already carries its own escape at its mint site.
77
+ */
78
+ export declare const ENGINE_ENVELOPES: readonly EngineEnvelope[];
79
+ /** The registry row for a tag name, or `undefined` when the tag is not in the census. */
80
+ export declare function engineEnvelope(tag: string): EngineEnvelope | undefined;
81
+ /** Registry-derived: the envelopes that read as HARNESS AUTHORITY on sight. */
82
+ export declare const ENGINE_AUTHORITY_ENVELOPE_TAGS: readonly string[];
83
+ /**
84
+ * Registry-derived: the tags {@link delimitUntrusted} / {@link inlineUntrusted} neutralize on top of
85
+ * the always-on reminder tag. These lanes carry EXTERNAL/worker bytes into a labeled data frame and
86
+ * already rewrite them by contract, so an authority envelope forged inside them is defused rather
87
+ * than merely fenced — the "forgery can be neutralized, never trusted" direction.
88
+ *
89
+ * Deliberately NOT applied inside {@link sanitizeUntrustedText} itself: that primitive is also used
90
+ * by mint sites over an ALREADY-ASSEMBLED body (the boundary/first-frame reminder wrap runs it over
91
+ * a body that may legitimately contain the engine's own `<skills>` block), so widening the primitive
92
+ * would have the engine defuse its OWN envelopes. Containment belongs to the fence, not the
93
+ * primitive.
94
+ */
95
+ export declare const FENCED_LANE_ENVELOPE_TAGS: readonly string[];
96
+ /**
97
+ * The same family as {@link FENCED_LANE_ENVELOPE_TAGS}, as a MUTABLE array for every body that will be
98
+ * SHELLED in a marked `<system-reminder>` — the attachment sink, the listing/diagnostics producers,
99
+ * the durable-resume orphan notice, the post-tool-batch hook relay, the git frame.
100
+ *
101
+ * These are not fences. They render deployment/server/model-supplied strings into a body the run loop
102
+ * then wraps in engine authority, so a forged envelope inside one is laundered by the wrapper. The
103
+ * wrapper itself cannot blanket-neutralize the family — it also shells the one body that legitimately
104
+ * IS an envelope (`buildSkillsBlock`'s `<skills>` fence) — so containment is expressed as OWNERSHIP:
105
+ * this full set for every body that owns nothing, minus its own tag for the one that does (see
106
+ * `attachmentEnvelopeTags` in turn-attachments.ts).
107
+ *
108
+ * Allocated ONCE so the sanitizer's memoized break-out regex is keyed by a stable value.
109
+ */
110
+ export declare const SHELLED_BODY_ENVELOPE_TAGS: string[];
111
+ /**
112
+ * Registry-derived: the tags the design/319 disclosure pipeline reports when they appear in VERBATIM
113
+ * external data (see {@link scanEnvelopeShaped}). `system-reminder` is absent on purpose — the mark
114
+ * pipeline scans it with a stronger (marked vs bare) verdict of its own.
115
+ */
116
+ export declare const DISCLOSED_ENVELOPE_TAGS: readonly string[];
117
+ /** The result of one envelope-shaped scan of an external projection. */
118
+ export interface EnvelopeShapedScan {
119
+ /** ≥1 disclosed-family envelope tag (open or close) is present. */
120
+ hit: boolean;
121
+ /** The REGISTRY spellings that matched, de-duplicated, in registry order. Registry-owned strings —
122
+ * never the matched bytes — so a caller may render them into trusted copy safely. */
123
+ tags: string[];
124
+ }
125
+ /**
126
+ * The envelope-family sibling of {@link scanReminderShaped}: detect text shaped like one of the
127
+ * engine's OTHER authority envelopes inside external data. Read-only — verbatim lanes are byte
128
+ * frozen (design/319 G3 veto: the Read↔Edit `old_string` quote-back loop), so the disclosure
129
+ * pipeline's only move is to append an engine-minted note saying what was seen.
130
+ *
131
+ * Grammar is deliberately the same shape as the reminder scan (attribute / case / tag-internal
132
+ * whitespace tolerant, open OR close tag), and the tag SET comes from the registry rather than a
133
+ * hand-copied list, so adding an envelope to {@link ENGINE_ENVELOPES} with `disclosed: true` is the
134
+ * whole change. Same accepted residual as the reminder scan: a tag the grammar does not recognize
135
+ * (fragmented, Unicode-lookalike) is not a hit — defense in depth, not a guarantee.
136
+ */
137
+ export declare function scanEnvelopeShaped(text: string): EnvelopeShapedScan;
138
+ /**
139
+ * Defuse a caller-declared occurrence of ONE specific envelope tag (opening AND closing) inside
140
+ * content, CC `zZe` @8339092 verbatim: ``t.replace(new RegExp(`<(?=/?${e}(?:[>\\s/]|$))`, "gi"), "<\\")``.
141
+ *
142
+ * Homed here (beside {@link ENGINE_ENVELOPES}) rather than in the observer module that first needed
143
+ * it: "escape one named envelope tag" is a containment primitive, and the registry's guard column
144
+ * points at it. Re-exported from `agents/observer.ts` for its existing importers.
145
+ *
146
+ * Deviation from CC (hardening): the tag is regex-escaped before entering the RegExp — CC
147
+ * interpolates raw, safe only because its slugs are `[a-zA-Z0-9_-]`; ours are too, but we don't rely
148
+ * on the caller for that invariant.
149
+ *
150
+ * NOTE the two sanctioned escape SHAPES in this tree, and why they are not merged: this one inserts
151
+ * a backslash (`<` → `<\`) because it is a byte-level CC anchor on the observer/teammate lanes;
152
+ * {@link sanitizeUntrustedText} inserts a zero-width space because its lanes are read by humans as
153
+ * well. Both are idempotent and both break the tag; a merge would move bytes on one of the two
154
+ * families for no security gain.
155
+ */
156
+ export declare function escapeEnvelopeTag(tag: string, text: string): string;
19
157
  /** design/319 (B ticket) — what one reminder-shaped scan of an external projection found. */
20
158
  export interface ReminderShapedScan {
21
159
  /** ≥1 reminder-shaped tag (open or close; attribute/case/whitespace tolerant) is present. */
@@ -87,6 +225,14 @@ export declare function defuseExactMarkInSegments(segments: readonly string[], m
87
225
  * `<discussion>`, so an untrusted member could emit `</statement>` to break out (search [46] BUG2);
88
226
  * the caller passes those tag names. Tag names must be literal (alphanumeric/hyphen) — they are code-supplied
89
227
  * wrapper names, never untrusted input. Idempotent for prompt assembly (a defused tag no longer matches).
228
+ *
229
+ * WHY THE ALWAYS-ON SET IS ONE TAG AND NOT THE WHOLE {@link ENGINE_ENVELOPES} AUTHORITY FAMILY: this is a
230
+ * PRIMITIVE, and mint sites also run it over an ALREADY-ASSEMBLED body — the boundary/first-frame reminder
231
+ * wrap sanitizes a body that may legitimately BE the engine's own `<skills>` block, and the memory write
232
+ * scan diffs against it to decide what markup a note may contain. Widening the primitive would have the
233
+ * engine defuse its own envelopes and would silently move an unrelated store's admission rule. Family-wide
234
+ * containment therefore lives at the FENCE ({@link delimitUntrusted} / {@link inlineUntrusted}, via
235
+ * {@link FENCED_LANE_ENVELOPE_TAGS}) and at each envelope's own mint site (the registry's guard column).
90
236
  */
91
237
  export declare function sanitizeUntrustedText(text: string, extraTags?: string[]): string;
92
238
  /**
@@ -237,8 +383,13 @@ export declare function defuseControlChars(text: string): string;
237
383
  * `\s` does NOT fully match: the C1 half carries the 8-bit CSI/OSC/ST forms (U+009B/U+009D/U+009C) a
238
384
  * C1-honoring terminal treats like their ESC-prefixed spellings, so leaving them through would let a
239
385
  * 'sanitized' value repaint the trusted line it is interpolated onto (adversarial round finding) — caps the
240
- * length, then applies the same tag-neutralization (`</system-reminder>`) + fence-sentinel (`<<<`/`>>>`)
241
- * defusing the fenced body gets.
386
+ * length, then applies the same tag-neutralization + fence-sentinel (`<<<`/`>>>`) defusing the fenced
387
+ * body gets.
388
+ *
389
+ * The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope
390
+ * family, not the reminder tag alone. A value interpolated onto a TRUSTED line is the cheapest place to
391
+ * open a forged envelope, and this lane rewrites bytes by contract already, so the whole family is
392
+ * defused here.
242
393
  * Defense-in-depth, NOT a guarantee (same posture as the rest of this module).
243
394
  */
244
395
  export declare function inlineUntrusted(text: string, maxLen?: number): string;
@@ -250,8 +401,43 @@ export declare function inlineUntrusted(text: string, maxLen?: number): string;
250
401
  export declare const REVIEWER_NOTE_MAX_BODY = 2048;
251
402
  /**
252
403
  * Wrap untrusted text in a clearly labeled opaque fence. The consuming prompt should instruct the model to
253
- * treat everything inside as untrusted data — never as instructions. Sanitizes internally (system-reminder
404
+ * treat everything inside as untrusted data — never as instructions. Sanitizes internally (authority-envelope
254
405
  * neutralization + fence-sentinel defusing, on the body AND the label), so callers may pass raw
255
406
  * worker/external text — and labels derived from external identifiers (hostnames, resource URIs).
407
+ *
408
+ * The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope
409
+ * family (design/319 sibling work): a fence declares its contents to be data, but a forged
410
+ * `<task-notification>` / `<user_memory>` / `<skills>` opening inside it was previously reproduced byte
411
+ * for byte, so a model that skims past the fence line reads harness-shaped text with no judgment bit.
412
+ * This lane rewrites bytes by contract (it is the CONTAINED half; the verbatim quote-back lanes stay
413
+ * frozen and are served by the disclosure trailer instead), so the whole family is defused here.
414
+ *
415
+ * A caller that must DISCLOSE a `maxBody` truncation calls {@link delimitUntrustedWithClip} instead —
416
+ * never a re-derived length test of its own (see that function and {@link neutralizeForFence}).
256
417
  */
257
418
  export declare function delimitUntrusted(label: string, text: string, maxBody?: number): string;
419
+ /**
420
+ * The EXACT body neutralization {@link delimitUntrusted} applies, exported so a caller that has to
421
+ * MEASURE the fenced body (its length in code points, whether the cap will bite) measures the string
422
+ * the fence really carries instead of re-deriving the transform from its own copy of the tag list.
423
+ *
424
+ * Two copies of that list is precisely what drifted once the fenced lane widened past the reminder tag:
425
+ * a caller pre-counting with the default (reminder-only) set under-measured the wide-set result by one
426
+ * ZWSP per widened-family tag occurrence, so a body got clipped while the truncation disclosure that
427
+ * decision drove was withheld — an attacker-selectable window just under the cap. Idempotent (both
428
+ * halves are), so a caller may hand the neutralized string straight back to the fence.
429
+ */
430
+ export declare function neutralizeForFence(text: string): string;
431
+ /**
432
+ * {@link delimitUntrusted} plus the clip verdict, for the callers that must DISCLOSE a truncation on a
433
+ * TRUSTED line outside the fence (§25 honesty: a bare ellipsis reads to the model as the source's own
434
+ * text, so a silent clip is a lie about what the source said).
435
+ *
436
+ * `clipped` is reported by the same measurement that performed the cut. A caller re-deriving "would this
437
+ * clip?" from its own transform + its own tag list is the shape that silently broke when the fence's tag
438
+ * set widened; there is no second condition here to keep in sync.
439
+ */
440
+ export declare function delimitUntrustedWithClip(label: string, text: string, maxBody?: number): {
441
+ text: string;
442
+ clipped: boolean;
443
+ };