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,234 @@
1
+ /**
2
+ * Network SSRF guardrails for HTTP and remote MCP tools.
3
+ *
4
+ * Enforces URL scheme safety and blocks loopback, private RFC 1918,
5
+ * link-local (cloud metadata 169.254.x.x), and multicast targets unless
6
+ * explicitly permitted by profile guardrail configuration.
7
+ *
8
+ * @module
9
+ */
10
+ import { TheorumError } from './error.js';
11
+ const PRIVATE_OR_LOCAL_IPV4_MATCHES = [
12
+ (b0) => b0 === 0, // 0.0.0.0/8
13
+ (b0) => b0 === 127, // 127.0.0.0/8 (loopback)
14
+ (b0) => b0 === 10, // 10.0.0.0/8
15
+ (b0, b1) => b0 === 100 && b1 >= 64 && b1 <= 127, // 100.64.0.0/10 (CGNAT)
16
+ (b0, b1) => b0 === 172 && b1 >= 16 && b1 <= 31, // 172.16.0.0/12
17
+ (b0, b1) => b0 === 192 && b1 === 168, // 192.168.0.0/16
18
+ (b0, b1) => b0 === 169 && b1 === 254, // 169.254.0.0/16 (link-local)
19
+ (b0, b1, b2) => b0 === 192 && b1 === 0 && (b2 === 0 || b2 === 2), // 192.0.0.0/24, 192.0.2.0/24
20
+ (b0, b1) => b0 === 198 && (b1 === 18 || b1 === 19), // 198.18.0.0/15
21
+ (b0, b1, b2) => b0 === 198 && b1 === 51 && b2 === 100, // 198.51.100.0/24
22
+ (b0, b1, b2) => b0 === 203 && b1 === 0 && b2 === 113, // 203.0.113.0/24
23
+ (b0) => b0 >= 224, // Multicast & Reserved (224.0.0.0/4, 240.0.0.0/4)
24
+ ];
25
+ function isPrivateOrLocalIPv4Parts(b0, b1, b2, _b3) {
26
+ return PRIVATE_OR_LOCAL_IPV4_MATCHES.some((match) => match(b0, b1, b2));
27
+ }
28
+ function isPrivateOrLocalIPv4(ip) {
29
+ const parts = ip.split('.').map((p) => Number.parseInt(p, 10));
30
+ if (parts.length !== 4) {
31
+ return false;
32
+ }
33
+ const b0 = parts[0];
34
+ const b1 = parts[1];
35
+ const b2 = parts[2];
36
+ const b3 = parts[3];
37
+ if (b0 === undefined ||
38
+ b1 === undefined ||
39
+ b2 === undefined ||
40
+ b3 === undefined ||
41
+ Number.isNaN(b0) ||
42
+ Number.isNaN(b1) ||
43
+ Number.isNaN(b2) ||
44
+ Number.isNaN(b3) ||
45
+ b0 < 0 ||
46
+ b0 > 255 ||
47
+ b1 < 0 ||
48
+ b1 > 255 ||
49
+ b2 < 0 ||
50
+ b2 > 255 ||
51
+ b3 < 0 ||
52
+ b3 > 255) {
53
+ return false;
54
+ }
55
+ return isPrivateOrLocalIPv4Parts(b0, b1, b2, b3);
56
+ }
57
+ function parseIPv6Words(ip) {
58
+ const trimmed = ip.toLowerCase().trim();
59
+ const lastColon = trimmed.lastIndexOf(':');
60
+ let ipv4Words = [];
61
+ let v6Str = trimmed;
62
+ if (lastColon !== -1) {
63
+ const potentialV4 = trimmed.slice(lastColon + 1);
64
+ if (potentialV4.includes('.')) {
65
+ const v4Parts = potentialV4.split('.').map((p) => Number.parseInt(p, 10));
66
+ if (v4Parts.length !== 4 || v4Parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) {
67
+ return null;
68
+ }
69
+ const b0 = v4Parts[0] ?? 0;
70
+ const b1 = v4Parts[1] ?? 0;
71
+ const b2 = v4Parts[2] ?? 0;
72
+ const b3 = v4Parts[3] ?? 0;
73
+ ipv4Words = [(b0 << 8) | b1, (b2 << 8) | b3];
74
+ v6Str = trimmed.slice(0, lastColon);
75
+ }
76
+ }
77
+ const parts = v6Str.split('::');
78
+ if (parts.length > 2)
79
+ return null;
80
+ const left = parts[0] ? parts[0].split(':').filter(Boolean) : [];
81
+ const right = parts.length === 2 && parts[1] ? parts[1].split(':').filter(Boolean) : [];
82
+ const needed = 8 - ipv4Words.length;
83
+ if (parts.length === 1) {
84
+ if (left.length !== needed)
85
+ return null;
86
+ }
87
+ else {
88
+ if (left.length + right.length >= needed)
89
+ return null;
90
+ }
91
+ const zerosNeeded = needed - (left.length + right.length);
92
+ const mid = new Array(zerosNeeded).fill(0);
93
+ const parseHex = (s) => (/^[0-9a-f]{1,4}$/.test(s) ? Number.parseInt(s, 16) : null);
94
+ const leftWords = [];
95
+ for (const s of left) {
96
+ const w = parseHex(s);
97
+ if (w === null)
98
+ return null;
99
+ leftWords.push(w);
100
+ }
101
+ const rightWords = [];
102
+ for (const s of right) {
103
+ const w = parseHex(s);
104
+ if (w === null)
105
+ return null;
106
+ rightWords.push(w);
107
+ }
108
+ return [...leftWords, ...mid, ...rightWords, ...ipv4Words];
109
+ }
110
+ /**
111
+ * Checks if an IPv6 address is in a private, loopback, or link-local range:
112
+ * - Loopback: ::1
113
+ * - Unspecified: ::
114
+ * - Link-local: fe80::/10
115
+ * - Unique Local (ULA): fc00::/7 (fc00:: - fdff::)
116
+ * - Multicast: ff00::/8
117
+ * - Documentation: 2001:db8::/32
118
+ * - Discard: 100::/64
119
+ * - IPv4-mapped (::ffff:0:0/96), IPv4-compatible (::/96), and NAT64 (64:ff9b::/96)
120
+ */
121
+ function isPrivateOrLocalIPv6(ip) {
122
+ const words = parseIPv6Words(ip);
123
+ if (words?.length !== 8) {
124
+ return false;
125
+ }
126
+ const w0 = words[0] ?? 0;
127
+ const w1 = words[1] ?? 0;
128
+ // Unspecified ::
129
+ if (words.every((w) => w === 0))
130
+ return true;
131
+ // Loopback ::1
132
+ if (words.slice(0, 7).every((w) => w === 0) && words[7] === 1)
133
+ return true;
134
+ // Link-local unicast (fe80::/10)
135
+ if ((w0 & 0xffc0) === 0xfe80)
136
+ return true;
137
+ // Unique local address (fc00::/7)
138
+ if ((w0 & 0xfe00) === 0xfc00)
139
+ return true;
140
+ // Multicast (ff00::/8)
141
+ if ((w0 & 0xff00) === 0xff00)
142
+ return true;
143
+ // Documentation (2001:db8::/32)
144
+ if (w0 === 0x2001 && w1 === 0x0db8)
145
+ return true;
146
+ // Discard prefix (100::/64)
147
+ if (w0 === 0x0100 && words.slice(1, 4).every((w) => w === 0))
148
+ return true;
149
+ // IPv4-mapped (::ffff:0:0/96)
150
+ const isV4Mapped = words.slice(0, 5).every((w) => w === 0) && words[5] === 0xffff;
151
+ // IPv4-compatible (::/96)
152
+ const isV4Compatible = words.slice(0, 6).every((w) => w === 0);
153
+ // NAT64 well-known prefix (64:ff9b::/96)
154
+ const isNat64 = w0 === 0x0064 && w1 === 0xff9b && words.slice(2, 6).every((w) => w === 0);
155
+ if (isV4Mapped || isV4Compatible || isNat64) {
156
+ const w6 = words[6] ?? 0;
157
+ const w7 = words[7] ?? 0;
158
+ const b0 = (w6 >> 8) & 0xff;
159
+ const b1 = w6 & 0xff;
160
+ const b2 = (w7 >> 8) & 0xff;
161
+ const b3 = w7 & 0xff;
162
+ return isPrivateOrLocalIPv4Parts(b0, b1, b2, b3);
163
+ }
164
+ return false;
165
+ }
166
+ /** Check if hostname represents localhost or private domain names */
167
+ export function isLocalhostName(hostname) {
168
+ let lower = hostname.toLowerCase();
169
+ while (lower.endsWith('.')) {
170
+ lower = lower.slice(0, -1);
171
+ }
172
+ return (lower === 'localhost' ||
173
+ lower.endsWith('.localhost') ||
174
+ lower === 'local' ||
175
+ lower.endsWith('.local') ||
176
+ lower === 'internal' ||
177
+ lower.endsWith('.internal') ||
178
+ lower === 'lan' ||
179
+ lower.endsWith('.lan') ||
180
+ lower === 'home.arpa' ||
181
+ lower.endsWith('.home.arpa') ||
182
+ lower === 'localdomain' ||
183
+ lower.endsWith('.localdomain') ||
184
+ lower === '127.0.0.1' ||
185
+ lower === '::1' ||
186
+ lower === '[::1]');
187
+ }
188
+ /** Check if an IP address string is loopback or private IPv4/IPv6 */
189
+ export function isPrivateOrLocalAddress(ipOrHost) {
190
+ const stripped = ipOrHost.startsWith('[') && ipOrHost.endsWith(']') ? ipOrHost.slice(1, -1) : ipOrHost;
191
+ return isPrivateOrLocalIPv4(stripped) || isPrivateOrLocalIPv6(stripped);
192
+ }
193
+ /**
194
+ * Validates a target URL against network guardrail policy.
195
+ * Throws a `TheorumError` if the URL is blocked.
196
+ */
197
+ export function assertSafeUrl(urlStr, policy) {
198
+ let parsed;
199
+ try {
200
+ parsed = new URL(urlStr);
201
+ }
202
+ catch {
203
+ throw new TheorumError(`Invalid URL provided: "${urlStr}"`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
204
+ }
205
+ const allowPrivate = policy?.allowPrivateNetworks ?? false;
206
+ const allowedHosts = policy?.allowedHosts?.map((h) => h.toLowerCase()) ?? [];
207
+ const hostname = parsed.hostname.toLowerCase();
208
+ // If host is explicitly whitelisted, allow it
209
+ if (allowedHosts.includes(hostname)) {
210
+ return parsed;
211
+ }
212
+ // Scheme validation
213
+ const defaultSchemes = allowPrivate ? ['http:', 'https:'] : ['https:'];
214
+ const allowedSchemes = policy?.allowedSchemes
215
+ ? policy.allowedSchemes.map((s) => (s.endsWith(':') ? s.toLowerCase() : `${s.toLowerCase()}:`))
216
+ : defaultSchemes;
217
+ if (!allowedSchemes.includes(parsed.protocol)) {
218
+ throw new TheorumError(`URL scheme "${parsed.protocol}" is not permitted by network policy. Allowed: ${allowedSchemes.join(', ')}`);
219
+ }
220
+ // Unless private networks are allowed, block localhost and private subnets
221
+ if (!allowPrivate) {
222
+ if (isLocalhostName(hostname)) {
223
+ throw new TheorumError(`Access to loopback target "${hostname}" blocked by network guardrail. Enable allowPrivateNetworks to permit local addresses.`);
224
+ }
225
+ if (isPrivateOrLocalIPv4(hostname)) {
226
+ throw new TheorumError(`Access to private IPv4 address "${hostname}" blocked by network guardrail.`);
227
+ }
228
+ const strippedV6 = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
229
+ if (isPrivateOrLocalIPv6(strippedV6)) {
230
+ throw new TheorumError(`Access to private IPv6 address "${hostname}" blocked by network guardrail.`);
231
+ }
232
+ }
233
+ return parsed;
234
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Guardrail policy resolution — one place where profile switches become defaults.
3
+ *
4
+ * Every ingress and egress path resolves through here so the turn engine and Live
5
+ * ingress cannot drift apart on what "unset" means.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ProfileGuardrailsSpec, ResolvedGuardrailPolicy, TrustLevel } from './types.js';
10
+ /** Detection switches for one piece of text, after trust is taken into account. */
11
+ export interface DetectionOptions {
12
+ sanitizeInput: boolean;
13
+ redactSensitive: boolean;
14
+ }
15
+ /**
16
+ * Apply kernel defaults to a profile's guardrail switches.
17
+ *
18
+ * Sanitization, sensitive redaction, and canary default on. Set `canary: false`
19
+ * to opt out of minting a per-turn token into the system prompt.
20
+ */
21
+ declare function resolveGuardrailPolicy(spec: ProfileGuardrailsSpec | undefined): ResolvedGuardrailPolicy;
22
+ /**
23
+ * Narrow a resolved policy to the detectors that may run against text of a given
24
+ * origin, on the way to the provider.
25
+ *
26
+ * Trusted text is author-time profile copy. Injection redaction would strip the
27
+ * host's own anti-injection instructions, and sensitive redaction would rewrite a
28
+ * prompt that legitimately shows a key or address format — so trusted text is
29
+ * passed to the provider verbatim. Trace safety does not depend on this: the trace
30
+ * writer redacts independently on every path.
31
+ *
32
+ * Assembled and untrusted text take whatever the profile enabled.
33
+ */
34
+ declare function detectionForTrust(policy: ResolvedGuardrailPolicy, trust: TrustLevel): DetectionOptions;
35
+ export { detectionForTrust, resolveGuardrailPolicy };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Guardrail policy resolution — one place where profile switches become defaults.
3
+ *
4
+ * Every ingress and egress path resolves through here so the turn engine and Live
5
+ * ingress cannot drift apart on what "unset" means.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * Apply kernel defaults to a profile's guardrail switches.
11
+ *
12
+ * Sanitization, sensitive redaction, and canary default on. Set `canary: false`
13
+ * to opt out of minting a per-turn token into the system prompt.
14
+ */
15
+ function resolveGuardrailPolicy(spec) {
16
+ const canarySpec = spec?.canary;
17
+ return {
18
+ sanitizeInput: spec?.sanitizeInput ?? true,
19
+ redactSensitive: spec?.redactSensitive ?? true,
20
+ // Object form configures the bind note; minting stays on.
21
+ canary: typeof canarySpec === 'boolean' ? canarySpec : true,
22
+ canaryBindNote: typeof canarySpec === 'object' ? canarySpec.bindNote : undefined,
23
+ egress: spec?.egress,
24
+ network: spec?.network,
25
+ quota: spec?.quota,
26
+ taint: spec?.taint,
27
+ };
28
+ }
29
+ /**
30
+ * Narrow a resolved policy to the detectors that may run against text of a given
31
+ * origin, on the way to the provider.
32
+ *
33
+ * Trusted text is author-time profile copy. Injection redaction would strip the
34
+ * host's own anti-injection instructions, and sensitive redaction would rewrite a
35
+ * prompt that legitimately shows a key or address format — so trusted text is
36
+ * passed to the provider verbatim. Trace safety does not depend on this: the trace
37
+ * writer redacts independently on every path.
38
+ *
39
+ * Assembled and untrusted text take whatever the profile enabled.
40
+ */
41
+ function detectionForTrust(policy, trust) {
42
+ if (trust === 'trusted') {
43
+ return { sanitizeInput: false, redactSensitive: false };
44
+ }
45
+ return {
46
+ sanitizeInput: policy.sanitizeInput,
47
+ redactSensitive: policy.redactSensitive,
48
+ };
49
+ }
50
+ export { detectionForTrust, resolveGuardrailPolicy };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Progressive-yield outbound gate — stream cleared prefixes while holding a
3
+ * lookback window so canary / sensitive / boundary / injection (and optional
4
+ * host `egress.enforce`) can inspect split-token matches before release.
5
+ *
6
+ * @module
7
+ */
8
+ import type { EgressEnforcer, GuardrailContext, GuardrailHit, ResolvedGuardrailPolicy } from './types.js';
9
+ /** Default lookback for span detectors that are not canary-sized. */
10
+ declare const DEFAULT_HOLDBACK = 256;
11
+ export type ProgressiveYieldOk = {
12
+ blocked: false;
13
+ emit: string;
14
+ };
15
+ export type ProgressiveYieldBlocked = {
16
+ blocked: true;
17
+ hits: GuardrailHit[];
18
+ };
19
+ export type ProgressiveYieldResult = ProgressiveYieldOk | ProgressiveYieldBlocked;
20
+ export interface ProgressiveYieldGateOptions {
21
+ /** Stage facts handed to `enforce`; also carries the turn canary. */
22
+ context: GuardrailContext;
23
+ /** When set, each step runs this policy on the accumulated window before emit. */
24
+ enforce?: EgressEnforcer;
25
+ /** Floor for lookback beyond canary overlap (characters). */
26
+ holdback?: number;
27
+ }
28
+ interface ProgressiveYieldGate {
29
+ process: (fragment: string) => Promise<ProgressiveYieldResult>;
30
+ flush: () => Promise<ProgressiveYieldResult>;
31
+ /** Full window inspected so far (for end-of-attempt egress / repair). */
32
+ accumulated: () => string;
33
+ /** Lookback tail not yet released to the host. Peek only. */
34
+ unreleased: () => string;
35
+ /**
36
+ * Take the tail not yet released and mark it released.
37
+ *
38
+ * Used when the runner records withheld text for the egress window: without
39
+ * advancing the cursor a later `flush` re-releases the same range and the
40
+ * attempt buffer ends up holding the text twice.
41
+ */
42
+ drainUnreleased: () => string;
43
+ }
44
+ declare function createProgressiveYieldGate(options: ProgressiveYieldGateOptions): ProgressiveYieldGate;
45
+ /**
46
+ * Shared constructor for runTurn + Live: gate when canary and/or egress.enforce
47
+ * is active. `context.canary` is set only when the profile enabled canary minting.
48
+ */
49
+ declare function createOutboundProgressiveGate(policy: ResolvedGuardrailPolicy, context: GuardrailContext): ProgressiveYieldGate | null;
50
+ export type { ProgressiveYieldGate };
51
+ export { createOutboundProgressiveGate, createProgressiveYieldGate, DEFAULT_HOLDBACK };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Progressive-yield outbound gate — stream cleared prefixes while holding a
3
+ * lookback window so canary / sensitive / boundary / injection (and optional
4
+ * host `egress.enforce`) can inspect split-token matches before release.
5
+ *
6
+ * @module
7
+ */
8
+ import { collectEgressHits, runEnforcer } from './egress.js';
9
+ /** Default lookback for span detectors that are not canary-sized. */
10
+ const DEFAULT_HOLDBACK = 256;
11
+ const PEM_BEGIN = '-----BEGIN';
12
+ function resolveHoldback(canary, holdback) {
13
+ const canaryHold = canary ? Math.max(0, canary.length - 1) : 0;
14
+ return Math.max(canaryHold, holdback ?? DEFAULT_HOLDBACK);
15
+ }
16
+ function holdbackForWindow(window, base) {
17
+ // Incomplete PEM bodies can be large; do not release past BEGIN until END/flush.
18
+ const begin = window.lastIndexOf(PEM_BEGIN);
19
+ if (begin < 0)
20
+ return base;
21
+ const fromBegin = window.slice(begin);
22
+ if (/-----END (?:RSA )?PRIVATE KEY-----/.test(fromBegin))
23
+ return base;
24
+ return Math.max(base, window.length - begin);
25
+ }
26
+ function createProgressiveYieldGate(options) {
27
+ const { context } = options;
28
+ const baseHoldback = resolveHoldback(context.canary, options.holdback);
29
+ let accumulated = '';
30
+ let emitted = 0;
31
+ async function scan(window) {
32
+ if (options.enforce) {
33
+ // Mid-stream the gate can only release or stop: emitted prefixes cannot be
34
+ // rewritten, so `redact` stops here and end-of-attempt egress applies the
35
+ // full verdict. `flag` is advisory and keeps the stream flowing.
36
+ const verdict = await runEnforcer(options.enforce, { text: window }, context);
37
+ if (verdict.action === 'block' || verdict.action === 'redact') {
38
+ return verdict.hits.length > 0
39
+ ? verdict.hits
40
+ : [{ rule: 'egress.blocked', severity: 'high' }];
41
+ }
42
+ // Host enforce is authoritative when present (matches end-of-attempt egress).
43
+ return null;
44
+ }
45
+ const hits = collectEgressHits(window, context.canary);
46
+ return hits.length > 0 ? hits : null;
47
+ }
48
+ async function release(releaseTail) {
49
+ const hits = await scan(accumulated);
50
+ if (hits) {
51
+ return { blocked: true, hits };
52
+ }
53
+ if (releaseTail) {
54
+ const emit = accumulated.slice(emitted);
55
+ emitted = accumulated.length;
56
+ return { blocked: false, emit };
57
+ }
58
+ const hold = holdbackForWindow(accumulated, baseHoldback);
59
+ const safeEnd = Math.max(emitted, accumulated.length - hold);
60
+ const emit = accumulated.slice(emitted, safeEnd);
61
+ emitted = safeEnd;
62
+ return { blocked: false, emit };
63
+ }
64
+ return {
65
+ async process(fragment) {
66
+ if (!fragment) {
67
+ return { blocked: false, emit: '' };
68
+ }
69
+ accumulated += fragment;
70
+ return await release(false);
71
+ },
72
+ async flush() {
73
+ return await release(true);
74
+ },
75
+ accumulated: () => accumulated,
76
+ unreleased: () => accumulated.slice(emitted),
77
+ drainUnreleased() {
78
+ const tail = accumulated.slice(emitted);
79
+ emitted = accumulated.length;
80
+ return tail;
81
+ },
82
+ };
83
+ }
84
+ /**
85
+ * Shared constructor for runTurn + Live: gate when canary and/or egress.enforce
86
+ * is active. `context.canary` is set only when the profile enabled canary minting.
87
+ */
88
+ function createOutboundProgressiveGate(policy, context) {
89
+ const enforce = policy.egress?.enforce;
90
+ if (!enforce && !context.canary) {
91
+ return null;
92
+ }
93
+ return createProgressiveYieldGate({
94
+ context,
95
+ ...(enforce ? { enforce } : {}),
96
+ });
97
+ }
98
+ export { createOutboundProgressiveGate, createProgressiveYieldGate, DEFAULT_HOLDBACK };
@@ -4,7 +4,21 @@ declare function skipQuota(peer: string, req: Request): boolean;
4
4
  declare function clientIp(peer: string, req: Request): string;
5
5
  declare function takeSlot(profile: Profile, ip: string, now: number): QuotaSlotStatus;
6
6
  declare function releaseSlot(profile: Profile, ip: string): void;
7
- declare function quotaMessage(profile: Profile): string;
7
+ /**
8
+ * Structured quota-trip report. The kernel authors no copy here: `message` is
9
+ * present if and only if the host set `guardrails.quota.message`.
10
+ */
11
+ interface QuotaExhausted {
12
+ code: 'quota_exhausted';
13
+ perDay: number;
14
+ message?: string;
15
+ }
16
+ /**
17
+ * Structured data for a tripped quota, or `undefined` when the profile has no
18
+ * quota configured. Hosts render their own copy from `code` / `perDay` /
19
+ * `message` — there is no English fallback in the kernel.
20
+ */
21
+ declare function quotaExhausted(profile: Profile): QuotaExhausted | undefined;
8
22
  declare function resetSlots(): void;
9
- export type { QuotaSlotStatus };
10
- export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot };
23
+ export type { QuotaExhausted, QuotaSlotStatus };
24
+ export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -1,3 +1,4 @@
1
+ import { resolveGuardrailPolicy } from './policy.js';
1
2
  const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
2
3
  const slots = new Map();
3
4
  function utcDay(now) {
@@ -28,7 +29,7 @@ function clientIp(peer, req) {
28
29
  return 'unknown';
29
30
  }
30
31
  function takeSlot(profile, ip, now) {
31
- const quota = profile.guardrails?.quota;
32
+ const quota = resolveGuardrailPolicy(profile.guardrails).quota;
32
33
  if (!quota) {
33
34
  return 'not_configured';
34
35
  }
@@ -55,10 +56,23 @@ function releaseSlot(profile, ip) {
55
56
  slot.busy = false;
56
57
  }
57
58
  }
58
- function quotaMessage(profile) {
59
- return `Enjoying ${profile.identity.handle}? You've reached today's limit`;
59
+ /**
60
+ * Structured data for a tripped quota, or `undefined` when the profile has no
61
+ * quota configured. Hosts render their own copy from `code` / `perDay` /
62
+ * `message` — there is no English fallback in the kernel.
63
+ */
64
+ function quotaExhausted(profile) {
65
+ const quota = resolveGuardrailPolicy(profile.guardrails).quota;
66
+ if (!quota) {
67
+ return undefined;
68
+ }
69
+ return {
70
+ code: 'quota_exhausted',
71
+ perDay: quota.perDay,
72
+ ...(quota.message !== undefined ? { message: quota.message } : {}),
73
+ };
60
74
  }
61
75
  function resetSlots() {
62
76
  slots.clear();
63
77
  }
64
- export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot };
78
+ export { clientIp, quotaExhausted, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -3,18 +3,48 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import type { NormalizedTurnRequest, TurnRequest } from '../kernel/types.js';
6
+ import type { NormalizedTurnRequest, TurnEvent, TurnRequest } from '../kernel/types.js';
7
+ import { type DetectionOptions } from './policy.js';
8
+ import type { GuardrailHit, TrustLevel } from './types.js';
9
+ /**
10
+ * Detect and redact injection / sensitive spans. Returns hits for observability
11
+ * (rule + offsets + optional `match` preview for debugging).
12
+ */
13
+ declare function detectText(text: string, options?: Partial<DetectionOptions>): {
14
+ text: string;
15
+ hits: GuardrailHit[];
16
+ };
7
17
  /** Sanitize one text value using prompt-injection and sensitive-data detectors. */
8
- declare function sanitizeText(text: string, options?: {
9
- sanitizeInput?: boolean;
10
- redactSensitive?: boolean;
11
- }): string;
18
+ declare function sanitizeText(text: string, options?: Partial<DetectionOptions>): string;
12
19
  /** Redact only sensitive data (credentials, PII) — skip injection patterns. */
13
20
  declare function redactSensitiveOnly(text: string): string;
14
21
  declare const PROJECT_ID_MAX = 128;
15
22
  declare function sanitizeProjectId(id: string | undefined): string | undefined;
23
+ /**
24
+ * Sanitize the text of each history message; tool calls, ids, and metadata pass
25
+ * through untouched.
26
+ *
27
+ * Exported because every path that injects messages into a turn needs it — turn
28
+ * history, and host steer injects mid-turn. A second copy would drift.
29
+ */
30
+ declare function sanitizeHistory(history: import('../kernel/types.js').TurnHistoryMessage[], options: DetectionOptions, hits?: GuardrailHit[]): import('../kernel/types.js').TurnHistoryMessage[];
31
+ /**
32
+ * Detection switches for one profile at one trust level.
33
+ *
34
+ * Falls back to full detection when the profile is not registered yet, so an
35
+ * unknown id never silently disables guardrails.
36
+ */
37
+ declare function detectionForProfile(profileId: string, trust: TrustLevel): DetectionOptions;
16
38
  /** Sanitize all user-controlled text and blobs in a turn request. */
17
39
  declare function sanitizeTurnRequest(req: TurnRequest): NormalizedTurnRequest;
40
+ /**
41
+ * Sanitize a turn request and return guardrail events for any redactionsactions spans.
42
+ * Attachments/voice are validated but do not emit content-span events.
43
+ */
44
+ declare function sanitizeTurnRequestWithEvents(req: TurnRequest): {
45
+ request: NormalizedTurnRequest;
46
+ events: TurnEvent[];
47
+ };
18
48
  /**
19
49
  * Trace-safe request sanitize. Prefers full `sanitizeTurnRequest`; if blob/policy
20
50
  * checks throw, still redacts text and keeps attachments for hashing — never invents empty input.
@@ -23,4 +53,4 @@ declare function sanitizeTurnRequestForTrace(req: TurnRequest): {
23
53
  request: NormalizedTurnRequest;
24
54
  sanitizeError?: string;
25
55
  };
26
- export { PROJECT_ID_MAX, redactSensitiveOnly, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, sanitizeTurnRequestForTrace, };
56
+ export { detectionForProfile, detectText, PROJECT_ID_MAX, redactSensitiveOnly, sanitizeHistory, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, sanitizeTurnRequestForTrace, sanitizeTurnRequestWithEvents, };