theorum 1.0.0 → 1.1.3

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 (204) hide show
  1. package/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. package/esm/src/interface/types.js +0 -13
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Tool boundary guardrails — the surface where untrusted bytes re-enter the
3
+ * model's context carrying the model's own authority.
4
+ *
5
+ * A tool result is not user text: the model asked for it, so it arrives looking
6
+ * like something the turn already trusts. Remote HTTP and MCP servers control
7
+ * their own response bodies (including their error strings), and a delegated
8
+ * agent answers in prose that reads as authoritative. Everything crossing this
9
+ * boundary is therefore fenced, detected, and labelled with where it came from.
10
+ *
11
+ * @module
12
+ */
13
+ import type { AdvisoryLevel, GuardrailEvent, GuardrailHit, Provenance, ResolvedGuardrailPolicy, ToolOrigin, TurnTaint, Verdict } from './types.js';
14
+ declare const TOOL_CLOSE = "</tool_data>";
15
+ /** True when a result's bytes came from outside the host's own code. */
16
+ declare function isRemoteOrigin(origin: ToolOrigin): boolean;
17
+ /**
18
+ * Wrap tool output so the model reads it as data and can see where it came from.
19
+ *
20
+ * The origin is on the tag rather than in prose so a result cannot claim a
21
+ * friendlier provenance than it has by writing one into its own body.
22
+ */
23
+ declare function wrapToolData(text: string, provenance: Provenance, advisory?: AdvisoryLevel, guidance?: string): string;
24
+ export interface GuardedToolText {
25
+ /** Text to hand the model, fenced and redacted. */
26
+ text: string;
27
+ /** Emitted when the guard did anything worth recording. */
28
+ event?: GuardrailEvent;
29
+ /**
30
+ * Directive signals found in the content.
31
+ *
32
+ * Reported, never redacted: legitimate tool output is frequently
33
+ * instruction-shaped, so rewriting on this signal would corrupt real data.
34
+ * These raise the turn's taint instead.
35
+ */
36
+ suspicious?: GuardrailHit[];
37
+ }
38
+ /**
39
+ * Compose the model-facing text for a tool result.
40
+ *
41
+ * `finding` is the tool's summary and `data` its structured payload; both reach
42
+ * the model, so both are guarded together rather than only the prose half.
43
+ */
44
+ declare function composeToolText(finding: string, data: unknown): string;
45
+ /**
46
+ * Guard one tool result on its way into the model's context.
47
+ *
48
+ * Local host tools are still detected — a host tool reading a database returns
49
+ * data the host did not write — but only remote origins are fenced, because
50
+ * fencing a local tool's output would change prompts hosts have already tuned.
51
+ */
52
+ declare function guardToolResult(finding: string, data: unknown, provenance: Provenance, policy: ResolvedGuardrailPolicy, callableTools?: readonly string[]): GuardedToolText;
53
+ /**
54
+ * Guard a tool failure message.
55
+ *
56
+ * A remote server authors its own error strings, so an unguarded failure message
57
+ * is the cleanest injection path across this boundary: it reaches the model
58
+ * verbatim and is framed by the kernel as a system report.
59
+ */
60
+ declare function guardToolFailureText(message: string, provenance: Provenance, policy: ResolvedGuardrailPolicy): GuardedToolText;
61
+ /**
62
+ * Inspect model-supplied tool arguments before the call runs.
63
+ *
64
+ * Arguments are model-authored, so the risk is not instruction smuggling but
65
+ * exfiltration: a credential lifted from context and posted outward as a
66
+ * parameter. Detection reports rather than rewrites — silently altering a tool
67
+ * argument would make the call succeed against something the model did not ask
68
+ * for.
69
+ */
70
+ declare function inspectToolArguments(args: unknown, policy: ResolvedGuardrailPolicy): Verdict;
71
+ /** Guardrail event for a flagged tool call, for the runner to emit. */
72
+ declare function toolCallEvent(verdict: Verdict, provenance: Provenance): GuardrailEvent | undefined;
73
+ /**
74
+ * Record a tool result against the turn's taint.
75
+ *
76
+ * Only remote origins taint: a local host tool returns bytes the host's own code
77
+ * produced, and treating those as attacker-influenceable would make the gate
78
+ * useless in practice.
79
+ */
80
+ declare function recordTaint(taint: TurnTaint | undefined, provenance: Provenance, suspicious?: GuardrailHit[]): TurnTaint;
81
+ /** True when the turn has already read attacker-influenceable content. */
82
+ declare function isTainted(taint: TurnTaint | undefined): boolean;
83
+ /** True when remote content this turn read looked like it was steering the agent. */
84
+ declare function isSuspicious(taint: TurnTaint | undefined): boolean;
85
+ /**
86
+ * Decide whether a tool call may proceed given what the turn has already read.
87
+ *
88
+ * A `flag` says the call is happening on a tainted turn and is worth recording; a
89
+ * `block` says the profile asked for it to be refused. Reporting happens whether
90
+ * or not enforcement is configured, so the risk is visible before a host opts in.
91
+ */
92
+ declare function checkTaintGate(taint: TurnTaint | undefined, access: string, policy: ResolvedGuardrailPolicy): Verdict;
93
+ export { checkTaintGate, composeToolText, guardToolFailureText, guardToolResult, inspectToolArguments, isRemoteOrigin, isSuspicious, isTainted, recordTaint, TOOL_CLOSE, toolCallEvent, wrapToolData, };
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Tool boundary guardrails — the surface where untrusted bytes re-enter the
3
+ * model's context carrying the model's own authority.
4
+ *
5
+ * A tool result is not user text: the model asked for it, so it arrives looking
6
+ * like something the turn already trusts. Remote HTTP and MCP servers control
7
+ * their own response bodies (including their error strings), and a delegated
8
+ * agent answers in prose that reads as authoritative. Everything crossing this
9
+ * boundary is therefore fenced, detected, and labelled with where it came from.
10
+ *
11
+ * @module
12
+ */
13
+ import { lexiconText } from './lexicon.js';
14
+ import { detectionForTrust } from './policy.js';
15
+ import { sanitizeText } from './sanitize.js';
16
+ import { textForScan } from './serialize.js';
17
+ import { advisoryLevel, directiveHits } from './tool-directives.js';
18
+ const TOOL_CLOSE = '</tool_data>';
19
+ const TOOL_OPEN = '<tool_data';
20
+ /** Origins whose bytes the host does not author and cannot vouch for. */
21
+ const REMOTE_ORIGINS = new Set(['http', 'mcp', 'delegated']);
22
+ /** True when a result's bytes came from outside the host's own code. */
23
+ function isRemoteOrigin(origin) {
24
+ return REMOTE_ORIGINS.has(origin);
25
+ }
26
+ /**
27
+ * Strip fence markers a tool result tried to forge before wrapping it.
28
+ * Linear scan — avoids polynomial regex on forged `<tool_data…>` runs.
29
+ */
30
+ function stripToolFences(text) {
31
+ const lower = text.toLowerCase();
32
+ let out = '';
33
+ let i = 0;
34
+ while (i < text.length) {
35
+ const openAt = lower.indexOf(TOOL_OPEN, i);
36
+ const closeAt = lower.indexOf(TOOL_CLOSE, i);
37
+ let next = -1;
38
+ let kind = null;
39
+ if (openAt >= 0 && (closeAt < 0 || openAt <= closeAt)) {
40
+ next = openAt;
41
+ kind = 'open';
42
+ }
43
+ else if (closeAt >= 0) {
44
+ next = closeAt;
45
+ kind = 'close';
46
+ }
47
+ if (next < 0 || kind === null) {
48
+ out += text.slice(i);
49
+ break;
50
+ }
51
+ out += text.slice(i, next);
52
+ if (kind === 'close') {
53
+ i = next + TOOL_CLOSE.length;
54
+ continue;
55
+ }
56
+ const gt = text.indexOf('>', next + TOOL_OPEN.length);
57
+ if (gt < 0) {
58
+ out += text.slice(next);
59
+ break;
60
+ }
61
+ i = gt + 1;
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Wrap tool output so the model reads it as data and can see where it came from.
67
+ *
68
+ * The origin is on the tag rather than in prose so a result cannot claim a
69
+ * friendlier provenance than it has by writing one into its own body.
70
+ */
71
+ function wrapToolData(text, provenance, advisory = 'none', guidance) {
72
+ const attrs = `tool="${provenance.tool}" origin="${provenance.origin}"` +
73
+ (advisory === 'none' ? '' : ` advisory="${advisory}"`);
74
+ const notice = advisory === 'none' ? '' : `${advisoryNotice(advisory)}${guidance ? ` ${guidance}` : ''}\n`;
75
+ return `<${'tool_data'} ${attrs}>\n${notice}${stripToolFences(text)}\n${TOOL_CLOSE}`;
76
+ }
77
+ /**
78
+ * The kernel's own statement of what it observed.
79
+ *
80
+ * Deliberately an observation, not an instruction: what the agent should do about
81
+ * it is product behaviour, supplied by the host as `advisoryGuidance`. Emitted
82
+ * only when signals fired, so it stays rare enough to carry weight — a warning on
83
+ * every fetch is one the model learns to skip.
84
+ */
85
+ function advisoryNotice(advisory) {
86
+ return lexiconText(advisory === 'high' ? 'advisory.notice_high' : 'advisory.notice_elevated');
87
+ }
88
+ /**
89
+ * Compose the model-facing text for a tool result.
90
+ *
91
+ * `finding` is the tool's summary and `data` its structured payload; both reach
92
+ * the model, so both are guarded together rather than only the prose half.
93
+ */
94
+ function composeToolText(finding, data) {
95
+ if (data === undefined) {
96
+ return finding;
97
+ }
98
+ const rendered = textForScan(data);
99
+ if (rendered.unscannable) {
100
+ return finding;
101
+ }
102
+ return `${finding}\n${rendered.text}`;
103
+ }
104
+ /**
105
+ * Guard one tool result on its way into the model's context.
106
+ *
107
+ * Local host tools are still detected — a host tool reading a database returns
108
+ * data the host did not write — but only remote origins are fenced, because
109
+ * fencing a local tool's output would change prompts hosts have already tuned.
110
+ */
111
+ function guardToolResult(finding, data, provenance, policy, callableTools = []) {
112
+ const composed = composeToolText(finding, data);
113
+ const options = detectionForTrust(policy, 'untrusted');
114
+ const redacted = sanitizeText(composed, options);
115
+ const changed = redacted !== composed;
116
+ // Directive detection runs on remote content only: a local tool's output is
117
+ // bytes the host's own code produced.
118
+ const remote = isRemoteOrigin(provenance.origin);
119
+ const suspicious = remote ? directiveHits(composed, callableTools) : [];
120
+ const advisory = advisoryLevel(suspicious);
121
+ const fenced = remote
122
+ ? wrapToolData(redacted, provenance, advisory, policy.taint?.advisoryGuidance)
123
+ : redacted;
124
+ const hits = [
125
+ ...(changed ? [{ rule: 'tool_result.redacted', severity: 'medium' }] : []),
126
+ ...suspicious,
127
+ ];
128
+ if (hits.length === 0) {
129
+ return { text: fenced };
130
+ }
131
+ return {
132
+ text: fenced,
133
+ ...(suspicious.length > 0 ? { suspicious } : {}),
134
+ event: {
135
+ stage: 'tool_result',
136
+ trust: 'untrusted',
137
+ // Redaction changed the text; directive signals only annotate it.
138
+ action: changed ? 'redact' : 'flag',
139
+ hits,
140
+ provenance,
141
+ },
142
+ };
143
+ }
144
+ /**
145
+ * Guard a tool failure message.
146
+ *
147
+ * A remote server authors its own error strings, so an unguarded failure message
148
+ * is the cleanest injection path across this boundary: it reaches the model
149
+ * verbatim and is framed by the kernel as a system report.
150
+ */
151
+ function guardToolFailureText(message, provenance, policy) {
152
+ const options = detectionForTrust(policy, 'untrusted');
153
+ const redacted = sanitizeText(stripToolFences(message), options);
154
+ if (redacted === message) {
155
+ return { text: redacted };
156
+ }
157
+ return {
158
+ text: redacted,
159
+ event: {
160
+ stage: 'tool_result',
161
+ trust: 'untrusted',
162
+ action: 'redact',
163
+ hits: [{ rule: 'tool_failure.redacted', severity: 'medium' }],
164
+ provenance,
165
+ },
166
+ };
167
+ }
168
+ /**
169
+ * Inspect model-supplied tool arguments before the call runs.
170
+ *
171
+ * Arguments are model-authored, so the risk is not instruction smuggling but
172
+ * exfiltration: a credential lifted from context and posted outward as a
173
+ * parameter. Detection reports rather than rewrites — silently altering a tool
174
+ * argument would make the call succeed against something the model did not ask
175
+ * for.
176
+ */
177
+ function inspectToolArguments(args, policy) {
178
+ if (!policy.redactSensitive) {
179
+ return { action: 'allow' };
180
+ }
181
+ const rendered = textForScan(args);
182
+ if (rendered.unscannable) {
183
+ return { action: 'allow' };
184
+ }
185
+ const options = { sanitizeInput: false, redactSensitive: true };
186
+ if (sanitizeText(rendered.text, options) === rendered.text) {
187
+ return { action: 'allow' };
188
+ }
189
+ return {
190
+ action: 'flag',
191
+ hits: [{ rule: 'tool_call.sensitive-argument', severity: 'high' }],
192
+ };
193
+ }
194
+ /** Guardrail event for a flagged tool call, for the runner to emit. */
195
+ function toolCallEvent(verdict, provenance) {
196
+ if (verdict.action === 'allow') {
197
+ return undefined;
198
+ }
199
+ return {
200
+ stage: 'tool_call',
201
+ trust: 'untrusted',
202
+ action: verdict.action,
203
+ hits: verdict.hits,
204
+ provenance,
205
+ };
206
+ }
207
+ /**
208
+ * Record a tool result against the turn's taint.
209
+ *
210
+ * Only remote origins taint: a local host tool returns bytes the host's own code
211
+ * produced, and treating those as attacker-influenceable would make the gate
212
+ * useless in practice.
213
+ */
214
+ function recordTaint(taint, provenance, suspicious = []) {
215
+ const sources = taint?.sources ?? [];
216
+ const prior = taint?.suspicious ?? [];
217
+ if (!isRemoteOrigin(provenance.origin)) {
218
+ return { sources, suspicious: prior };
219
+ }
220
+ return { sources: [...sources, provenance], suspicious: [...prior, ...suspicious] };
221
+ }
222
+ /** True when the turn has already read attacker-influenceable content. */
223
+ function isTainted(taint) {
224
+ return (taint?.sources.length ?? 0) > 0;
225
+ }
226
+ /** True when remote content this turn read looked like it was steering the agent. */
227
+ function isSuspicious(taint) {
228
+ return (taint?.suspicious.length ?? 0) > 0;
229
+ }
230
+ /** Capability rank, so a single threshold can express "this and anything worse". */
231
+ const CAPABILITY_RANK = {
232
+ 'read-only': 0,
233
+ 'read-write': 1,
234
+ destructive: 2,
235
+ };
236
+ const GATE_RANK = {
237
+ off: Number.POSITIVE_INFINITY,
238
+ destructive: 2,
239
+ write: 1,
240
+ };
241
+ /**
242
+ * Decide whether a tool call may proceed given what the turn has already read.
243
+ *
244
+ * A `flag` says the call is happening on a tainted turn and is worth recording; a
245
+ * `block` says the profile asked for it to be refused. Reporting happens whether
246
+ * or not enforcement is configured, so the risk is visible before a host opts in.
247
+ */
248
+ function checkTaintGate(taint, access, policy) {
249
+ if (!isTainted(taint)) {
250
+ return { action: 'allow' };
251
+ }
252
+ const rank = CAPABILITY_RANK[access] ?? 0;
253
+ if (rank === 0) {
254
+ return { action: 'allow' };
255
+ }
256
+ const suspicious = isSuspicious(taint);
257
+ const hits = [
258
+ {
259
+ rule: suspicious ? 'tool_call.steered-turn' : 'tool_call.tainted-turn',
260
+ severity: 'high',
261
+ },
262
+ ];
263
+ // Gated on origin alone. Whether the content looked directive changes the rule
264
+ // reported, never whether the call is refused.
265
+ if (rank < GATE_RANK[policy.taint?.afterRemoteRead ?? 'off']) {
266
+ return { action: 'flag', hits };
267
+ }
268
+ const read = taint?.sources.map((s) => s.tool).join(', ') ?? '';
269
+ const reason = lexiconText(suspicious ? 'taint.reason_steered' : 'taint.reason_tainted');
270
+ return {
271
+ action: 'block',
272
+ hits,
273
+ rejection: lexiconText('taint.blocked', { access, sources: read, reason }),
274
+ };
275
+ }
276
+ export { checkTaintGate, composeToolText, guardToolFailureText, guardToolResult, inspectToolArguments, isRemoteOrigin, isSuspicious, isTainted, recordTaint, TOOL_CLOSE, toolCallEvent, wrapToolData, };
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Guardrail vocabulary — trust levels, stages, verdicts, and profile policy shape.
3
+ *
4
+ * This module is the single source of truth for guardrail types. It must not import
5
+ * from `src/kernel/`: the kernel type-imports `ProfileGuardrailsSpec` for
6
+ * `ProfileCommon.guardrails`, and that edge stays one-directional. Implementation
7
+ * modules under `src/guardrails/` may import kernel types freely.
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * Origin trust for text entering the model's context.
13
+ *
14
+ * - `trusted` — author-time profile text (`identity.system`). Sensitive redaction
15
+ * only; injection redaction would mangle the host's own instructions.
16
+ * - `assembled` — host-built per turn (`req.system`). Interpolates retrieval and
17
+ * user data, so it is permeable and takes full detection.
18
+ * - `untrusted` — user input, tool results, attachments, delegated agents.
19
+ */
20
+ export declare const TRUST_LEVELS: readonly ["trusted", "assembled", "untrusted"];
21
+ export type TrustLevel = (typeof TRUST_LEVELS)[number];
22
+ /** Boundary a guardrail check runs at. */
23
+ export declare const GUARDRAIL_STAGES: readonly ["input", "history", "system", "attachment", "tool_call", "tool_result", "output_delta", "output_final", "network", "live_inbound", "live_outbound", "trace"];
24
+ export type GuardrailStage = (typeof GUARDRAIL_STAGES)[number];
25
+ /** How serious a hit is. Does not decide what happens next — that is `onBlock`. */
26
+ export declare const SEVERITIES: readonly ["info", "low", "medium", "high"];
27
+ export type Severity = (typeof SEVERITIES)[number];
28
+ /** Egress block handling. */
29
+ export declare const EGRESS_ON_BLOCK: readonly ["reject_to_agent", "refuse_to_user"];
30
+ export type EgressOnBlock = (typeof EGRESS_ON_BLOCK)[number];
31
+ /** One detector match. */
32
+ export interface GuardrailHit {
33
+ /** Stable rule id, e.g. `injection.instruction-override`. */
34
+ rule: string;
35
+ severity: Severity;
36
+ /** Offsets into the inspected text; absent for whole-payload checks. */
37
+ span?: {
38
+ start: number;
39
+ end: number;
40
+ };
41
+ /**
42
+ * Exact matched substring (capped). Present when detectors had the source text.
43
+ * Stripped from host/trace unless `observability.include.guardrailMatchPreview`.
44
+ */
45
+ match?: string;
46
+ }
47
+ /**
48
+ * Outcome of one guardrail evaluation.
49
+ *
50
+ * A discriminated union so a new variant fails every unhandled `switch` at
51
+ * compile time rather than falling through at runtime.
52
+ */
53
+ export type Verdict = {
54
+ action: 'allow';
55
+ } | {
56
+ action: 'redact';
57
+ text: string;
58
+ hits: GuardrailHit[];
59
+ } | {
60
+ action: 'flag';
61
+ hits: GuardrailHit[];
62
+ } | {
63
+ action: 'block';
64
+ hits: GuardrailHit[];
65
+ /** Sent to the model on a repair turn when `onBlock` is `reject_to_agent`. */
66
+ rejection: string;
67
+ /** Shown to the user when `onBlock` is `refuse_to_user`. Host-owned copy. */
68
+ refusal?: string;
69
+ };
70
+ export type GuardrailAction = Verdict['action'];
71
+ /**
72
+ * Where a tool result came from.
73
+ *
74
+ * `local` is host TypeScript the profile registered; `http` and `mcp` are remote
75
+ * services whose bytes the host does not control. `delegated` is another agent
76
+ * answering through the tool boundary — its output is model-generated prose that
77
+ * reads as authoritative, which is why depth is tracked separately.
78
+ */
79
+ export declare const TOOL_ORIGINS: readonly ["local", "builtin", "http", "mcp", "delegated"];
80
+ export type ToolOrigin = (typeof TOOL_ORIGINS)[number];
81
+ /** Where a piece of content entered the turn from. */
82
+ export interface Provenance {
83
+ origin: ToolOrigin;
84
+ /** Registered tool name. */
85
+ tool: string;
86
+ /**
87
+ * Hops from the user's turn. A direct tool call is 1; a tool result produced by
88
+ * a delegated agent that itself called tools is deeper. Depth matters because a
89
+ * two-hop delegation can otherwise launder remote content into trusted-looking
90
+ * output.
91
+ */
92
+ depth: number;
93
+ }
94
+ /**
95
+ * Untrusted content a turn has already taken into its context.
96
+ *
97
+ * Once a turn has read attacker-influenceable bytes, a later tool call is a
98
+ * confused-deputy risk: the content can ask the agent to act, and the agent has
99
+ * authority the content does not. Sources are kept in order so a policy can reason
100
+ * about depth as well as presence.
101
+ */
102
+ export interface TurnTaint {
103
+ sources: Provenance[];
104
+ /**
105
+ * Directive hits found in remote content this turn read.
106
+ *
107
+ * Separates "read something remote" from "read something that tried to steer
108
+ * me". The second is far rarer, so a gate keyed on it refuses far less
109
+ * legitimate work.
110
+ */
111
+ suspicious: GuardrailHit[];
112
+ }
113
+ /**
114
+ * What a turn may still do after it has ingested untrusted remote content.
115
+ *
116
+ * Enforcement is opt-in. Tracking and reporting are on by default — every
117
+ * remote read is observable — but refusing tool calls changes what working agents
118
+ * are allowed to do, so a host declares which access levels to gate rather than
119
+ * having the kernel guess.
120
+ */
121
+ /**
122
+ * How strongly the tool-ingress signals fired, derived from the hits themselves.
123
+ *
124
+ * Not a probability: there is no calibrated model behind it. `elevated` means one
125
+ * directive signal alongside an external destination; `high` means the content
126
+ * named a tool the model can call, or several signals agreed.
127
+ */
128
+ export declare const ADVISORY_LEVELS: readonly ["none", "elevated", "high"];
129
+ export type AdvisoryLevel = (typeof ADVISORY_LEVELS)[number];
130
+ export declare const TAINT_GATES: readonly ["off", "destructive", "write"];
131
+ export type TaintGate = (typeof TAINT_GATES)[number];
132
+ /**
133
+ * Only structural facts gate tool calls.
134
+ *
135
+ * Content signals from `tool-directives.ts` deliberately have no gate here. They
136
+ * are pattern matches with no measured precision, and refusing a tool call on an
137
+ * unpredictable signal makes an agent unreliable rather than safe — the failure is
138
+ * invisible to the user and looks like the agent being stupid. Those signals
139
+ * annotate the fence and raise telemetry; the model still gets to decide, and the
140
+ * host still gets to see.
141
+ */
142
+ export interface TaintGuardrailSpec {
143
+ /**
144
+ * Host copy appended to the fence when tool content looks directive.
145
+ *
146
+ * The kernel states what it observed; what the agent should *do* about it —
147
+ * ask the user, refuse, proceed carefully — is product behaviour and stays
148
+ * host-owned. Omitted means the observation is stated without guidance.
149
+ */
150
+ advisoryGuidance?: string;
151
+ /**
152
+ * Least-severe tool capability refused once the turn has read remote content.
153
+ *
154
+ * - `off` (default) — report only.
155
+ * - `destructive` — refuse hard-to-undo calls.
156
+ * - `write` — refuse those and any state-changing call.
157
+ *
158
+ * Stated as a capability threshold rather than a list of tool `access` values so
159
+ * the guardrail vocabulary stays independent of the tool registry; the kernel
160
+ * maps a tool's declared access onto it.
161
+ */
162
+ afterRemoteRead?: TaintGate;
163
+ }
164
+ /** Facts a check may read. Deliberately excludes the full profile. */
165
+ export interface GuardrailContext {
166
+ stage: GuardrailStage;
167
+ trust: TrustLevel;
168
+ profileId: string;
169
+ canary?: string;
170
+ role?: string;
171
+ slots?: Record<string, string>;
172
+ /** Set on tool-shaped stages; absent for user and system text. */
173
+ provenance?: Provenance;
174
+ }
175
+ /**
176
+ * One guardrail decision, as it reaches the host and the trace.
177
+ *
178
+ * Carries rule identity and offsets, never the matched content, so a trace sink
179
+ * can count and locate hits without becoming a second copy of the secret.
180
+ */
181
+ export interface GuardrailEvent {
182
+ stage: GuardrailStage;
183
+ trust: TrustLevel;
184
+ action: GuardrailAction;
185
+ hits: GuardrailHit[];
186
+ provenance?: Provenance;
187
+ }
188
+ /**
189
+ * User-visible output projected out of the turn's events.
190
+ *
191
+ * Structured output travels alongside text so a profile with `outputs.structured`
192
+ * is not invisible to its own egress policy.
193
+ */
194
+ export interface OutboundPayload {
195
+ /** Concatenated user-visible text for this attempt. */
196
+ text: string;
197
+ /** Structured output, when the profile emits it. */
198
+ structured?: unknown;
199
+ }
200
+ /** Evaluates candidate user-visible output before release. */
201
+ export type EgressEnforcer = (payload: OutboundPayload, context: GuardrailContext) => Verdict | Promise<Verdict>;
202
+ /** Profile egress policy for rejection, retry, or refusal behavior. */
203
+ export interface ProfileEgressSpec {
204
+ enforce: EgressEnforcer;
205
+ onBlock?: EgressOnBlock;
206
+ maxRetries?: number;
207
+ repairGuidance?: string;
208
+ }
209
+ /** SSRF and network access policy for HTTP and remote MCP tools. */
210
+ export interface NetworkGuardrailSpec {
211
+ /**
212
+ * When true, allows connections to localhost / loopback and private subnets
213
+ * (e.g. for local dev/testing). Default: false.
214
+ */
215
+ allowPrivateNetworks?: boolean;
216
+ /** Hostnames or IP addresses permitted regardless of private subnet status. */
217
+ allowedHosts?: string[];
218
+ /**
219
+ * Allowed URL schemes. Defaults to `['https']`, or `['http', 'https']` when
220
+ * `allowPrivateNetworks` is set.
221
+ */
222
+ allowedSchemes?: string[];
223
+ }
224
+ /** Optional daily turn quota consumed by host HTTP middleware. */
225
+ export interface QuotaGuardrailSpec {
226
+ perDay: number;
227
+ /**
228
+ * Host copy surfaced when the quota trips. The kernel never authors this:
229
+ * `quotaExhausted` returns structured data (`code`, `perDay`) and includes
230
+ * this string only when the host set it.
231
+ */
232
+ message?: string;
233
+ }
234
+ /**
235
+ * Per-turn canary switches. `true` / `false` toggles minting with the
236
+ * registered default bind note; the object form supplies host copy.
237
+ */
238
+ export interface CanaryGuardrailSpec {
239
+ /**
240
+ * Host template appended to the system prompt binding the canary. Must
241
+ * contain the `{canary}` placeholder; `bindCanary` refuses a note that lost
242
+ * the token. Omitted means the lexicon default (`canary.bind_note`).
243
+ */
244
+ bindNote?: string;
245
+ }
246
+ /** Profile guardrail switches enforced by the kernel. */
247
+ export interface ProfileGuardrailsSpec {
248
+ /** Optional daily turn quota; omitted means quota enforcement is not configured. */
249
+ quota?: QuotaGuardrailSpec;
250
+ canary?: boolean | CanaryGuardrailSpec;
251
+ sanitizeInput?: boolean;
252
+ redactSensitive?: boolean;
253
+ egress?: ProfileEgressSpec;
254
+ /** SSRF and network access policies for HTTP and MCP tools. */
255
+ network?: NetworkGuardrailSpec;
256
+ /** What the turn may still do after reading untrusted remote content. */
257
+ taint?: TaintGuardrailSpec;
258
+ }
259
+ /** The guardrail field names a `host` profile may set. */
260
+ export declare const HOST_GUARDRAIL_FIELDS: readonly ["sanitizeInput", "redactSensitive", "network", "taint"];
261
+ /**
262
+ * The guardrail switches a `host` profile may set.
263
+ *
264
+ * A host profile runs no model, so only the guards that fire on the `invokeTool`
265
+ * path exist for it: the detectors applied to model-supplied arguments and to
266
+ * tool result and failure text (`sanitizeInput`, `redactSensitive`), SSRF
267
+ * clearance for declarative HTTP and MCP targets (`network`), and the
268
+ * confused-deputy gate on a tainted turn (`taint`). Everything else in
269
+ * {@link ProfileGuardrailsSpec} — quota, canary, egress — guards a model turn
270
+ * and is refused by `defineProfile` on `type: 'host'`.
271
+ *
272
+ * This is a view of the one guardrail vocabulary, not a second hierarchy.
273
+ */
274
+ export type HostGuardrailsSpec = Pick<ProfileGuardrailsSpec, (typeof HOST_GUARDRAIL_FIELDS)[number]>;
275
+ /**
276
+ * A profile's guardrail switches with defaults applied.
277
+ *
278
+ * Every path resolves through `resolveGuardrailPolicy` so turn and Live ingress
279
+ * cannot drift apart on defaults.
280
+ */
281
+ export interface ResolvedGuardrailPolicy {
282
+ sanitizeInput: boolean;
283
+ redactSensitive: boolean;
284
+ canary: boolean;
285
+ /** Host bind-note template from `guardrails.canary.bindNote`, when set. */
286
+ canaryBindNote?: string;
287
+ egress?: ProfileEgressSpec;
288
+ network?: NetworkGuardrailSpec;
289
+ quota?: QuotaGuardrailSpec;
290
+ taint?: TaintGuardrailSpec;
291
+ }