@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.
- package/bin/sponsio.cjs +11 -0
- package/contracts/benchmark/agentdojo.yaml +551 -0
- package/contracts/benchmark/odcv_bench.yaml +393 -0
- package/contracts/benchmark/redcode_exec.yaml +488 -0
- package/contracts/benchmark/swebench.yaml +176 -0
- package/contracts/benchmark/tau2_bench.yaml +984 -0
- package/contracts/capability/credentials.yaml +8 -8
- package/contracts/capability/database.yaml +6 -6
- package/contracts/capability/destructive.yaml +213 -0
- package/contracts/capability/factual-claims.yaml +79 -0
- package/contracts/capability/filesystem-strict.yaml +4 -4
- package/contracts/capability/filesystem.yaml +9 -9
- package/contracts/capability/host-config-integrity.yaml +318 -0
- package/contracts/capability/self-modify.yaml +260 -9
- package/contracts/capability/shell.yaml +43 -41
- package/contracts/capability/subagent.yaml +5 -5
- package/contracts/incident/claude-code-secret-bypass.yaml +57 -26
- package/contracts/incident/cursor-railway-wipe.yaml +63 -7
- package/contracts/incident/mcp-composition.yaml +12 -12
- package/contracts/incident/openclaw.yaml +47 -92
- package/contracts/incident/subagent-escape.yaml +4 -4
- package/contracts/premium/.gitkeep +5 -6
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +283 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/cli.d.ts +25 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +491 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/config.d.ts +88 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +243 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/demo.d.ts +12 -0
- package/dist/cli/demo.d.ts.map +1 -0
- package/dist/cli/demo.js +350 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/doctor.d.ts +11 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +201 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/eval.d.ts +2 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/eval.js +361 -0
- package/dist/cli/eval.js.map +1 -0
- package/dist/cli/explain.d.ts +2 -0
- package/dist/cli/explain.d.ts.map +1 -0
- package/dist/cli/explain.js +266 -0
- package/dist/cli/explain.js.map +1 -0
- package/dist/cli/export-sessions.d.ts +2 -0
- package/dist/cli/export-sessions.d.ts.map +1 -0
- package/dist/cli/export-sessions.js +182 -0
- package/dist/cli/export-sessions.js.map +1 -0
- package/dist/cli/export.d.ts +2 -0
- package/dist/cli/export.d.ts.map +1 -0
- package/dist/cli/export.js +222 -0
- package/dist/cli/export.js.map +1 -0
- package/dist/cli/extractors/common.d.ts +27 -0
- package/dist/cli/extractors/common.d.ts.map +1 -0
- package/dist/cli/extractors/common.js +107 -0
- package/dist/cli/extractors/common.js.map +1 -0
- package/dist/cli/extractors/generic.d.ts +37 -0
- package/dist/cli/extractors/generic.d.ts.map +1 -0
- package/dist/cli/extractors/generic.js +185 -0
- package/dist/cli/extractors/generic.js.map +1 -0
- package/dist/cli/extractors/langchain.d.ts +18 -0
- package/dist/cli/extractors/langchain.d.ts.map +1 -0
- package/dist/cli/extractors/langchain.js +86 -0
- package/dist/cli/extractors/langchain.js.map +1 -0
- package/dist/cli/extractors/vercel.d.ts +17 -0
- package/dist/cli/extractors/vercel.d.ts.map +1 -0
- package/dist/cli/extractors/vercel.js +46 -0
- package/dist/cli/extractors/vercel.js.map +1 -0
- package/dist/cli/index.d.ts +38 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +146 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +177 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mode.d.ts +2 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +71 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/cli/onboard.d.ts +88 -0
- package/dist/cli/onboard.d.ts.map +1 -0
- package/dist/cli/onboard.js +758 -0
- package/dist/cli/onboard.js.map +1 -0
- package/dist/cli/otlp.d.ts +27 -0
- package/dist/cli/otlp.d.ts.map +1 -0
- package/dist/cli/otlp.js +79 -0
- package/dist/cli/otlp.js.map +1 -0
- package/dist/cli/package.json +1 -0
- package/dist/cli/packs.d.ts +16 -0
- package/dist/cli/packs.d.ts.map +1 -0
- package/dist/cli/packs.js +190 -0
- package/dist/cli/packs.js.map +1 -0
- package/dist/cli/patterns.d.ts +16 -0
- package/dist/cli/patterns.d.ts.map +1 -0
- package/dist/cli/patterns.js +136 -0
- package/dist/cli/patterns.js.map +1 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +54 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/replay.d.ts +2 -0
- package/dist/cli/replay.d.ts.map +1 -0
- package/dist/cli/replay.js +196 -0
- package/dist/cli/replay.js.map +1 -0
- package/dist/cli/report.d.ts +57 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +298 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/scan.d.ts +2 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +172 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/skill.d.ts +15 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +131 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tokens.d.ts +31 -0
- package/dist/cli/tokens.d.ts.map +1 -0
- package/dist/cli/tokens.js +73 -0
- package/dist/cli/tokens.js.map +1 -0
- package/dist/cli/types.d.ts +46 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +3 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/validate.d.ts +19 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +205 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/zod.d.ts +36 -0
- package/dist/cli/zod.d.ts.map +1 -0
- package/dist/cli/zod.js +226 -0
- package/dist/cli/zod.js.map +1 -0
- package/dist/cloud/client.d.ts +76 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +204 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/cloud/evidence.d.ts +108 -0
- package/dist/cloud/evidence.d.ts.map +1 -0
- package/dist/cloud/evidence.js +184 -0
- package/dist/cloud/evidence.js.map +1 -0
- package/dist/cloud/privacy.d.ts +67 -0
- package/dist/cloud/privacy.d.ts.map +1 -0
- package/dist/cloud/privacy.js +170 -0
- package/dist/cloud/privacy.js.map +1 -0
- package/dist/cloud/view-model.d.ts +49 -0
- package/dist/cloud/view-model.d.ts.map +1 -0
- package/dist/cloud/view-model.js +172 -0
- package/dist/cloud/view-model.js.map +1 -0
- package/dist/contract.d.ts +13 -13
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +21 -21
- package/dist/contract.js.map +1 -1
- package/dist/core/config-loader.d.ts +1 -1
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +116 -26
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/evaluator.d.ts +11 -1
- package/dist/core/evaluator.d.ts.map +1 -1
- package/dist/core/evaluator.js +275 -13
- package/dist/core/evaluator.js.map +1 -1
- package/dist/core/formula.d.ts +76 -17
- package/dist/core/formula.d.ts.map +1 -1
- package/dist/core/formula.js +95 -1
- package/dist/core/formula.js.map +1 -1
- package/dist/core/grounding.d.ts +86 -3
- package/dist/core/grounding.d.ts.map +1 -1
- package/dist/core/grounding.js +485 -143
- package/dist/core/grounding.js.map +1 -1
- package/dist/core/nl-parser.d.ts +2 -2
- package/dist/core/nl-parser.d.ts.map +1 -1
- package/dist/core/nl-parser.js +690 -42
- package/dist/core/nl-parser.js.map +1 -1
- package/dist/core/pack-loader.d.ts.map +1 -1
- package/dist/core/pack-loader.js +14 -2
- package/dist/core/pack-loader.js.map +1 -1
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +30 -1
- package/dist/core/parser.js.map +1 -1
- package/dist/core/pattern-factory.d.ts +4 -4
- package/dist/core/pattern-factory.d.ts.map +1 -1
- package/dist/core/pattern-factory.js +59 -4
- package/dist/core/pattern-factory.js.map +1 -1
- package/dist/core/patterns.d.ts +196 -5
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +400 -13
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/session-log.d.ts.map +1 -1
- package/dist/core/session-log.js +20 -1
- package/dist/core/session-log.js.map +1 -1
- package/dist/core/spans.d.ts +1 -1
- package/dist/core/spans.d.ts.map +1 -1
- package/dist/core/spans.js.map +1 -1
- package/dist/core/sto.d.ts +30 -66
- package/dist/core/sto.d.ts.map +1 -1
- package/dist/core/sto.js +39 -100
- package/dist/core/sto.js.map +1 -1
- package/dist/core/tool-names.d.ts +59 -0
- package/dist/core/tool-names.d.ts.map +1 -0
- package/dist/core/tool-names.js +128 -0
- package/dist/core/tool-names.js.map +1 -0
- package/dist/index.d.ts +227 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +539 -152
- package/dist/index.js.map +1 -1
- package/dist/integrations/claude-agent.d.ts +5 -0
- package/dist/integrations/claude-agent.d.ts.map +1 -1
- package/dist/integrations/claude-agent.js +42 -13
- package/dist/integrations/claude-agent.js.map +1 -1
- package/dist/integrations/google-adk.d.ts +5 -3
- package/dist/integrations/google-adk.d.ts.map +1 -1
- package/dist/integrations/google-adk.js +9 -5
- package/dist/integrations/google-adk.js.map +1 -1
- package/dist/integrations/langchain.d.ts +7 -0
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +21 -3
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/integrations/openai-agents.d.ts +22 -10
- package/dist/integrations/openai-agents.d.ts.map +1 -1
- package/dist/integrations/openai-agents.js +57 -16
- package/dist/integrations/openai-agents.js.map +1 -1
- package/dist/integrations/openai.d.ts +11 -0
- package/dist/integrations/openai.d.ts.map +1 -1
- package/dist/integrations/openai.js +130 -48
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/vercel-ai.d.ts +20 -9
- package/dist/integrations/vercel-ai.d.ts.map +1 -1
- package/dist/integrations/vercel-ai.js +131 -32
- package/dist/integrations/vercel-ai.js.map +1 -1
- package/dist/render/tokens.d.ts.map +1 -1
- package/dist/render/tokens.js +1 -0
- package/dist/render/tokens.js.map +1 -1
- package/package.json +41 -9
- package/prompts/onboard.md +259 -0
- package/prompts/scan.md +123 -0
- package/skills/SKILL.md +432 -0
- package/contracts/core/llm_safety.yaml +0 -130
package/dist/core/patterns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
457
|
-
"
|
|
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
|
-
|
|
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
|