@sponsio/sdk 0.1.0-alpha.3 → 0.2.0-alpha.10

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 (243) hide show
  1. package/bin/sponsio.cjs +11 -0
  2. package/contracts/benchmark/agentdojo.yaml +551 -0
  3. package/contracts/benchmark/odcv_bench.yaml +393 -0
  4. package/contracts/benchmark/redcode_exec.yaml +488 -0
  5. package/contracts/benchmark/swebench.yaml +176 -0
  6. package/contracts/benchmark/tau2_bench.yaml +984 -0
  7. package/contracts/capability/credentials.yaml +8 -8
  8. package/contracts/capability/database.yaml +6 -6
  9. package/contracts/capability/destructive.yaml +213 -0
  10. package/contracts/capability/factual-claims.yaml +79 -0
  11. package/contracts/capability/filesystem-strict.yaml +4 -4
  12. package/contracts/capability/filesystem.yaml +9 -9
  13. package/contracts/capability/host-config-integrity.yaml +318 -0
  14. package/contracts/capability/self-modify.yaml +260 -9
  15. package/contracts/capability/shell.yaml +43 -41
  16. package/contracts/capability/subagent.yaml +5 -5
  17. package/contracts/incident/claude-code-secret-bypass.yaml +57 -26
  18. package/contracts/incident/cursor-railway-wipe.yaml +63 -7
  19. package/contracts/incident/mcp-composition.yaml +12 -12
  20. package/contracts/incident/openclaw.yaml +47 -92
  21. package/contracts/incident/subagent-escape.yaml +4 -4
  22. package/contracts/premium/.gitkeep +5 -6
  23. package/dist/cli/check.d.ts +2 -0
  24. package/dist/cli/check.d.ts.map +1 -0
  25. package/dist/cli/check.js +283 -0
  26. package/dist/cli/check.js.map +1 -0
  27. package/dist/cli/cli.d.ts +25 -0
  28. package/dist/cli/cli.d.ts.map +1 -0
  29. package/dist/cli/cli.js +491 -0
  30. package/dist/cli/cli.js.map +1 -0
  31. package/dist/cli/config.d.ts +88 -0
  32. package/dist/cli/config.d.ts.map +1 -0
  33. package/dist/cli/config.js +243 -0
  34. package/dist/cli/config.js.map +1 -0
  35. package/dist/cli/demo.d.ts +12 -0
  36. package/dist/cli/demo.d.ts.map +1 -0
  37. package/dist/cli/demo.js +350 -0
  38. package/dist/cli/demo.js.map +1 -0
  39. package/dist/cli/doctor.d.ts +11 -0
  40. package/dist/cli/doctor.d.ts.map +1 -0
  41. package/dist/cli/doctor.js +201 -0
  42. package/dist/cli/doctor.js.map +1 -0
  43. package/dist/cli/eval.d.ts +2 -0
  44. package/dist/cli/eval.d.ts.map +1 -0
  45. package/dist/cli/eval.js +361 -0
  46. package/dist/cli/eval.js.map +1 -0
  47. package/dist/cli/explain.d.ts +2 -0
  48. package/dist/cli/explain.d.ts.map +1 -0
  49. package/dist/cli/explain.js +266 -0
  50. package/dist/cli/explain.js.map +1 -0
  51. package/dist/cli/export-sessions.d.ts +2 -0
  52. package/dist/cli/export-sessions.d.ts.map +1 -0
  53. package/dist/cli/export-sessions.js +182 -0
  54. package/dist/cli/export-sessions.js.map +1 -0
  55. package/dist/cli/export.d.ts +2 -0
  56. package/dist/cli/export.d.ts.map +1 -0
  57. package/dist/cli/export.js +222 -0
  58. package/dist/cli/export.js.map +1 -0
  59. package/dist/cli/extractors/common.d.ts +27 -0
  60. package/dist/cli/extractors/common.d.ts.map +1 -0
  61. package/dist/cli/extractors/common.js +107 -0
  62. package/dist/cli/extractors/common.js.map +1 -0
  63. package/dist/cli/extractors/generic.d.ts +37 -0
  64. package/dist/cli/extractors/generic.d.ts.map +1 -0
  65. package/dist/cli/extractors/generic.js +185 -0
  66. package/dist/cli/extractors/generic.js.map +1 -0
  67. package/dist/cli/extractors/langchain.d.ts +18 -0
  68. package/dist/cli/extractors/langchain.d.ts.map +1 -0
  69. package/dist/cli/extractors/langchain.js +86 -0
  70. package/dist/cli/extractors/langchain.js.map +1 -0
  71. package/dist/cli/extractors/vercel.d.ts +17 -0
  72. package/dist/cli/extractors/vercel.d.ts.map +1 -0
  73. package/dist/cli/extractors/vercel.js +46 -0
  74. package/dist/cli/extractors/vercel.js.map +1 -0
  75. package/dist/cli/index.d.ts +38 -0
  76. package/dist/cli/index.d.ts.map +1 -0
  77. package/dist/cli/index.js +146 -0
  78. package/dist/cli/index.js.map +1 -0
  79. package/dist/cli/init.d.ts +2 -0
  80. package/dist/cli/init.d.ts.map +1 -0
  81. package/dist/cli/init.js +177 -0
  82. package/dist/cli/init.js.map +1 -0
  83. package/dist/cli/mode.d.ts +2 -0
  84. package/dist/cli/mode.d.ts.map +1 -0
  85. package/dist/cli/mode.js +71 -0
  86. package/dist/cli/mode.js.map +1 -0
  87. package/dist/cli/onboard.d.ts +88 -0
  88. package/dist/cli/onboard.d.ts.map +1 -0
  89. package/dist/cli/onboard.js +758 -0
  90. package/dist/cli/onboard.js.map +1 -0
  91. package/dist/cli/otlp.d.ts +27 -0
  92. package/dist/cli/otlp.d.ts.map +1 -0
  93. package/dist/cli/otlp.js +79 -0
  94. package/dist/cli/otlp.js.map +1 -0
  95. package/dist/cli/package.json +1 -0
  96. package/dist/cli/packs.d.ts +16 -0
  97. package/dist/cli/packs.d.ts.map +1 -0
  98. package/dist/cli/packs.js +190 -0
  99. package/dist/cli/packs.js.map +1 -0
  100. package/dist/cli/patterns.d.ts +16 -0
  101. package/dist/cli/patterns.d.ts.map +1 -0
  102. package/dist/cli/patterns.js +136 -0
  103. package/dist/cli/patterns.js.map +1 -0
  104. package/dist/cli/prompt.d.ts +2 -0
  105. package/dist/cli/prompt.d.ts.map +1 -0
  106. package/dist/cli/prompt.js +54 -0
  107. package/dist/cli/prompt.js.map +1 -0
  108. package/dist/cli/replay.d.ts +2 -0
  109. package/dist/cli/replay.d.ts.map +1 -0
  110. package/dist/cli/replay.js +196 -0
  111. package/dist/cli/replay.js.map +1 -0
  112. package/dist/cli/report.d.ts +57 -0
  113. package/dist/cli/report.d.ts.map +1 -0
  114. package/dist/cli/report.js +298 -0
  115. package/dist/cli/report.js.map +1 -0
  116. package/dist/cli/scan.d.ts +2 -0
  117. package/dist/cli/scan.d.ts.map +1 -0
  118. package/dist/cli/scan.js +172 -0
  119. package/dist/cli/scan.js.map +1 -0
  120. package/dist/cli/skill.d.ts +15 -0
  121. package/dist/cli/skill.d.ts.map +1 -0
  122. package/dist/cli/skill.js +131 -0
  123. package/dist/cli/skill.js.map +1 -0
  124. package/dist/cli/tokens.d.ts +31 -0
  125. package/dist/cli/tokens.d.ts.map +1 -0
  126. package/dist/cli/tokens.js +73 -0
  127. package/dist/cli/tokens.js.map +1 -0
  128. package/dist/cli/types.d.ts +46 -0
  129. package/dist/cli/types.d.ts.map +1 -0
  130. package/dist/cli/types.js +3 -0
  131. package/dist/cli/types.js.map +1 -0
  132. package/dist/cli/validate.d.ts +19 -0
  133. package/dist/cli/validate.d.ts.map +1 -0
  134. package/dist/cli/validate.js +205 -0
  135. package/dist/cli/validate.js.map +1 -0
  136. package/dist/cli/zod.d.ts +36 -0
  137. package/dist/cli/zod.d.ts.map +1 -0
  138. package/dist/cli/zod.js +226 -0
  139. package/dist/cli/zod.js.map +1 -0
  140. package/dist/cloud/client.d.ts +76 -0
  141. package/dist/cloud/client.d.ts.map +1 -0
  142. package/dist/cloud/client.js +204 -0
  143. package/dist/cloud/client.js.map +1 -0
  144. package/dist/cloud/evidence.d.ts +108 -0
  145. package/dist/cloud/evidence.d.ts.map +1 -0
  146. package/dist/cloud/evidence.js +184 -0
  147. package/dist/cloud/evidence.js.map +1 -0
  148. package/dist/cloud/privacy.d.ts +67 -0
  149. package/dist/cloud/privacy.d.ts.map +1 -0
  150. package/dist/cloud/privacy.js +170 -0
  151. package/dist/cloud/privacy.js.map +1 -0
  152. package/dist/cloud/view-model.d.ts +49 -0
  153. package/dist/cloud/view-model.d.ts.map +1 -0
  154. package/dist/cloud/view-model.js +172 -0
  155. package/dist/cloud/view-model.js.map +1 -0
  156. package/dist/contract.d.ts +13 -13
  157. package/dist/contract.d.ts.map +1 -1
  158. package/dist/contract.js +21 -21
  159. package/dist/contract.js.map +1 -1
  160. package/dist/core/config-loader.d.ts +1 -1
  161. package/dist/core/config-loader.d.ts.map +1 -1
  162. package/dist/core/config-loader.js +116 -26
  163. package/dist/core/config-loader.js.map +1 -1
  164. package/dist/core/evaluator.d.ts +11 -1
  165. package/dist/core/evaluator.d.ts.map +1 -1
  166. package/dist/core/evaluator.js +275 -13
  167. package/dist/core/evaluator.js.map +1 -1
  168. package/dist/core/formula.d.ts +76 -17
  169. package/dist/core/formula.d.ts.map +1 -1
  170. package/dist/core/formula.js +95 -1
  171. package/dist/core/formula.js.map +1 -1
  172. package/dist/core/grounding.d.ts +86 -3
  173. package/dist/core/grounding.d.ts.map +1 -1
  174. package/dist/core/grounding.js +485 -143
  175. package/dist/core/grounding.js.map +1 -1
  176. package/dist/core/nl-parser.d.ts +2 -2
  177. package/dist/core/nl-parser.d.ts.map +1 -1
  178. package/dist/core/nl-parser.js +690 -42
  179. package/dist/core/nl-parser.js.map +1 -1
  180. package/dist/core/pack-loader.d.ts.map +1 -1
  181. package/dist/core/pack-loader.js +14 -2
  182. package/dist/core/pack-loader.js.map +1 -1
  183. package/dist/core/parser.d.ts.map +1 -1
  184. package/dist/core/parser.js +30 -1
  185. package/dist/core/parser.js.map +1 -1
  186. package/dist/core/pattern-factory.d.ts +4 -4
  187. package/dist/core/pattern-factory.d.ts.map +1 -1
  188. package/dist/core/pattern-factory.js +59 -4
  189. package/dist/core/pattern-factory.js.map +1 -1
  190. package/dist/core/patterns.d.ts +196 -5
  191. package/dist/core/patterns.d.ts.map +1 -1
  192. package/dist/core/patterns.js +400 -13
  193. package/dist/core/patterns.js.map +1 -1
  194. package/dist/core/session-log.d.ts.map +1 -1
  195. package/dist/core/session-log.js +20 -1
  196. package/dist/core/session-log.js.map +1 -1
  197. package/dist/core/spans.d.ts +1 -1
  198. package/dist/core/spans.d.ts.map +1 -1
  199. package/dist/core/spans.js.map +1 -1
  200. package/dist/core/sto.d.ts +30 -66
  201. package/dist/core/sto.d.ts.map +1 -1
  202. package/dist/core/sto.js +39 -100
  203. package/dist/core/sto.js.map +1 -1
  204. package/dist/core/tool-names.d.ts +59 -0
  205. package/dist/core/tool-names.d.ts.map +1 -0
  206. package/dist/core/tool-names.js +128 -0
  207. package/dist/core/tool-names.js.map +1 -0
  208. package/dist/index.d.ts +227 -33
  209. package/dist/index.d.ts.map +1 -1
  210. package/dist/index.js +539 -152
  211. package/dist/index.js.map +1 -1
  212. package/dist/integrations/claude-agent.d.ts +5 -0
  213. package/dist/integrations/claude-agent.d.ts.map +1 -1
  214. package/dist/integrations/claude-agent.js +42 -13
  215. package/dist/integrations/claude-agent.js.map +1 -1
  216. package/dist/integrations/google-adk.d.ts +5 -3
  217. package/dist/integrations/google-adk.d.ts.map +1 -1
  218. package/dist/integrations/google-adk.js +9 -5
  219. package/dist/integrations/google-adk.js.map +1 -1
  220. package/dist/integrations/langchain.d.ts +7 -0
  221. package/dist/integrations/langchain.d.ts.map +1 -1
  222. package/dist/integrations/langchain.js +21 -3
  223. package/dist/integrations/langchain.js.map +1 -1
  224. package/dist/integrations/openai-agents.d.ts +22 -10
  225. package/dist/integrations/openai-agents.d.ts.map +1 -1
  226. package/dist/integrations/openai-agents.js +57 -16
  227. package/dist/integrations/openai-agents.js.map +1 -1
  228. package/dist/integrations/openai.d.ts +11 -0
  229. package/dist/integrations/openai.d.ts.map +1 -1
  230. package/dist/integrations/openai.js +130 -48
  231. package/dist/integrations/openai.js.map +1 -1
  232. package/dist/integrations/vercel-ai.d.ts +20 -9
  233. package/dist/integrations/vercel-ai.d.ts.map +1 -1
  234. package/dist/integrations/vercel-ai.js +131 -32
  235. package/dist/integrations/vercel-ai.js.map +1 -1
  236. package/dist/render/tokens.d.ts.map +1 -1
  237. package/dist/render/tokens.js +1 -0
  238. package/dist/render/tokens.js.map +1 -1
  239. package/package.json +41 -9
  240. package/prompts/onboard.md +259 -0
  241. package/prompts/scan.md +123 -0
  242. package/skills/SKILL.md +432 -0
  243. package/contracts/core/llm_safety.yaml +0 -130
@@ -18,24 +18,31 @@
18
18
  * auditAfter, approvalFreshness, sanitizedBeforeSink, duplicateCallLimit
19
19
  */
20
20
  import { Atom, Not, And, Or, Implies, G, F, X, U, Le, Ge, Var, Const, } from "./formula.js";
21
+ import { canonicalTool } from "./tool-names.js";
21
22
  // --- Helpers ---
23
+ //
24
+ // Tool names are canonicalised (see core/tool-names.ts) so a rule keys on
25
+ // the same spelling grounding emits for every variant of the call:
26
+ // ``Issue_Refund``, ``"issue_refund "`` and ``mcp__finance__issue_refund``
27
+ // all bind a rule written on ``issue_refund``. Parity with Python's
28
+ // ``_called`` / ``_count_var`` / ``_physical_tool``.
22
29
  function called(tool) {
23
30
  // Supports "tool:pattern" format — produces called_with atom.
24
31
  if (tool.includes(":")) {
25
32
  const [physical, pattern] = tool.split(":", 2);
26
- return new Atom("called_with", [physical, pattern]);
33
+ return new Atom("called_with", [canonicalTool(physical), pattern]);
27
34
  }
28
- return new Atom("called", [tool]);
35
+ return new Atom("called", [canonicalTool(tool)]);
29
36
  }
30
37
  function countVar(tool) {
31
38
  if (tool.includes(":")) {
32
39
  const [physical, pattern] = tool.split(":", 2);
33
- return new Var("count_with", physical, pattern);
40
+ return new Var("count_with", canonicalTool(physical), pattern);
34
41
  }
35
- return new Var("count", tool);
42
+ return new Var("count", canonicalTool(tool));
36
43
  }
37
44
  function physicalTool(tool) {
38
- return tool.includes(":") ? tool.split(":", 1)[0] : tool;
45
+ return canonicalTool(tool.includes(":") ? tool.split(":", 1)[0] : tool);
39
46
  }
40
47
  /**
41
48
  * Reject empty / whitespace-only tool names at factory time.
@@ -112,6 +119,7 @@ export function mustPrecede(before, after) {
112
119
  formula: f,
113
120
  desc: `tool \`${before}\` must precede \`${after}\``,
114
121
  patternName: "must_precede",
122
+ args: [before, after],
115
123
  liveness: false,
116
124
  };
117
125
  }
@@ -122,9 +130,38 @@ export function alwaysFollowedBy(trigger, response) {
122
130
  formula: f,
123
131
  desc: `\`${trigger}\` must always be followed by \`${response}\``,
124
132
  patternName: "always_followed_by",
133
+ args: [trigger, response],
125
134
  liveness: true,
126
135
  };
127
136
  }
137
+ /**
138
+ * Prescriptive next-step obligation: ``G(trigger -> X(next_action))``.
139
+ *
140
+ * When ``trigger`` holds at the current event, the agent must satisfy
141
+ * ``next_action`` at the very next event. Prescriptive counterpart to
142
+ * the block-style patterns (``never_together``, ``arg_blacklist``):
143
+ * instead of "you must not do X", a workflow_step says "you must do X
144
+ * next".
145
+ *
146
+ * Both arguments are arbitrary atoms (called(...), ctx(k, v),
147
+ * arg_field_has(...), etc.), so the same factory covers tool ordering,
148
+ * ctx-driven remediation, and arg-conditional follow-ups.
149
+ *
150
+ * NOT marked liveness: X is one-step bounded and the runtime can decide
151
+ * the obligation after a single event, unlike F.
152
+ */
153
+ export function workflowStep(trigger, nextAction, desc) {
154
+ const f = new G(new Implies(trigger, new X(nextAction)));
155
+ const triggerStr = `${trigger.predicate}(${trigger.args.join(", ")})`;
156
+ const nextStr = `${nextAction.predicate}(${nextAction.args.join(", ")})`;
157
+ return {
158
+ formula: f,
159
+ desc: desc ?? `after ${triggerStr} the next event must satisfy ${nextStr}`,
160
+ patternName: "workflow_step",
161
+ args: [trigger, nextAction],
162
+ liveness: false,
163
+ };
164
+ }
128
165
  export function noReversal(commitment, contradiction) {
129
166
  ensureDistinct(commitment, contradiction, "noReversal", "commitment", "contradiction");
130
167
  const f = new G(new Implies(called(commitment), new G(new Not(called(contradiction)))));
@@ -132,6 +169,7 @@ export function noReversal(commitment, contradiction) {
132
169
  formula: f,
133
170
  desc: `cannot call \`${contradiction}\` after \`${commitment}\``,
134
171
  patternName: "no_reversal",
172
+ args: [commitment, contradiction],
135
173
  liveness: false,
136
174
  };
137
175
  }
@@ -141,6 +179,7 @@ export function requiresPermission(tool, permission) {
141
179
  formula: f,
142
180
  desc: `\`${tool}\` requires permission \`${permission}\``,
143
181
  patternName: "requires_permission",
182
+ args: [tool, permission],
144
183
  liveness: false,
145
184
  };
146
185
  }
@@ -151,6 +190,7 @@ export function noDataLeak(source, external) {
151
190
  formula: f,
152
191
  desc: `no data leak from \`${source}\` to \`${external}\``,
153
192
  patternName: "no_data_leak",
193
+ args: [source, external],
154
194
  liveness: false,
155
195
  };
156
196
  }
@@ -161,6 +201,7 @@ export function mutualExclusion(a, b) {
161
201
  formula: f,
162
202
  desc: `tools \`${a}\` and \`${b}\` are mutually exclusive`,
163
203
  patternName: "mutual_exclusion",
204
+ args: [a, b],
164
205
  liveness: false,
165
206
  };
166
207
  }
@@ -170,11 +211,20 @@ export function rateLimit(tool, maxCalls) {
170
211
  formula: f,
171
212
  desc: `tool \`${tool}\` at most ${maxCalls} times`,
172
213
  patternName: "rate_limit",
214
+ args: [tool, maxCalls],
173
215
  liveness: false,
174
216
  };
175
217
  }
176
218
  export function idempotent(tool) {
177
- return { ...rateLimit(tool, 1), patternName: "idempotent", desc: `\`${tool}\` at most once` };
219
+ // The spread would otherwise carry rate_limit's own args (tool, 1) into
220
+ // a rule whose identity is one tool, and the cloud groups findings by
221
+ // (patternName, args).
222
+ return {
223
+ ...rateLimit(tool, 1),
224
+ patternName: "idempotent",
225
+ args: [tool],
226
+ desc: `\`${tool}\` at most once`,
227
+ };
178
228
  }
179
229
  export function deadline(trigger, action, steps) {
180
230
  ensureDistinct(trigger, action, "deadline", "trigger", "action");
@@ -187,7 +237,12 @@ export function deadline(trigger, action, steps) {
187
237
  formula: f,
188
238
  desc: `\`${action}\` must occur within ${steps} steps of \`${trigger}\``,
189
239
  patternName: "deadline",
190
- liveness: true,
240
+ args: [trigger, action, steps],
241
+ // Bounded-window liveness: ``X(boundedEventually(action, steps))``
242
+ // is decidable on a bounded prefix (steps + 1 events past the
243
+ // trigger), so it CAN fire mid-session — runtime parity with
244
+ // Python ``deadline.liveness == False``.
245
+ liveness: false,
191
246
  };
192
247
  }
193
248
  export function mustConfirm(action) {
@@ -197,6 +252,7 @@ export function mustConfirm(action) {
197
252
  formula: f,
198
253
  desc: `\`${action}\` requires confirmation (\`${confirm}\`)`,
199
254
  patternName: "must_confirm",
255
+ args: [action],
200
256
  liveness: false,
201
257
  };
202
258
  }
@@ -206,6 +262,7 @@ export function cooldown(action, steps) {
206
262
  formula: f,
207
263
  desc: `\`${action}\` has a cooldown of ${steps} steps`,
208
264
  patternName: "cooldown",
265
+ args: [action, steps],
209
266
  liveness: false,
210
267
  };
211
268
  }
@@ -217,6 +274,7 @@ export function segregationOfDuty(a, b) {
217
274
  return {
218
275
  ...me,
219
276
  patternName: "segregation_of_duty",
277
+ args: [a, b],
220
278
  desc: `\`${a}\` and \`${b}\` must be performed by different agents`,
221
279
  };
222
280
  }
@@ -224,8 +282,9 @@ export function boundedRetry(action, maxRetries) {
224
282
  const f = new G(new Le(countVar(action), new Const(maxRetries)));
225
283
  return {
226
284
  formula: f,
227
- desc: `\`${action}\` limited to ${maxRetries} retries`,
285
+ desc: `\`${action}\` limited to ${maxRetries} ${maxRetries === 1 ? "retry" : "retries"}`,
228
286
  patternName: "bounded_retry",
287
+ args: [action, maxRetries],
229
288
  liveness: false,
230
289
  };
231
290
  }
@@ -236,6 +295,7 @@ export function loopDetection(action, maxConsecutive) {
236
295
  formula: f,
237
296
  desc: `\`${action}\` max ${maxConsecutive} consecutive calls`,
238
297
  patternName: "loop_detection",
298
+ args: [action, maxConsecutive],
239
299
  liveness: false,
240
300
  };
241
301
  }
@@ -246,6 +306,7 @@ export function dryRunBeforeCommit(dryRun, commit) {
246
306
  ...base,
247
307
  desc: `\`${dryRun}\` dry-run must precede \`${commit}\``,
248
308
  patternName: "dry_run_before_commit",
309
+ args: [dryRun, commit],
249
310
  };
250
311
  }
251
312
  export function backupBeforeDestructive(backup, action) {
@@ -254,6 +315,7 @@ export function backupBeforeDestructive(backup, action) {
254
315
  ...base,
255
316
  desc: `\`${backup}\` backup must precede destructive action \`${action}\``,
256
317
  patternName: "backup_before_destructive",
318
+ args: [backup, action],
257
319
  };
258
320
  }
259
321
  export function auditAfter(action, audit) {
@@ -262,6 +324,7 @@ export function auditAfter(action, audit) {
262
324
  ...base,
263
325
  desc: `\`${action}\` must be followed by audit step \`${audit}\``,
264
326
  patternName: "audit_after",
327
+ args: [action, audit],
265
328
  liveness: true,
266
329
  };
267
330
  }
@@ -278,6 +341,7 @@ export function approvalFreshness(approval, action, steps) {
278
341
  formula: f,
279
342
  desc: `\`${action}\` requires approval \`${approval}\` within ${steps} steps`,
280
343
  patternName: "approval_freshness",
344
+ args: [approval, action, steps],
281
345
  liveness: false,
282
346
  };
283
347
  }
@@ -290,6 +354,7 @@ export function sanitizedBeforeSink(source, sanitizer, sink) {
290
354
  formula: f,
291
355
  desc: `after \`${source}\`, \`${sanitizer}\` must precede \`${sink}\``,
292
356
  patternName: "sanitized_before_sink",
357
+ args: [source, sanitizer, sink],
293
358
  liveness: false,
294
359
  };
295
360
  }
@@ -304,6 +369,7 @@ export function duplicateCallLimit(tool, argsPattern, maxCount) {
304
369
  formula: f,
305
370
  desc: `\`${tool}\` calls matching ${JSON.stringify(argsPattern)} at most ${maxCount} times`,
306
371
  patternName: "duplicate_call_limit",
372
+ args: [tool, argsPattern, maxCount],
307
373
  liveness: false,
308
374
  };
309
375
  }
@@ -319,6 +385,7 @@ export function argBlacklist(tool, field, patterns) {
319
385
  formula: f,
320
386
  desc: `\`${tool}\`.${field} must not match ${JSON.stringify(patterns)}`,
321
387
  patternName: "arg_blacklist",
388
+ args: [tool, field, patterns],
322
389
  liveness: false,
323
390
  };
324
391
  }
@@ -338,6 +405,7 @@ export function argAllowlist(tool, field, patterns) {
338
405
  formula: f,
339
406
  desc: `\`${tool}\`.${field} must match one of ${JSON.stringify(patterns)}`,
340
407
  patternName: "arg_allowlist",
408
+ args: [tool, field, patterns],
341
409
  liveness: false,
342
410
  };
343
411
  }
@@ -348,6 +416,7 @@ export function scopeLimit(tool, allowedPaths) {
348
416
  formula: f,
349
417
  desc: `\`${tool}\` restricted to paths: ${allowedPaths.join(", ")}`,
350
418
  patternName: "scope_limit",
419
+ args: [tool, allowedPaths],
351
420
  liveness: false,
352
421
  };
353
422
  }
@@ -358,6 +427,7 @@ export function argLengthLimit(tool, param, maxChars) {
358
427
  formula: f,
359
428
  desc: `\`${tool}\`.${param} must not exceed ${maxChars} characters`,
360
429
  patternName: "arg_length_limit",
430
+ args: [tool, param, maxChars],
361
431
  liveness: false,
362
432
  };
363
433
  }
@@ -367,6 +437,7 @@ export function dataIntact(boundTool, originalPaths) {
367
437
  formula: f,
368
438
  desc: `\`${boundTool}\` must use only original data from ${originalPaths.join(", ")}`,
369
439
  patternName: "data_intact",
440
+ args: [boundTool, originalPaths],
370
441
  liveness: false,
371
442
  };
372
443
  }
@@ -379,6 +450,7 @@ export function destructiveActionGate(tool, approverRole = "approver") {
379
450
  formula: f,
380
451
  desc: `\`${tool}\` is destructive and requires \`${approverRole}\` approval`,
381
452
  patternName: "destructive_action_gate",
453
+ args: [tool, approverRole],
382
454
  liveness: false,
383
455
  };
384
456
  }
@@ -392,7 +464,7 @@ export function untrustedSourceGate(source, sink, confirm = "") {
392
464
  patternName: "untrusted_source_gate_assumption",
393
465
  liveness: false,
394
466
  },
395
- enforcement: mustPrecede(confirmAction, sink),
467
+ guarantee: mustPrecede(confirmAction, sink),
396
468
  };
397
469
  }
398
470
  export function requiredStepsCompletion(trigger, steps) {
@@ -429,6 +501,7 @@ export function requiredStepsCompletion(trigger, steps) {
429
501
  formula: f,
430
502
  desc: `after \`${trigger}\`, all steps must complete: ${steps.join(", ")}`,
431
503
  patternName: "required_steps_completion",
504
+ args: [trigger, steps],
432
505
  liveness: true,
433
506
  };
434
507
  }
@@ -446,15 +519,111 @@ export function toolAllowlist(allowedTools) {
446
519
  const formula = new G(new Implies(new Atom("called_any"), disjunction));
447
520
  return {
448
521
  formula,
449
- desc: `only allowed tools: ${allowedTools.join(", ")}`,
522
+ desc: `only [${allowedTools.join(", ")}] may be called`,
450
523
  patternName: "tool_allowlist",
524
+ args: [allowedTools],
451
525
  liveness: false,
452
526
  };
453
527
  }
528
+ /**
529
+ * Substitute a forbidden tool call with a pre-approved safe one.
530
+ *
531
+ * Parity with Python ``sponsio.patterns.library.redirect_to_safe``:
532
+ * compiles to ``G(Not(called(unsafe)))``, so any call to ``unsafe`` is
533
+ * a violation.
534
+ *
535
+ * **Important runtime caveat.** The TS SDK is currently det-only and
536
+ * has no strategy / dispatch system. The formula side of
537
+ * ``redirectToSafe`` works here (a violation IS surfaced), but the TS
538
+ * SDK does NOT yet do the Python-side trick of bundling a
539
+ * ``RedirectToSafe`` strategy on the formula and having adapter
540
+ * ``wrap()`` paths substitute the call. The runtime outcome on TS is
541
+ * functionally a block: the unsafe tool is rejected, and the user's
542
+ * application loop has to decide whether to dispatch the safe
543
+ * substitute manually. Full strategy / dispatch parity is tracked as
544
+ * a separate work item. Users wiring redirect-style contracts who
545
+ * need transparent substitution should stay on the Python SDK
546
+ * (LangGraph adapter) until the TS strategy port lands.
547
+ */
548
+ /**
549
+ * Every emitted claim of this predicate must verify as PASS.
550
+ *
551
+ * Compiles to `G(claim_emitted(pred) -> evidence_verdict(pred, PASS))`.
552
+ * Both atoms are grounded from the same `evidence` event, so the
553
+ * implication is decided at the timestep the verdict lands: a claim
554
+ * whose verdict is anything but PASS (MISMATCH, UNDERDETERMINED,
555
+ * NO_EVIDENCE, STALE, SOURCE_UNAVAILABLE, EXTRACTION_AMBIGUOUS)
556
+ * violates immediately.
557
+ *
558
+ * Parity with `sponsio.patterns.library.claim_requires_evidence`.
559
+ */
560
+ export function claimRequiresEvidence(pred, desc) {
561
+ ensureNonEmpty(pred, "claimRequiresEvidence", "pred");
562
+ return {
563
+ formula: new G(new Implies(new Atom("claim_emitted", [pred]), new Atom("evidence_verdict", [pred, "PASS"]))),
564
+ desc: desc || `claims of ${pred} must verify against evidence (PASS)`,
565
+ patternName: "claim_requires_evidence",
566
+ args: [pred],
567
+ liveness: false,
568
+ };
569
+ }
570
+ /**
571
+ * An UNDERDETERMINED verdict must resolve to a clarify action.
572
+ *
573
+ * Compiles to `G(evidence_verdict(pred, UNDERDETERMINED) ->
574
+ * evidence_action(pred, clarify))`.
575
+ *
576
+ * Guards the policy wiring rather than the claim itself: when the
577
+ * evidence was ambiguous, the recorded action on that same event must be
578
+ * `clarify`. A project override that quietly releases or hard-blocks an
579
+ * ambiguous claim trips this contract.
580
+ *
581
+ * Parity with `sponsio.patterns.library.underdetermined_must_clarify`.
582
+ */
583
+ export function underdeterminedMustClarify(pred, desc) {
584
+ ensureNonEmpty(pred, "underdeterminedMustClarify", "pred");
585
+ return {
586
+ formula: new G(new Implies(new Atom("evidence_verdict", [pred, "UNDERDETERMINED"]), new Atom("evidence_action", [pred, "clarify"]))),
587
+ desc: desc || `ambiguous ${pred} claims must be clarified, not released or blocked`,
588
+ patternName: "underdetermined_must_clarify",
589
+ args: [pred],
590
+ liveness: false,
591
+ };
592
+ }
593
+ export function redirectToSafe(unsafe, safe) {
594
+ ensureDistinct(unsafe, safe, "redirectToSafe", "unsafe", "safe");
595
+ return {
596
+ formula: new G(new Not(called(unsafe))),
597
+ desc: `redirect \`${unsafe}\` -> \`${safe}\``,
598
+ patternName: "redirect_to_safe",
599
+ args: [unsafe, safe],
600
+ liveness: false,
601
+ strategy: "redirect",
602
+ safeName: safe,
603
+ };
604
+ }
605
+ /**
606
+ * Recursive ``rm`` in any spelling: ``rm -rf``, ``rm -fr``, ``rm -Rf``,
607
+ * ``rm -r``, ``rm -f -r``, ``rm --recursive --force``, ``rm -v --recursive``.
608
+ * ``count_with`` is a regex over the serialized args, so a literal
609
+ * ``"rm -rf"`` only caught that one spelling. Mirrors
610
+ * ``RM_RECURSIVE_PATTERN`` in ``sponsio/patterns/library.py``; keep the
611
+ * two in sync.
612
+ */
613
+ export const RM_RECURSIVE_PATTERN = "\\brm(?:\\s+(?:-[a-zA-Z]+|--[a-z-]+))*\\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)(?=\\s|$)";
454
614
  export function dangerousBashCommands(forbidden) {
615
+ // Same list and order as the Python default so the two runtimes emit
616
+ // identical descriptions and block the same commands.
455
617
  const defaults = [
456
- "sed -i", "rm -rf", "cp /app/data", "mv /app/data",
457
- "python -c", "chmod", "> /app", "tee /app",
618
+ RM_RECURSIVE_PATTERN,
619
+ "sudo",
620
+ "chmod",
621
+ "sed -i",
622
+ "python -c",
623
+ "> /app",
624
+ "cp /app/data",
625
+ "mv /app/data",
626
+ "tee /app",
458
627
  ];
459
628
  const cmds = forbidden ?? defaults;
460
629
  // G(count_with(bash, cmd) <= 0) for each cmd — AND them all
@@ -467,6 +636,7 @@ export function dangerousBashCommands(forbidden) {
467
636
  formula: f,
468
637
  desc: `bash commands [${cmds.join(", ")}] are banned`,
469
638
  patternName: "dangerous_bash_commands",
639
+ args: [cmds],
470
640
  liveness: false,
471
641
  };
472
642
  }
@@ -509,6 +679,7 @@ export function dangerousSqlVerbs(tool = "execute_sql", forbidden) {
509
679
  formula: f,
510
680
  desc: `\`${tool}\` must not use SQL verbs [${verbs.join(", ")}]`,
511
681
  patternName: "dangerous_sql_verbs",
682
+ args: [tool, verbs],
512
683
  liveness: false,
513
684
  };
514
685
  }
@@ -518,6 +689,7 @@ export function irreversibleOnce(action) {
518
689
  formula: f,
519
690
  desc: `\`${action}\` is irreversible and may be called at most once`,
520
691
  patternName: "irreversible_once",
692
+ args: [action],
521
693
  liveness: false,
522
694
  };
523
695
  }
@@ -529,9 +701,10 @@ export function confirmAfterSource(source, action) {
529
701
  formula: called(source),
530
702
  desc: `\`${source}\` has been called`,
531
703
  patternName: "confirm_after_source_assumption",
704
+ args: [source, action],
532
705
  liveness: false,
533
706
  },
534
- enforcement: mustPrecede(confirm, action),
707
+ guarantee: mustPrecede(confirm, action),
535
708
  };
536
709
  }
537
710
  // --- Resource / delegation patterns ---
@@ -541,6 +714,7 @@ export function tokenBudget(maxTokens, scope = "total") {
541
714
  formula: f,
542
715
  desc: `session ${scope} tokens must not exceed ${maxTokens}`,
543
716
  patternName: "token_budget",
717
+ args: [maxTokens, scope],
544
718
  liveness: false,
545
719
  };
546
720
  }
@@ -570,6 +744,7 @@ export function argValueRange(tool, field, minVal, maxVal) {
570
744
  formula: f,
571
745
  desc: `\`${tool}\`.${field} must be in range ${rangeStr}`,
572
746
  patternName: "arg_value_range",
747
+ args: [tool, field, minVal, maxVal],
573
748
  liveness: false,
574
749
  };
575
750
  }
@@ -579,7 +754,219 @@ export function delegationDepthLimit(maxDepth) {
579
754
  formula: f,
580
755
  desc: `delegation chain must not exceed depth ${maxDepth}`,
581
756
  patternName: "delegation_depth_limit",
757
+ args: [maxDepth],
758
+ liveness: false,
759
+ };
760
+ }
761
+ // --- Layer 3 — Response content patterns ---
762
+ /** Default PII regex set, mirrored from Python ``_DEFAULT_PII_PATTERNS``. */
763
+ const DEFAULT_PII_PATTERNS = {
764
+ ssn: "\\b\\d{3}-\\d{2}-\\d{4}\\b",
765
+ credit_card: "\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b",
766
+ email: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b",
767
+ phone: "\\b(?:\\+?1[-.\\s]?)?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b",
768
+ };
769
+ /**
770
+ * Response length must stay within the given word / character budget.
771
+ *
772
+ * Grounded against ``response_words`` / ``response_chars`` populated on
773
+ * ``llm_response`` events with content. At non-response events both
774
+ * default to 0, so the constraint is vacuously satisfied.
775
+ */
776
+ export function maxLength(opts) {
777
+ const { maxWords, maxChars, desc } = opts;
778
+ if (maxWords == null && maxChars == null) {
779
+ throw new Error("max_length requires maxWords or maxChars");
780
+ }
781
+ const parts = [];
782
+ if (maxWords != null)
783
+ parts.push(new Le(new Var("response_words"), new Const(maxWords)));
784
+ if (maxChars != null)
785
+ parts.push(new Le(new Var("response_chars"), new Const(maxChars)));
786
+ const body = parts.length === 1 ? parts[0] : new And(parts[0], parts[1]);
787
+ const f = new G(body);
788
+ let descStr;
789
+ if (desc)
790
+ descStr = desc;
791
+ else if (maxWords != null && maxChars != null)
792
+ descStr = `response ≤ ${maxWords} words and ≤ ${maxChars} chars`;
793
+ else if (maxWords != null)
794
+ descStr = `response ≤ ${maxWords} words`;
795
+ else
796
+ descStr = `response ≤ ${maxChars} chars`;
797
+ return {
798
+ formula: f,
799
+ desc: descStr,
800
+ patternName: "max_length",
801
+ liveness: false,
802
+ };
803
+ }
804
+ /**
805
+ * Response must not contain regex-detectable PII (SSN, CC, email, phone).
806
+ *
807
+ * Uses the ``llm_said`` grounding atom — the selected default patterns
808
+ * are joined with ``|`` and matched against each ``llm_response`` event.
809
+ * For semantic PII detection (names, contextual identifiers) use a sto
810
+ * atom; this pattern only covers syntactic PII.
811
+ */
812
+ export function noPii(fields) {
813
+ const selected = fields ?? Object.keys(DEFAULT_PII_PATTERNS);
814
+ const unknown = selected.filter((f) => !(f in DEFAULT_PII_PATTERNS));
815
+ if (unknown.length > 0) {
816
+ throw new Error(`unknown PII field(s): ${JSON.stringify(unknown.sort())}. ` +
817
+ `Available: ${JSON.stringify(Object.keys(DEFAULT_PII_PATTERNS).sort())}`);
818
+ }
819
+ const pattern = selected.map((f) => DEFAULT_PII_PATTERNS[f]).join("|");
820
+ const f = new G(new Not(new Atom("llm_said", [pattern])));
821
+ return {
822
+ formula: f,
823
+ desc: `response must not contain PII (${selected.join(", ")})`,
824
+ patternName: "no_pii",
825
+ args: [selected],
826
+ liveness: false,
827
+ };
828
+ }
829
+ /**
830
+ * Response must not contain any of the given keywords. Keywords are
831
+ * escaped and joined into a word-boundary-anchored regex checked
832
+ * against each ``llm_response`` event via ``llm_said``.
833
+ *
834
+ * JS regex doesn't support inline ``(?i)`` flags — we lower-case the
835
+ * regex by expanding letters to ``[aA]``-style char classes, matching
836
+ * Python's ``(?i)`` semantics.
837
+ */
838
+ export function noKeywords(words) {
839
+ if (!words || words.length === 0) {
840
+ throw new Error("no_keywords requires at least one keyword");
841
+ }
842
+ const pattern = `\\b(${words.map((w) => caseInsensitiveLiteral(w)).join("|")})\\b`;
843
+ const f = new G(new Not(new Atom("llm_said", [pattern])));
844
+ return {
845
+ formula: f,
846
+ desc: `response must not contain keywords: ${JSON.stringify(words)}`,
847
+ patternName: "no_keywords",
848
+ args: [words],
582
849
  liveness: false,
583
850
  };
584
851
  }
852
+ // --- Layer 3 — External-fact (ctx) patterns ---
853
+ /**
854
+ * When ``tool`` is called, ``ctx[key]`` must be one of ``allowedValues``.
855
+ * Fail-closed: missing context evaluates to violation, so forgetting
856
+ * to wire ``observeContext`` is loud rather than silent.
857
+ */
858
+ export function ctxRequired(tool, key, allowedValues) {
859
+ ensureNonEmpty(tool, "ctxRequired", "tool");
860
+ ensureNonEmpty(key, "ctxRequired", "key");
861
+ if (!allowedValues || allowedValues.length === 0) {
862
+ throw new Error("ctx_required: 'allowed_values' must not be empty — an empty " +
863
+ "allowlist rejects every call to the tool. Use " +
864
+ "`tool_allowlist([])` if you really want to block everything.");
865
+ }
866
+ const cleanValues = allowedValues.map((v) => String(v));
867
+ let disjunction = new Atom("ctx", [key, cleanValues[0]]);
868
+ for (let i = 1; i < cleanValues.length; i++) {
869
+ disjunction = new Or(disjunction, new Atom("ctx", [key, cleanValues[i]]));
870
+ }
871
+ const f = new G(new Implies(called(tool), disjunction));
872
+ return {
873
+ formula: f,
874
+ desc: `${tool} requires ctx[${key}] ∈ [${cleanValues.join(", ")}]`,
875
+ patternName: "ctx_required",
876
+ args: [tool, key, cleanValues],
877
+ liveness: false,
878
+ };
879
+ }
880
+ /**
881
+ * When ``tool`` is called, ``ctx[key]`` must match the regex ``pattern``.
882
+ * Regex variant of :func:`ctxRequired` for cases where the allowed set
883
+ * is better expressed as a pattern than an exhaustive list.
884
+ */
885
+ export function ctxMatchesRequired(tool, key, pattern) {
886
+ ensureNonEmpty(tool, "ctxMatchesRequired", "tool");
887
+ ensureNonEmpty(key, "ctxMatchesRequired", "key");
888
+ ensureNonEmpty(pattern, "ctxMatchesRequired", "pattern");
889
+ const f = new G(new Implies(called(tool), new Atom("ctx_matches", [key, pattern])));
890
+ return {
891
+ formula: f,
892
+ desc: `${tool} requires ctx[${key}] to match /${pattern}/`,
893
+ patternName: "ctx_matches_required",
894
+ args: [tool, key, pattern],
895
+ liveness: false,
896
+ };
897
+ }
898
+ // --- Time-window patterns (event-clock) ---
899
+ /**
900
+ * Constrain how recently a predicate was last true. The argument is the
901
+ * grounded predicate key string (e.g. ``"called(refund)"`` or
902
+ * ``"ctx(approval.role, alice)"``) — same convention as Python's
903
+ * ``time_since`` factory.
904
+ */
905
+ export function timeSince(predicateKey, maxSeconds) {
906
+ ensureNonEmpty(predicateKey, "timeSince", "predicate_key");
907
+ if (typeof maxSeconds !== "number" || !Number.isFinite(maxSeconds) || maxSeconds < 0) {
908
+ throw new Error(`time_since: max_seconds must be a non-negative number (got ${maxSeconds}).`);
909
+ }
910
+ const f = new G(new Le(new Var("time_since", predicateKey), new Const(maxSeconds)));
911
+ return {
912
+ formula: f,
913
+ desc: `${predicateKey} must have occurred within last ${maxSeconds}s`,
914
+ patternName: "time_since",
915
+ args: [predicateKey, maxSeconds],
916
+ liveness: false,
917
+ };
918
+ }
919
+ /**
920
+ * Gate ``action`` on a recent allow-decision approval from ``role``.
921
+ *
922
+ * Compiles to::
923
+ * G(called(action) → (
924
+ * ctx_matches("approval.role", role)
925
+ * ∧ ctx_matches("approval.decision", "allow")
926
+ * ∧ time_since("ctx(approval.role, role)") ≤ maxSeconds
927
+ * ))
928
+ *
929
+ * Pairs with ``observeApproval({role, decision})`` on the integration
930
+ * side.
931
+ */
932
+ export function approvalActive(action, role, maxSeconds) {
933
+ ensureNonEmpty(action, "approvalActive", "action");
934
+ ensureNonEmpty(role, "approvalActive", "role");
935
+ if (typeof maxSeconds !== "number" || !Number.isFinite(maxSeconds) || maxSeconds < 0) {
936
+ throw new Error(`approval_active: max_seconds must be a non-negative number (got ${maxSeconds}).`);
937
+ }
938
+ // Match Python: ``role_key = f"ctx(approval.role, {role})"`` — exact
939
+ // string used as the time_since target. predKey would escape spaces /
940
+ // commas; keep the unescaped form so the time_since extractor passes
941
+ // the right key through to grounding's ``state.last_ts`` lookup.
942
+ const roleKey = `ctx(approval.role, ${role})`;
943
+ const escaped = role.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
944
+ const body = new And(new And(new Atom("ctx_matches", ["approval.role", escaped]), new Atom("ctx_matches", ["approval.decision", "allow"])), new Le(new Var("time_since", roleKey), new Const(maxSeconds)));
945
+ const f = new G(new Implies(called(action), body));
946
+ return {
947
+ formula: f,
948
+ desc: `${action} requires active ${role} approval (≤${maxSeconds}s old)`,
949
+ patternName: "approval_active",
950
+ args: [action, role, maxSeconds],
951
+ liveness: false,
952
+ };
953
+ }
954
+ /**
955
+ * @deprecated — use :func:`mutualExclusion` instead.
956
+ *
957
+ * In sequential traces, two tool calls are always at different
958
+ * timesteps, so the formula ``G(¬(called(A) ∧ called(B)))`` is
959
+ * trivially satisfied and can never detect violations. Delegates to
960
+ * ``mutualExclusion`` for correct behavior; kept so yaml authored
961
+ * against Python's deprecated entry still loads.
962
+ */
963
+ export function neverTogether(a, b) {
964
+ const me = mutualExclusion(a, b);
965
+ return {
966
+ ...me,
967
+ patternName: "never_together",
968
+ args: [a, b],
969
+ desc: `${a} and ${b} must never occur together`,
970
+ };
971
+ }
585
972
  //# sourceMappingURL=patterns.js.map