@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Grounding: convert tool-call events into per-timestep predicate valuations.
2
+ * Grounding: convert events into per-timestep predicate valuations.
3
3
  *
4
4
  * Port of sponsio/tracer/grounding.py (Det-relevant atoms).
5
5
  *
@@ -16,18 +16,61 @@
16
16
  * arg_paths_within(tool, *prefixes) — all paths in args within prefix set
17
17
  * token_count(type) — cumulative tokens (from event.args.tokens)
18
18
  * delegation_depth — depth of agent-to-agent delegation
19
+ * ctx(key, value) — external fact pushed via observe_context
20
+ * ctx_matches(key, pattern) — current ctx[key] matches regex
21
+ * llm_said(pattern) — llm_response content matches regex
22
+ * response_words / response_chars — response length on llm_response
23
+ * time_since(predicate_key) — event-clock delta since predicate last True
19
24
  */
20
25
  import { predKey } from "./formula.js";
26
+ import { numericString } from "./evaluator.js";
27
+ import { canonicalTool, toolAliases } from "./tool-names.js";
21
28
  export function newGroundingState() {
22
29
  return {
23
30
  callCounts: {},
24
31
  callWithCounts: {},
25
32
  lastTool: null,
33
+ lastToolAliases: [],
26
34
  consecutiveCounts: {},
27
35
  tokenCounts: {},
28
36
  delegationDepth: 0,
37
+ currentCtx: {},
38
+ now: 0,
39
+ lastTs: {},
40
+ trueAtPrev: new Set(),
29
41
  };
30
42
  }
43
+ /**
44
+ * Predicates whose grounding requires the formula AST to tell
45
+ * grounding which arg tuples to check (regex on content, ctx_matches,
46
+ * time_since key targets, …). Mirrors Python's ``_CONTENT_PREDICATES``.
47
+ */
48
+ const CONTENT_PREDICATES = new Set([
49
+ "called_with",
50
+ "count_with",
51
+ "arg_has",
52
+ "arg_field_has",
53
+ "arg_length_exceeds",
54
+ "arg_paths_within",
55
+ "arg_numeric",
56
+ "llm_said",
57
+ "ctx_matches",
58
+ "time_since",
59
+ ]);
60
+ /**
61
+ * Var names that must be extracted from the formula tree for grounding.
62
+ * Includes both the atom-style content predicates and bare arithmetic
63
+ * Vars so ``Le(Var(count, x), Const(N))`` carries ``("x",)`` through.
64
+ */
65
+ const VAR_PREDICATES = new Set([
66
+ "count_with",
67
+ "count",
68
+ "consecutive_count",
69
+ "arg_numeric",
70
+ "token_count",
71
+ "delegation_depth",
72
+ "time_since",
73
+ ]);
31
74
  /**
32
75
  * Collect content atoms from a formula tree.
33
76
  * These are atoms whose grounding depends on event content (arg patterns).
@@ -37,20 +80,14 @@ export function collectContentAtoms(formulas) {
37
80
  function walk(node) {
38
81
  if (node.kind === "Atom") {
39
82
  const pred = node.predicate;
40
- if ([
41
- "called_with", "count_with",
42
- "arg_has", "arg_field_has", "arg_length_exceeds", "arg_paths_within",
43
- ].includes(pred)) {
83
+ if (CONTENT_PREDICATES.has(pred)) {
44
84
  if (!atoms[pred])
45
85
  atoms[pred] = new Set();
46
86
  atoms[pred].add(node.args.join("|"));
47
87
  }
48
88
  }
49
89
  else if (node.kind === "Var") {
50
- if ([
51
- "count_with", "count", "consecutive_count",
52
- "arg_numeric", "token_count", "delegation_depth",
53
- ].includes(node.name)) {
90
+ if (VAR_PREDICATES.has(node.name)) {
54
91
  const pred = node.name;
55
92
  if (!atoms[pred])
56
93
  atoms[pred] = new Set();
@@ -69,172 +106,439 @@ export function collectContentAtoms(formulas) {
69
106
  walk(f);
70
107
  return atoms;
71
108
  }
109
+ function escapeRegexLiteral(literal) {
110
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ }
112
+ /**
113
+ * Ground a single event into a predicate valuation dict.
114
+ *
115
+ * Routes by ``event.event_type``:
116
+ * - ``tool_call`` (default) — emits the full tool-call atom set
117
+ * - ``llm_response`` — emits ``llm_said(pattern)`` + length atoms
118
+ * - ``context_update`` — merges ``event.args`` into ``state.currentCtx``
119
+ *
120
+ * Ctx atoms (``ctx(k, v)``), accumulator snapshots, and ``time_since``
121
+ * fire on every event regardless of type (matching Python parity).
122
+ */
123
+ /**
124
+ * Is `path` inside `prefix`, as directories rather than as text?
125
+ *
126
+ * A plain `startsWith` answered two different questions wrong. It let
127
+ * `/safe/../etc/passwd` through, because the string does begin with
128
+ * `/safe` — the oldest escape there is, against a rule whose whole
129
+ * purpose is confinement. And it counted `/safeguard/evil.txt` as inside
130
+ * `/safe`, because a prefix of the text is not a prefix of the path.
131
+ *
132
+ * Parity with Python's `_path_within` in `sponsio/tracer/grounding.py`.
133
+ */
134
+ function normalizePosix(input) {
135
+ const text = input.replace(/\\/g, "/");
136
+ const absolute = text.startsWith("/");
137
+ const out = [];
138
+ for (const segment of text.split("/")) {
139
+ if (segment === "" || segment === ".")
140
+ continue;
141
+ if (segment === "..") {
142
+ if (out.length && out[out.length - 1] !== "..")
143
+ out.pop();
144
+ else if (!absolute)
145
+ out.push("..");
146
+ continue;
147
+ }
148
+ out.push(segment);
149
+ }
150
+ const joined = out.join("/");
151
+ if (absolute)
152
+ return "/" + joined;
153
+ return joined || ".";
154
+ }
155
+ export function pathWithin(path, prefix) {
156
+ const normalized = normalizePosix(path);
157
+ const root = normalizePosix(prefix);
158
+ if (root === "/")
159
+ return normalized.startsWith("/");
160
+ return normalized === root || normalized.startsWith(root + "/");
161
+ }
162
+ /**
163
+ * Fail at construction on a contract pattern that is not a valid regular
164
+ * expression. Mirrors Python, where a malformed contract raises when the
165
+ * guard is built. Without this an ``llm_said`` / ``ctx_matches`` pattern
166
+ * that never compiles silently never matches (fail-open), and an
167
+ * ``arg_*`` pattern throws on the first tool call instead of at startup.
168
+ *
169
+ * Patterns are stored as ``args.join("|")``; each predicate knows which
170
+ * tail of that string is the regex, exactly as ``groundEvent`` reads it.
171
+ */
172
+ export function validateContentPatterns(atoms) {
173
+ const regexTail = {
174
+ called_with: 1, // (tool, pattern)
175
+ count_with: 1, // (tool, pattern)
176
+ arg_has: 1, // (tool, pattern)
177
+ arg_field_has: 2, // (tool, field, pattern)
178
+ llm_said: 0, // (pattern)
179
+ ctx_matches: 1, // (key, pattern)
180
+ };
181
+ for (const [pred, skip] of Object.entries(regexTail)) {
182
+ const set = atoms[pred];
183
+ if (!set)
184
+ continue;
185
+ for (const raw of set) {
186
+ const parts = raw.split("|");
187
+ if (parts.length <= skip)
188
+ continue;
189
+ const pattern = parts.slice(skip).join("|");
190
+ try {
191
+ new RegExp(pattern);
192
+ }
193
+ catch (err) {
194
+ const detail = err instanceof Error ? err.message : String(err);
195
+ throw new Error(`sponsio: contract pattern for ${pred} is not a valid regular ` +
196
+ `expression: ${JSON.stringify(pattern)} (${detail})`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ /**
202
+ * Read a tool argument as a number, the way a numeric cap needs it.
203
+ *
204
+ * ``"$5,000"``, ``"5,000"`` and ``"5000 USD"`` are the shapes a model
205
+ * writes for an amount. ``parseFloat`` read ``"5,000"`` as ``5`` and
206
+ * ``"$5,000"`` as nothing at all, and the evaluator reads a missing
207
+ * ``arg_numeric`` as ``0``, so ``argValueRange`` waved them through a cap
208
+ * that stopped ``5000``. The normalisation is the one ordered comparisons
209
+ * already use (``numericString`` in core/evaluator.ts), so both paths
210
+ * agree. Parity with Python ``numeric_arg`` in sponsio/tracer/grounding.py:
211
+ * a boolean reads as 0/1, a number as itself, anything else as undefined.
212
+ */
213
+ export function numericArg(value) {
214
+ if (typeof value === "boolean")
215
+ return value ? 1 : 0;
216
+ if (typeof value === "number")
217
+ return value;
218
+ if (typeof value === "string") {
219
+ const n = numericString(value);
220
+ return n === null ? undefined : n;
221
+ }
222
+ return undefined;
223
+ }
72
224
  /**
73
- * Ground a single tool-call event into a predicate valuation dict.
225
+ * Does a contract's target tool name the tool this event called?
226
+ *
227
+ * The target is canonicalised and looked up among every spelling the
228
+ * event answers to (see core/tool-names.ts), so a rule on ``issue_refund``
229
+ * binds ``Issue_Refund``, ``"issue_refund "`` and
230
+ * ``mcp__finance__issue_refund``. Parity with Python ``_tool_matches``
231
+ * for plain names; the ``tool:argpattern`` form is split by the pattern
232
+ * factories before it reaches grounding in this runtime.
74
233
  */
234
+ export function toolMatches(targetTool, aliases) {
235
+ const target = canonicalTool(targetTool);
236
+ for (const a of aliases)
237
+ if (a === target)
238
+ return true;
239
+ return false;
240
+ }
75
241
  export function groundEvent(event, state, contentAtoms) {
76
242
  const v = {};
77
- const tool = event.tool;
78
- const argsStr = event.args ? JSON.stringify(event.args) : "";
79
- // called(tool)
80
- v[predKey("called", tool)] = true;
81
- // called_any — fires whenever any tool is invoked. Used by
82
- // ``tool_allowlist`` to encode "whenever any tool runs, it must
83
- // be one of the allowed ones".
84
- v[predKey("called_any")] = true;
85
- // count(tool)
86
- state.callCounts[tool] = (state.callCounts[tool] || 0) + 1;
87
- v[predKey("count", tool)] = state.callCounts[tool];
88
- // consecutive_count(tool)
89
- if (tool === state.lastTool) {
90
- state.consecutiveCounts[tool] = (state.consecutiveCounts[tool] || 1) + 1;
243
+ const eventType = event.event_type ?? "tool_call";
244
+ // Advance the event clock (mirrors ``state.now = float(event.ts)``).
245
+ if (typeof event.ts === "number") {
246
+ state.now = event.ts;
91
247
  }
92
248
  else {
93
- if (state.lastTool)
94
- state.consecutiveCounts[state.lastTool] = 0;
95
- state.consecutiveCounts[tool] = 1;
249
+ state.now += 1;
96
250
  }
97
- state.lastTool = tool;
98
- v[predKey("consecutive_count", tool)] = state.consecutiveCounts[tool];
99
- // called_with / count_with — regex on serialized args
100
- if (contentAtoms) {
101
- const cwPatterns = new Set();
102
- for (const key of ["called_with", "count_with"]) {
103
- const s = contentAtoms[key];
104
- if (s)
105
- for (const p of s)
106
- cwPatterns.add(p);
251
+ if (eventType === "tool_call") {
252
+ const tool = event.tool;
253
+ const argsStr = event.args ? JSON.stringify(event.args) : "";
254
+ // called(tool) — grounded under every spelling a contract could
255
+ // reasonably have named it by: exact, whitespace-stripped, case-folded,
256
+ // and the bare name behind an ``mcp__server__`` prefix. A rule written
257
+ // against ``issue_refund`` was otherwise inert against
258
+ // ``mcp__finance__issue_refund``, and with ``mustPrecede`` compiling to
259
+ // ``Or(order-holds, never-called)`` that read as "satisfied".
260
+ // Canonical spellings only: ``predKey`` collapses the case variants of
261
+ // a tool-keyed predicate, so a per-raw-spelling counter would have two
262
+ // entries landing on one key, the snapshot loop writing whichever came
263
+ // last.
264
+ const aliases = [...new Set(toolAliases(tool).map(canonicalTool))];
265
+ for (const alias of aliases)
266
+ v[predKey("called", alias)] = true;
267
+ // called_any — fires whenever any tool is invoked. Used by
268
+ // ``tool_allowlist`` to encode "whenever any tool runs, it must
269
+ // be one of the allowed ones".
270
+ v[predKey("called_any")] = true;
271
+ // count(tool) — one counter per alias, so ``rateLimit("issue_refund")``
272
+ // counts the MCP-prefixed and differently-cased calls too.
273
+ for (const alias of aliases) {
274
+ state.callCounts[alias] = (state.callCounts[alias] || 0) + 1;
275
+ v[predKey("count", alias)] = state.callCounts[alias];
107
276
  }
108
- for (const raw of cwPatterns) {
109
- const parts = raw.split("|");
110
- if (parts.length >= 2) {
111
- const targetTool = parts[0];
112
- const pattern = parts.slice(1).join("|");
113
- if (targetTool === tool) {
114
- const matched = new RegExp(pattern).test(argsStr);
115
- v[predKey("called_with", targetTool, pattern)] = matched;
116
- if (matched) {
117
- const cwKey = `${targetTool}|${pattern}`;
118
- state.callWithCounts[cwKey] = (state.callWithCounts[cwKey] || 0) + 1;
119
- }
120
- v[predKey("count_with", targetTool, pattern)] =
121
- state.callWithCounts[`${targetTool}|${pattern}`] || 0;
122
- }
277
+ // consecutive_count(tool) — keyed per alias like ``count``; "same tool
278
+ // again" is decided on the canonical form.
279
+ // ``search`` then ``mcp__web__search`` is the same tool again: the run
280
+ // continues when the alias sets overlap, not only on equal canonicals.
281
+ const canonical = canonicalTool(tool);
282
+ const lastAliases = new Set(state.lastToolAliases);
283
+ if (canonical === state.lastTool || aliases.some((a) => lastAliases.has(a))) {
284
+ for (const alias of aliases) {
285
+ state.consecutiveCounts[alias] = (state.consecutiveCounts[alias] || 1) + 1;
286
+ }
287
+ }
288
+ else {
289
+ for (const stale of state.lastToolAliases)
290
+ state.consecutiveCounts[stale] = 0;
291
+ for (const alias of aliases)
292
+ state.consecutiveCounts[alias] = 1;
293
+ }
294
+ state.lastTool = canonical;
295
+ state.lastToolAliases = aliases;
296
+ for (const alias of aliases) {
297
+ v[predKey("consecutive_count", alias)] = state.consecutiveCounts[alias];
298
+ }
299
+ // arg_value(tool, field) — raw value for Term-based lookups.
300
+ // ``ArgValue(tool, field)`` reads state[predKey("arg_value", tool,
301
+ // field)] for runtime comparisons (e.g. Eq(ArgValue(tool, "amount"),
302
+ // CtxValue("approved_amount"))). Emit unconditionally for every arg
303
+ // field, every tool_call — Terms read by direct key lookup, not via
304
+ // contentAtoms extraction.
305
+ // Written under every alias (``predKey`` collapses the case variants),
306
+ // so ``ArgValue("issue_refund", "amount")`` reads a call that arrived as
307
+ // ``mcp__finance__issue_refund``.
308
+ if (event.args) {
309
+ for (const [field, val] of Object.entries(event.args)) {
310
+ for (const alias of aliases)
311
+ v[predKey("arg_value", alias, field)] = val;
123
312
  }
124
313
  }
125
- // arg_has(tool, pattern)
126
- const argHasPatterns = contentAtoms["arg_has"];
127
- if (argHasPatterns && argsStr) {
128
- for (const raw of argHasPatterns) {
314
+ // called_with / count_with — regex on serialized args
315
+ if (contentAtoms) {
316
+ const cwPatterns = new Set();
317
+ for (const key of ["called_with", "count_with"]) {
318
+ const s = contentAtoms[key];
319
+ if (s)
320
+ for (const p of s)
321
+ cwPatterns.add(p);
322
+ }
323
+ for (const raw of cwPatterns) {
129
324
  const parts = raw.split("|");
130
325
  if (parts.length >= 2) {
131
326
  const targetTool = parts[0];
132
327
  const pattern = parts.slice(1).join("|");
133
- if (targetTool === tool) {
134
- v[predKey("arg_has", targetTool, pattern)] = new RegExp(pattern).test(argsStr);
328
+ if (toolMatches(targetTool, aliases)) {
329
+ const matched = new RegExp(pattern).test(argsStr);
330
+ v[predKey("called_with", targetTool, pattern)] = matched;
331
+ if (matched) {
332
+ const cwKey = `${targetTool}|${pattern}`;
333
+ state.callWithCounts[cwKey] = (state.callWithCounts[cwKey] || 0) + 1;
334
+ }
335
+ v[predKey("count_with", targetTool, pattern)] =
336
+ state.callWithCounts[`${targetTool}|${pattern}`] || 0;
135
337
  }
136
338
  }
137
339
  }
138
- }
139
- // arg_field_has(tool, field, pattern)
140
- const afhPatterns = contentAtoms["arg_field_has"];
141
- if (afhPatterns && event.args) {
142
- for (const raw of afhPatterns) {
143
- const parts = raw.split("|");
144
- if (parts.length >= 3) {
145
- const targetTool = parts[0];
146
- const field = parts[1];
147
- const pattern = parts.slice(2).join("|");
148
- if (targetTool === tool) {
149
- const fieldVal = event.args[field];
150
- const matched = fieldVal != null && new RegExp(pattern).test(String(fieldVal));
151
- v[predKey("arg_field_has", targetTool, field, pattern)] = matched;
340
+ // arg_has(tool, pattern)
341
+ const argHasPatterns = contentAtoms["arg_has"];
342
+ if (argHasPatterns && argsStr) {
343
+ for (const raw of argHasPatterns) {
344
+ const parts = raw.split("|");
345
+ if (parts.length >= 2) {
346
+ const targetTool = parts[0];
347
+ const pattern = parts.slice(1).join("|");
348
+ if (toolMatches(targetTool, aliases)) {
349
+ v[predKey("arg_has", targetTool, pattern)] = new RegExp(pattern).test(argsStr);
350
+ }
152
351
  }
153
352
  }
154
353
  }
155
- }
156
- // arg_length_exceeds(tool, field, max_chars)
157
- const ale = contentAtoms["arg_length_exceeds"];
158
- if (ale && event.args) {
159
- for (const raw of ale) {
160
- const parts = raw.split("|");
161
- if (parts.length >= 3) {
162
- const targetTool = parts[0];
163
- const field = parts[1];
164
- const maxChars = parseInt(parts[2], 10) || 500;
165
- if (targetTool === tool) {
166
- const fieldVal = event.args[field] ?? "";
167
- v[predKey("arg_length_exceeds", targetTool, field, String(maxChars))] =
168
- String(fieldVal).length > maxChars;
354
+ // arg_field_has(tool, field, pattern)
355
+ const afhPatterns = contentAtoms["arg_field_has"];
356
+ if (afhPatterns && event.args) {
357
+ for (const raw of afhPatterns) {
358
+ const parts = raw.split("|");
359
+ if (parts.length >= 3) {
360
+ const targetTool = parts[0];
361
+ const field = parts[1];
362
+ const pattern = parts.slice(2).join("|");
363
+ if (toolMatches(targetTool, aliases)) {
364
+ const fieldVal = event.args[field];
365
+ const matched = fieldVal != null && new RegExp(pattern).test(String(fieldVal));
366
+ v[predKey("arg_field_has", targetTool, field, pattern)] = matched;
367
+ }
169
368
  }
170
369
  }
171
370
  }
172
- }
173
- // arg_numeric(tool, field) — extract numeric value via 3 strategies:
174
- // 1. Direct dict key
175
- // 2. CLI flag "--field VALUE"
176
- // 3. Positional token in command string
177
- const anPatterns = contentAtoms["arg_numeric"];
178
- if (anPatterns) {
179
- for (const raw of anPatterns) {
180
- const parts = raw.split("|");
181
- if (parts.length >= 2) {
182
- const targetTool = parts[0];
183
- const field = parts[1];
184
- if (targetTool === tool) {
185
- let numericVal = null;
186
- // Strategy 1: direct dict key
187
- if (event.args && field in event.args) {
188
- const val = event.args[field];
189
- const n = typeof val === "number" ? val : parseFloat(String(val));
190
- if (!isNaN(n))
191
- numericVal = n;
371
+ // arg_length_exceeds(tool, field, max_chars)
372
+ const ale = contentAtoms["arg_length_exceeds"];
373
+ if (ale && event.args) {
374
+ for (const raw of ale) {
375
+ const parts = raw.split("|");
376
+ if (parts.length >= 3) {
377
+ const targetTool = parts[0];
378
+ const field = parts[1];
379
+ const maxChars = parseInt(parts[2], 10) || 500;
380
+ if (toolMatches(targetTool, aliases)) {
381
+ const fieldVal = event.args[field] ?? "";
382
+ v[predKey("arg_length_exceeds", targetTool, field, String(maxChars))] =
383
+ String(fieldVal).length > maxChars;
192
384
  }
193
- // Strategy 2: CLI --field VALUE
194
- if (numericVal == null && argsStr) {
195
- const flagRe = new RegExp(`--${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+([+-]?\\d+(?:\\.\\d+)?)`);
196
- const m = argsStr.match(flagRe);
197
- if (m)
198
- numericVal = parseFloat(m[1]);
199
- }
200
- // Strategy 3: positional index
201
- if (numericVal == null && event.args && /^\d+$/.test(field)) {
202
- const cmdStr = String(event.args.command ?? "");
203
- const tokens = cmdStr.split(/\s+/);
204
- const pos = parseInt(field, 10);
205
- if (pos < tokens.length) {
206
- const n = parseFloat(tokens[pos]);
207
- if (!isNaN(n))
385
+ }
386
+ }
387
+ }
388
+ // arg_numeric(tool, field) — extract numeric value via 3 strategies:
389
+ // 1. Direct dict key
390
+ // 2. CLI flag "--field VALUE"
391
+ // 3. Positional token in command string
392
+ const anPatterns = contentAtoms["arg_numeric"];
393
+ if (anPatterns) {
394
+ for (const raw of anPatterns) {
395
+ const parts = raw.split("|");
396
+ if (parts.length >= 2) {
397
+ const targetTool = parts[0];
398
+ const field = parts[1];
399
+ if (toolMatches(targetTool, aliases)) {
400
+ let numericVal = null;
401
+ // Strategy 1: direct dict key, read with the same
402
+ // normaliser ordered comparisons use ("$5,000" -> 5000).
403
+ if (event.args && Object.prototype.hasOwnProperty.call(event.args, field)) {
404
+ const n = numericArg(event.args[field]);
405
+ if (n !== undefined)
208
406
  numericVal = n;
209
407
  }
408
+ // Strategy 2: CLI --field VALUE
409
+ if (numericVal == null && argsStr) {
410
+ const flagRe = new RegExp(`--${escapeRegexLiteral(field)}\\s+([+-]?\\d+(?:\\.\\d+)?)`);
411
+ const m = argsStr.match(flagRe);
412
+ if (m)
413
+ numericVal = parseFloat(m[1]);
414
+ }
415
+ // Strategy 3: positional index
416
+ if (numericVal == null && event.args && /^\d+$/.test(field)) {
417
+ const cmdStr = String(event.args.command ?? "");
418
+ const tokens = cmdStr.split(/\s+/);
419
+ const pos = parseInt(field, 10);
420
+ if (pos < tokens.length) {
421
+ const n = parseFloat(tokens[pos]);
422
+ if (!isNaN(n))
423
+ numericVal = n;
424
+ }
425
+ }
426
+ if (numericVal != null) {
427
+ v[predKey("arg_numeric", targetTool, field)] = numericVal;
428
+ }
210
429
  }
211
- if (numericVal != null) {
212
- v[predKey("arg_numeric", targetTool, field)] = numericVal;
430
+ }
431
+ }
432
+ }
433
+ // arg_paths_within(tool, *prefixes) — all /-paths in args within prefix set
434
+ const apw = contentAtoms["arg_paths_within"];
435
+ if (apw && argsStr) {
436
+ for (const raw of apw) {
437
+ const parts = raw.split("|");
438
+ if (parts.length >= 2) {
439
+ const targetTool = parts[0];
440
+ const prefixes = parts.slice(1);
441
+ if (toolMatches(targetTool, aliases)) {
442
+ const paths = argsStr.match(/(\/[^\s;|&>"']+)/g) ?? [];
443
+ const allWithin = paths.length === 0 ||
444
+ paths.every((p) => prefixes.some((pre) => pathWithin(p, pre)));
445
+ v[predKey("arg_paths_within", targetTool, ...prefixes)] = allWithin;
213
446
  }
214
447
  }
215
448
  }
216
449
  }
217
450
  }
218
- // arg_paths_within(tool, *prefixes) — all /-paths in args within prefix set
219
- const apw = contentAtoms["arg_paths_within"];
220
- if (apw && argsStr) {
221
- for (const raw of apw) {
222
- const parts = raw.split("|");
223
- if (parts.length >= 2) {
224
- const targetTool = parts[0];
225
- const prefixes = parts.slice(1);
226
- if (targetTool === tool) {
227
- const paths = argsStr.match(/(\/[^\s;|&>"']+)/g) ?? [];
228
- const allWithin = paths.length === 0 ||
229
- paths.every((p) => prefixes.some((pre) => p.startsWith(pre)));
230
- v[predKey("arg_paths_within", targetTool, ...prefixes)] = allWithin;
451
+ }
452
+ else if (eventType === "llm_response") {
453
+ // ── llm_said(pattern) ───────────────────────────────────────
454
+ const content = event.content ?? "";
455
+ if (content && contentAtoms && contentAtoms["llm_said"]) {
456
+ for (const raw of contentAtoms["llm_said"]) {
457
+ const pattern = raw;
458
+ if (!pattern)
459
+ continue;
460
+ let matched = false;
461
+ try {
462
+ matched = new RegExp(pattern).test(content);
463
+ }
464
+ catch {
465
+ // Bad regex → never matches (parity with Python re.search → no match).
466
+ }
467
+ v[predKey("llm_said", pattern)] = matched;
468
+ }
469
+ }
470
+ // response_words / response_chars — unparameterized Vars consumed by max_length.
471
+ if (content) {
472
+ v["response_words"] = content.trim().length === 0 ? 0 : content.trim().split(/\s+/).length;
473
+ v["response_chars"] = content.length;
474
+ }
475
+ // ``args.segment`` convention: extended-thinking integrations tag
476
+ // segment="thinking" / "answer" so contracts can scope content
477
+ // checks to one segment only.
478
+ if (event.args && typeof event.args.segment === "string" && event.args.segment) {
479
+ v[predKey("segment", String(event.args.segment))] = true;
480
+ }
481
+ }
482
+ else if (eventType === "context_update") {
483
+ // Merge user-pushed facts into state.currentCtx so every
484
+ // subsequent event sees them as ctx(k, v) atoms. Apply BEFORE
485
+ // the ctx-emission loop so a contract at the same timestep as
486
+ // the update already observes the new keys.
487
+ if (event.args) {
488
+ for (const [k, val] of Object.entries(event.args)) {
489
+ if (k == null)
490
+ continue;
491
+ state.currentCtx[String(k)] = val == null ? "" : String(val);
492
+ }
493
+ }
494
+ }
495
+ else if (eventType === "evidence" && event.key) {
496
+ // A cloud claim-verification verdict fed back into the trace.
497
+ // `key` is the predicate name; `args` carries the server's verdict
498
+ // and policy action verbatim. No verdict computation happens here —
499
+ // the atoms only restate what the service answered, so contracts
500
+ // (`claim_requires_evidence`, `underdetermined_must_clarify`) can
501
+ // reference it. Verdict upper-cased, action lower-cased, matching
502
+ // the server's own vocabulary and Python's grounding.
503
+ v[predKey("claim_emitted", event.key)] = true;
504
+ const verdict = String(event.args?.verdict ?? "").toUpperCase();
505
+ if (verdict)
506
+ v[predKey("evidence_verdict", event.key, verdict)] = true;
507
+ const action = String(event.args?.action ?? "").toLowerCase();
508
+ if (action)
509
+ v[predKey("evidence_action", event.key, action)] = true;
510
+ }
511
+ // ── ctx(k, v) / ctx_value(k) — emit per current_ctx entry, every event ─
512
+ // ctx(k, v) is the boolean atom (k==v). ctx_value(k) carries the raw
513
+ // value so Term-based comparisons can read it without hard-coding the
514
+ // expected value: e.g. Eq(CtxValue("approved_amount"),
515
+ // ArgValue("issue_refund", "amount")).
516
+ for (const [k, val] of Object.entries(state.currentCtx)) {
517
+ v[predKey("ctx", k, val)] = true;
518
+ v[predKey("ctx_value", k)] = val;
519
+ }
520
+ // ── ctx_matches(key, pattern) — regex against current_ctx[key] ──
521
+ if (contentAtoms && contentAtoms["ctx_matches"]) {
522
+ for (const raw of contentAtoms["ctx_matches"]) {
523
+ const parts = raw.split("|");
524
+ if (parts.length >= 2) {
525
+ const key = parts[0];
526
+ const pattern = parts.slice(1).join("|");
527
+ const cur = state.currentCtx[key];
528
+ let matched = false;
529
+ if (cur != null) {
530
+ try {
531
+ matched = new RegExp(pattern).test(cur);
532
+ }
533
+ catch {
534
+ matched = false;
231
535
  }
232
536
  }
537
+ v[predKey("ctx_matches", key, pattern)] = matched;
233
538
  }
234
539
  }
235
540
  }
236
- // token_count(type) — cumulative token count
237
- // Extract from event.args.tokens if present (gen_ai.usage.* convention)
541
+ // token_count(type) — cumulative token count (only meaningful for events with usage)
238
542
  if (event.args) {
239
543
  const tokens = event.args.tokens;
240
544
  if (tokens) {
@@ -251,18 +555,56 @@ export function groundEvent(event, state, contentAtoms) {
251
555
  for (const [scope, cnt] of Object.entries(state.tokenCounts)) {
252
556
  v[predKey("token_count", scope)] = cnt;
253
557
  }
254
- // delegation_depth — incremented by event.event_type === "delegation"
255
- if (event.event_type === "delegation") {
558
+ // delegation_depth — incremented by event_type === "delegation"
559
+ if (eventType === "delegation") {
256
560
  state.delegationDepth += 1;
257
561
  }
258
562
  v[predKey("delegation_depth")] = state.delegationDepth;
259
- // Emit count/count_with for all tracked vars
563
+ // Emit count snapshots for all tracked tools
260
564
  for (const [t, cnt] of Object.entries(state.callCounts)) {
261
565
  v[predKey("count", t)] = cnt;
262
566
  }
263
567
  for (const [key, cnt] of Object.entries(state.callWithCounts)) {
264
- const [t, p] = [key.split("|")[0], key.split("|").slice(1).join("|")];
265
- v[predKey("count_with", t, p)] = cnt;
568
+ const idx = key.indexOf("|");
569
+ if (idx > 0) {
570
+ v[predKey("count_with", key.slice(0, idx), key.slice(idx + 1))] = cnt;
571
+ }
572
+ }
573
+ // ── last_ts bookkeeping (fresh False→True transitions) ─────────
574
+ // A predicate that was True last event and still True now is
575
+ // "sustained" — we do NOT refresh its last_ts. Sustained covers
576
+ // ctx atoms re-emitted while held in scope; this is what makes
577
+ // time_since(ctx(approval.role, alice)) measure time since the
578
+ // approval was granted, not the trivial 0 from re-emission.
579
+ const trueNow = new Set();
580
+ for (const [key, val] of Object.entries(v)) {
581
+ if (val === true) {
582
+ trueNow.add(key);
583
+ if (!state.trueAtPrev.has(key)) {
584
+ state.lastTs[key] = state.now;
585
+ }
586
+ }
587
+ }
588
+ state.trueAtPrev = trueNow;
589
+ // ── time atoms (emitted last so they see fresh state.now) ─────
590
+ v["now"] = state.now;
591
+ if (contentAtoms && contentAtoms["time_since"]) {
592
+ for (const raw of contentAtoms["time_since"]) {
593
+ if (!raw)
594
+ continue;
595
+ const targetKey = raw;
596
+ let delta;
597
+ if (targetKey in state.lastTs) {
598
+ delta = state.now - state.lastTs[targetKey];
599
+ }
600
+ else {
601
+ // Sentinel — predicate has never fired. "Very long ago" is
602
+ // the right semantics for Le(time_since(P), N): if P never
603
+ // happened, the constraint must evaluate False.
604
+ delta = 1e18;
605
+ }
606
+ v[predKey("time_since", targetKey)] = delta;
607
+ }
266
608
  }
267
609
  return v;
268
610
  }