@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/index.js
CHANGED
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
* if (result.blocked) { ... }
|
|
21
21
|
*/
|
|
22
22
|
import { evaluate } from "./core/evaluator.js";
|
|
23
|
-
import { groundEvent, newGroundingState, collectContentAtoms, } from "./core/grounding.js";
|
|
23
|
+
import { groundEvent, newGroundingState, collectContentAtoms, validateContentPatterns, numericArg, toolMatches, } from "./core/grounding.js";
|
|
24
24
|
import { parseNl } from "./core/nl-parser.js";
|
|
25
|
+
import { canonicalTool, toolAliases } from "./core/tool-names.js";
|
|
25
26
|
import { loadSponsoConfig, } from "./core/config-loader.js";
|
|
26
27
|
import { SessionLogger } from "./core/session-log.js";
|
|
27
|
-
import { ContractCheckSpan, EnforcementSpan, GuaranteeSpan, SpanCollector, } from "./core/spans.js";
|
|
28
|
+
import { AgentTurnSpan, ContractCheckSpan, EnforcementSpan, GuaranteeSpan, SpanCollector, } from "./core/spans.js";
|
|
28
29
|
import { renderSession } from "./render/session-view.js";
|
|
30
|
+
import { CloudClient, readProject } from "./cloud/client.js";
|
|
31
|
+
import * as privacy from "./cloud/privacy.js";
|
|
32
|
+
import { buildViewModel, newSessionId, } from "./cloud/view-model.js";
|
|
29
33
|
// Re-exports
|
|
30
34
|
export { evaluate } from "./core/evaluator.js";
|
|
31
35
|
export * from "./core/formula.js";
|
|
@@ -36,7 +40,19 @@ export { groundEvent, newGroundingState, collectContentAtoms, } from "./core/gro
|
|
|
36
40
|
export { loadSponsoConfig } from "./core/config-loader.js";
|
|
37
41
|
export { SessionLogger, rotateSessions } from "./core/session-log.js";
|
|
38
42
|
export { contract, ContractBuilder } from "./contract.js";
|
|
39
|
-
export {
|
|
43
|
+
export { parseScore, CloudFeatureError } from "./core/sto.js";
|
|
44
|
+
export { CloudClient, CloudError, readApiKey, readProject, baseUrl, } from "./cloud/client.js";
|
|
45
|
+
export { LEVELS as PRIVACY_LEVELS } from "./cloud/privacy.js";
|
|
46
|
+
export { buildViewModel, newSessionId } from "./cloud/view-model.js";
|
|
47
|
+
/** Actions after which the original tool call must NOT run. */
|
|
48
|
+
export const STOPPING_ACTIONS = new Set([
|
|
49
|
+
"blocked",
|
|
50
|
+
"redirected",
|
|
51
|
+
]);
|
|
52
|
+
/** Parity with Python's ``is_stopping_action``. */
|
|
53
|
+
export function isStoppingAction(action) {
|
|
54
|
+
return STOPPING_ACTIONS.has(action);
|
|
55
|
+
}
|
|
40
56
|
/**
|
|
41
57
|
* Best-effort accessor for the agent-facing line. Prefers the
|
|
42
58
|
* structured ``agentMsg`` field when populated; falls back to
|
|
@@ -54,13 +70,34 @@ export function selectAgentMessage(violations, fallback = "Contract violation")
|
|
|
54
70
|
}
|
|
55
71
|
return fallback;
|
|
56
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Fill the derived half of a verdict from the violations' actions. One
|
|
75
|
+
* place, so a new `DetAction` cannot be handled inconsistently across
|
|
76
|
+
* the several sites that build a `CheckResult`.
|
|
77
|
+
*/
|
|
78
|
+
export function deriveVerdict(detViolations, stoViolations = []) {
|
|
79
|
+
const acted = detViolations.filter((v) => v.action !== undefined);
|
|
80
|
+
const redirect = acted.find((v) => v.action === "redirected");
|
|
81
|
+
return {
|
|
82
|
+
blocked: acted.some((v) => v.action === "blocked"),
|
|
83
|
+
escalated: acted.some((v) => v.action === "escalated"),
|
|
84
|
+
redirected: redirect !== undefined,
|
|
85
|
+
...(redirect?.redirectedTo !== undefined
|
|
86
|
+
? { redirectedTo: redirect.redirectedTo }
|
|
87
|
+
: {}),
|
|
88
|
+
stopOriginal: acted.some((v) => isStoppingAction(v.action)),
|
|
89
|
+
allViolations: [...detViolations, ...stoViolations],
|
|
90
|
+
needsRetry: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
57
93
|
export class Sponsio {
|
|
58
94
|
agentId;
|
|
59
95
|
mode;
|
|
60
96
|
_contracts;
|
|
97
|
+
// Sto state. Always empty/null in this build, the constructor
|
|
98
|
+
// rejects yaml sto contracts and inline `judge:` at config load.
|
|
99
|
+
// Kept as schema-stable surface for external composition / wrapping.
|
|
61
100
|
_stoContracts;
|
|
62
|
-
_judge;
|
|
63
|
-
_judgeFallback;
|
|
64
101
|
_stoContext;
|
|
65
102
|
_trace;
|
|
66
103
|
_state;
|
|
@@ -68,6 +105,14 @@ export class Sponsio {
|
|
|
68
105
|
_violations;
|
|
69
106
|
_logger;
|
|
70
107
|
_turnSpans = [];
|
|
108
|
+
// The same turns with the violations they produced, which the span
|
|
109
|
+
// tree does not carry in a form the cloud can read: it records that a
|
|
110
|
+
// contract was violated, not which pattern and arguments, and the
|
|
111
|
+
// cloud groups findings by exactly those.
|
|
112
|
+
_runSpans = [];
|
|
113
|
+
_sessionId = newSessionId();
|
|
114
|
+
_startedAt = Date.now();
|
|
115
|
+
_rulebookStamp;
|
|
71
116
|
constructor(options = {}) {
|
|
72
117
|
this.agentId = options.agentId ?? "agent";
|
|
73
118
|
this._trace = [];
|
|
@@ -75,8 +120,6 @@ export class Sponsio {
|
|
|
75
120
|
this._violations = [];
|
|
76
121
|
this._contracts = [];
|
|
77
122
|
this._stoContracts = [];
|
|
78
|
-
this._judge = null;
|
|
79
|
-
this._judgeFallback = "allow";
|
|
80
123
|
this._stoContext = {};
|
|
81
124
|
// ── Gather contracts + yaml-derived settings ────────────────────
|
|
82
125
|
let yamlMode;
|
|
@@ -98,6 +141,13 @@ export class Sponsio {
|
|
|
98
141
|
// ── Resolve mode: env > ctor > yaml > default ────────────────────
|
|
99
142
|
this.mode = resolveMode(options.mode, yamlMode);
|
|
100
143
|
// ── Parse NL strings into det formulas ───────────────────────────
|
|
144
|
+
// A contract that does not parse is a rule that is not there. In
|
|
145
|
+
// observe mode that is a warning, because the point of observe is to
|
|
146
|
+
// keep running and surface what would fire. In enforce it is fatal:
|
|
147
|
+
// the alternative is an agent that reports success while enforcing a
|
|
148
|
+
// book with holes in it, and the operator has no way to know which
|
|
149
|
+
// holes. Python raises here; TypeScript logged one line and ran on.
|
|
150
|
+
const unparsed = [];
|
|
101
151
|
for (const c of sources) {
|
|
102
152
|
if (typeof c === "string") {
|
|
103
153
|
const parsed = parseNl(c);
|
|
@@ -105,48 +155,172 @@ export class Sponsio {
|
|
|
105
155
|
this._contracts.push(parsed);
|
|
106
156
|
}
|
|
107
157
|
else {
|
|
108
|
-
|
|
158
|
+
unparsed.push(c);
|
|
109
159
|
}
|
|
110
160
|
}
|
|
111
161
|
else {
|
|
112
162
|
this._contracts.push(c);
|
|
113
163
|
}
|
|
114
164
|
}
|
|
115
|
-
|
|
165
|
+
if (unparsed.length > 0) {
|
|
166
|
+
const listed = unparsed.map((c) => ` - ${c}`).join("\n");
|
|
167
|
+
if (this.mode === "enforce") {
|
|
168
|
+
throw new Error(`sponsio: ${unparsed.length} contract(s) could not be parsed, and ` +
|
|
169
|
+
`this guard is in enforce mode. THESE RULES ARE NOT ARMED. Fix ` +
|
|
170
|
+
`them, or write them as structured \`pattern:\` blocks in a ` +
|
|
171
|
+
`sponsio.yaml, which covers every pattern the library has.\n` +
|
|
172
|
+
listed);
|
|
173
|
+
}
|
|
174
|
+
console.warn(`[sponsio] ${unparsed.length} contract(s) could not be parsed and ` +
|
|
175
|
+
`are NOT armed (observe mode, so the run continues):\n${listed}`);
|
|
176
|
+
}
|
|
177
|
+
// ── Sto pipeline is not part of this build ──────────────────────
|
|
116
178
|
// The managed LLM-judge catalog (`tone` / `relevance` / `llm_judge`
|
|
117
|
-
//
|
|
118
|
-
// engine
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
this._judgeFallback = "allow";
|
|
179
|
+
// ...) and the judge client are not part of this build (the
|
|
180
|
+
// deterministic engine provides no implementation). The engine
|
|
181
|
+
// logs-and-skips any yaml sto contracts and any inline `judge:`
|
|
182
|
+
// option, never constructs an evaluator, never contacts an LLM.
|
|
183
|
+
// The API surface (`guardAfter`, `setContext`, `judge` ctor
|
|
184
|
+
// option) is preserved so a shared yaml between deterministic
|
|
185
|
+
// builds and external sto implementations doesn't refuse to load.
|
|
125
186
|
for (const spec of yamlStoSpecs) {
|
|
126
187
|
yamlSkipped.push({
|
|
127
188
|
kind: "sto-contract",
|
|
128
|
-
detail: `${spec.desc} (sto pipeline is
|
|
189
|
+
detail: `${spec.desc} (sto pipeline is not supported in this build)`,
|
|
129
190
|
});
|
|
130
191
|
}
|
|
131
192
|
if (options.judge) {
|
|
132
193
|
yamlSkipped.push({
|
|
133
194
|
kind: "sto-contract",
|
|
134
|
-
detail: "`judge:` option ignored (sto pipeline is
|
|
195
|
+
detail: "`judge:` option ignored (sto pipeline is not supported in this build)",
|
|
135
196
|
});
|
|
136
197
|
}
|
|
137
|
-
// Reference yamlJudge so unused-var lints don't fire
|
|
138
|
-
// is parsed by the loader for shared-yaml compatibility but
|
|
139
|
-
//
|
|
198
|
+
// Reference yamlJudge so unused-var lints don't fire: the field
|
|
199
|
+
// is parsed by the loader for shared-yaml compatibility but this
|
|
200
|
+
// build never constructs a judge from it.
|
|
140
201
|
void yamlJudge;
|
|
141
202
|
// ── Warn once about yaml features the TS runtime can't handle ───
|
|
142
203
|
warnOnceAboutSkipped(yamlSkipped);
|
|
143
204
|
this._contentAtoms = collectContentAtoms(this._contracts.map((c) => c.formula));
|
|
205
|
+
// A pattern that does not compile is a broken contract; refuse to
|
|
206
|
+
// arm it rather than let it never match at runtime.
|
|
207
|
+
validateContentPatterns(this._contentAtoms);
|
|
144
208
|
// ── Session log ─────────────────────────────────────────────────
|
|
145
209
|
const wantLog = options.sessionLog ?? true;
|
|
146
210
|
this._logger = wantLog
|
|
147
211
|
? new SessionLogger(this.agentId, { baseDir: options.sessionLogBaseDir })
|
|
148
212
|
: null;
|
|
149
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Predicates whose truth depends on the *contents* of a tool call's
|
|
216
|
+
* arguments. If a call arrives with no arguments, none of these keys is
|
|
217
|
+
* written and the evaluator reads a missing key as false, which makes
|
|
218
|
+
* ``argBlacklist`` (``!arg_field_has(...)``) report "satisfied" for a
|
|
219
|
+
* call nothing looked at. Mirrors Python's
|
|
220
|
+
* ``BaseGuard._ARG_SHAPED_PREDICATES``.
|
|
221
|
+
*/
|
|
222
|
+
static ARG_SHAPED_PREDICATES = [
|
|
223
|
+
"arg_has",
|
|
224
|
+
"arg_field_has",
|
|
225
|
+
"arg_length_exceeds",
|
|
226
|
+
"arg_paths_within",
|
|
227
|
+
"arg_numeric",
|
|
228
|
+
"arg_value",
|
|
229
|
+
];
|
|
230
|
+
/** Canonical names of the tools some contract reads the arguments of. */
|
|
231
|
+
_toolsNeedingArgs() {
|
|
232
|
+
const tools = new Set();
|
|
233
|
+
for (const pred of Sponsio.ARG_SHAPED_PREDICATES) {
|
|
234
|
+
for (const raw of this._contentAtoms[pred] ?? []) {
|
|
235
|
+
const tool = raw.split("|")[0];
|
|
236
|
+
if (tool)
|
|
237
|
+
tools.add(canonicalTool(tool));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return tools;
|
|
241
|
+
}
|
|
242
|
+
/** ``[tool, field]`` pairs some ``arg_numeric`` rule reads. */
|
|
243
|
+
_numericRuleTargets() {
|
|
244
|
+
const pairs = [];
|
|
245
|
+
for (const raw of this._contentAtoms["arg_numeric"] ?? []) {
|
|
246
|
+
const parts = raw.split("|");
|
|
247
|
+
if (parts.length >= 2)
|
|
248
|
+
pairs.push([parts[0], parts[1]]);
|
|
249
|
+
}
|
|
250
|
+
return pairs;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Refuse a call whose argument rules cannot be evaluated.
|
|
254
|
+
*
|
|
255
|
+
* Two cases, both of which used to read as "not violated":
|
|
256
|
+
*
|
|
257
|
+
* - The call arrived with no arguments (``{}``, ``undefined``, ``null``)
|
|
258
|
+
* but some rule reads this tool's arguments. An adapter that loses the
|
|
259
|
+
* arguments, a partial streamed call or a malformed payload all land
|
|
260
|
+
* here, and every argument check would pass vacuously.
|
|
261
|
+
* - A field an ``arg_numeric`` rule bounds is present but is not a
|
|
262
|
+
* number (``"five thousand"``). Grounding leaves ``arg_numeric`` unset
|
|
263
|
+
* and the evaluator reads the gap as 0, which passes every upper
|
|
264
|
+
* bound. An absent field is left alone: CLI-flag and positional rules
|
|
265
|
+
* on ``Bash`` legitimately see calls without their flag.
|
|
266
|
+
*
|
|
267
|
+
* ``SPONSIO_ALLOW_MISSING_ARGS=1`` restores the permissive behaviour.
|
|
268
|
+
* Mirrors Python ``BaseGuard._args_unevaluable`` and
|
|
269
|
+
* ``_unreadable_numeric_arg``; both block under rule id
|
|
270
|
+
* ``args:unevaluable``.
|
|
271
|
+
*/
|
|
272
|
+
_unevaluableArgs(toolName, args) {
|
|
273
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
274
|
+
if (env?.SPONSIO_ALLOW_MISSING_ARGS === "1")
|
|
275
|
+
return null;
|
|
276
|
+
const empty = args == null || (typeof args === "object" && Object.keys(args).length === 0);
|
|
277
|
+
if (empty) {
|
|
278
|
+
// Every spelling grounding binds the call under, so
|
|
279
|
+
// ``mcp__shell__Bash`` meets a rule written for ``Bash``.
|
|
280
|
+
const needed = this._toolsNeedingArgs();
|
|
281
|
+
if (!toolAliases(toolName).some((alias) => needed.has(alias)))
|
|
282
|
+
return null;
|
|
283
|
+
const message = `BLOCKED: ${this.agentId}.${toolName}. \`${toolName}\` was called ` +
|
|
284
|
+
`with no arguments, but a contract inspects its arguments. The ` +
|
|
285
|
+
`check cannot be evaluated, so the call is refused rather than ` +
|
|
286
|
+
`passed unchecked (set SPONSIO_ALLOW_MISSING_ARGS=1 to allow).`;
|
|
287
|
+
return {
|
|
288
|
+
desc: message,
|
|
289
|
+
message,
|
|
290
|
+
action: "blocked",
|
|
291
|
+
ruleId: "args:unevaluable",
|
|
292
|
+
agentMsg: `The action \`${toolName}\` was rejected because its arguments ` +
|
|
293
|
+
`were missing and a policy needs to read them. Retry with the ` +
|
|
294
|
+
`arguments included.`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const aliases = toolAliases(toolName);
|
|
298
|
+
for (const [target, field] of this._numericRuleTargets()) {
|
|
299
|
+
if (!Object.prototype.hasOwnProperty.call(args, field))
|
|
300
|
+
continue;
|
|
301
|
+
if (!toolMatches(target, aliases))
|
|
302
|
+
continue;
|
|
303
|
+
const value = args[field];
|
|
304
|
+
if (value === null || value === undefined)
|
|
305
|
+
continue;
|
|
306
|
+
if (numericArg(value) !== undefined)
|
|
307
|
+
continue;
|
|
308
|
+
const shown = JSON.stringify(String(value).slice(0, 60));
|
|
309
|
+
const message = `BLOCKED: ${this.agentId}.${toolName}. \`${field}\`=${shown} is ` +
|
|
310
|
+
`not a number, but a contract bounds it numerically. The bound ` +
|
|
311
|
+
`cannot be checked, so the call is refused rather than passed ` +
|
|
312
|
+
`unchecked (set SPONSIO_ALLOW_MISSING_ARGS=1 to allow).`;
|
|
313
|
+
return {
|
|
314
|
+
desc: message,
|
|
315
|
+
message,
|
|
316
|
+
action: "blocked",
|
|
317
|
+
ruleId: "args:unevaluable",
|
|
318
|
+
agentMsg: `The action \`${toolName}\` was rejected because \`${field}\` ` +
|
|
319
|
+
`must be a plain number. Retry with a numeric value.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
150
324
|
/**
|
|
151
325
|
* Check a tool call against contracts before execution.
|
|
152
326
|
*
|
|
@@ -165,6 +339,20 @@ export class Sponsio {
|
|
|
165
339
|
return this._contracts.map((c) => c.desc);
|
|
166
340
|
}
|
|
167
341
|
guardBefore(toolName, args = {}) {
|
|
342
|
+
const unevaluable = this._unevaluableArgs(toolName, args);
|
|
343
|
+
if (unevaluable) {
|
|
344
|
+
const violations = [unevaluable.message];
|
|
345
|
+
this._violations.push(...violations);
|
|
346
|
+
this._logViolations(toolName, violations, [unevaluable.desc], "blocked");
|
|
347
|
+
return {
|
|
348
|
+
...deriveVerdict([unevaluable]),
|
|
349
|
+
allowed: false,
|
|
350
|
+
message: unevaluable.message,
|
|
351
|
+
violations,
|
|
352
|
+
detViolations: [unevaluable],
|
|
353
|
+
stoViolations: [],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
168
356
|
const event = { tool: toolName, args };
|
|
169
357
|
const snapshot = this._snapshotState();
|
|
170
358
|
const valuation = groundEvent(event, this._state, this._contentAtoms);
|
|
@@ -176,9 +364,24 @@ export class Sponsio {
|
|
|
176
364
|
const violations = [];
|
|
177
365
|
const violatedDescs = [];
|
|
178
366
|
const detViolations = [];
|
|
367
|
+
let anyStopping = false;
|
|
179
368
|
for (const contract of this._contracts) {
|
|
180
369
|
const checkSpan = new ContractCheckSpan(contract.desc, "hard");
|
|
181
370
|
collector.push(checkSpan);
|
|
371
|
+
// Liveness obligations (``audit_after`` / ``deadline`` /
|
|
372
|
+
// ``required_steps_completion`` / ``always_followed_by``) cannot
|
|
373
|
+
// be discharged on a *prefix* of a finite trace — checking them
|
|
374
|
+
// mid-session would fire on every trigger before the obligated
|
|
375
|
+
// step has had a chance to run. Mirrors Python's
|
|
376
|
+
// ``RuntimeMonitor`` which skips ``liveness=True`` formulas
|
|
377
|
+
// during incremental checks and revisits them at session end
|
|
378
|
+
// (``finishSession``).
|
|
379
|
+
if (contract.liveness) {
|
|
380
|
+
const guaranteeSpan = new GuaranteeSpan(contract.desc, true);
|
|
381
|
+
collector.add(guaranteeSpan, "ok");
|
|
382
|
+
collector.pop("ok");
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
182
385
|
const result = evaluate(contract.formula, this._trace);
|
|
183
386
|
// The TS evaluator is currently a single-pass deterministic
|
|
184
387
|
// check; it doesn't separately track assumption vs guarantee
|
|
@@ -187,37 +390,71 @@ export class Sponsio {
|
|
|
187
390
|
const guaranteeSpan = new GuaranteeSpan(contract.desc, result);
|
|
188
391
|
collector.add(guaranteeSpan, result ? "ok" : "violated");
|
|
189
392
|
if (!result) {
|
|
190
|
-
|
|
393
|
+
// Per-contract `mode:` overrides the global mode for this one
|
|
394
|
+
// verdict — same routing as Python's RuntimeMonitor.
|
|
395
|
+
const effMode = contract.mode ?? this.mode;
|
|
396
|
+
// The contract's strategy decides which outcome this is, so a
|
|
397
|
+
// `redirect_to_safe` violation reports `redirected` with its
|
|
398
|
+
// substitute instead of collapsing into a plain block. Observe
|
|
399
|
+
// never stops, whatever the strategy says.
|
|
400
|
+
const action = effMode === "observe"
|
|
401
|
+
? "observed"
|
|
402
|
+
: contract.strategy === "redirect"
|
|
403
|
+
? "redirected"
|
|
404
|
+
: contract.strategy === "escalate"
|
|
405
|
+
? "escalated"
|
|
406
|
+
: "blocked";
|
|
407
|
+
if (isStoppingAction(action))
|
|
408
|
+
anyStopping = true;
|
|
409
|
+
const verb = effMode === "observe" ? "WOULD-BLOCK" : "BLOCKED";
|
|
191
410
|
const msg = `${verb}: ${this.agentId}.${toolName} — det constraint violated: ${contract.desc}`;
|
|
192
411
|
violations.push(msg);
|
|
193
412
|
violatedDescs.push(contract.desc);
|
|
413
|
+
const safe = action === "redirected" ? contract.safeName : undefined;
|
|
194
414
|
detViolations.push({
|
|
195
415
|
desc: contract.desc,
|
|
196
416
|
message: msg,
|
|
417
|
+
action,
|
|
418
|
+
...(safe !== undefined ? { redirectedTo: safe, alternatives: [safe] } : {}),
|
|
197
419
|
ruleId: contract.patternName || contract.desc,
|
|
198
420
|
agentMsg: `The action \`${toolName}\` was rejected by policy ` +
|
|
199
421
|
`(${contract.patternName || contract.desc}): ${contract.desc}. ` +
|
|
200
|
-
|
|
422
|
+
(safe !== undefined
|
|
423
|
+
? `Call \`${safe}\` instead.`
|
|
424
|
+
: `Choose a different approach.`),
|
|
201
425
|
});
|
|
202
426
|
// Nest violation + enforcement spans under the failed
|
|
203
427
|
// guarantee so the renderer can pick up the verdict word.
|
|
204
|
-
const
|
|
428
|
+
const strategyName = contract.strategy === "redirect"
|
|
429
|
+
? "RedirectToSafe"
|
|
430
|
+
: contract.strategy === "escalate"
|
|
431
|
+
? "EscalateToHuman"
|
|
432
|
+
: "DetBlock";
|
|
433
|
+
const enforce = new EnforcementSpan(strategyName, action);
|
|
205
434
|
guaranteeSpan.children.push(enforce);
|
|
206
435
|
enforce.finish("violated");
|
|
207
436
|
}
|
|
208
437
|
collector.pop(result ? "ok" : "violated");
|
|
209
438
|
}
|
|
210
439
|
const hasViolations = violations.length > 0;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
440
|
+
const stopping = anyStopping;
|
|
441
|
+
const turn = collector.finishRoot(stopping, this._contracts.length, violations.length);
|
|
442
|
+
this._turnSpans.push(turn);
|
|
443
|
+
this._runSpans.push({ span: turn, violations: [...detViolations] });
|
|
444
|
+
if (stopping) {
|
|
214
445
|
this._trace.pop();
|
|
215
446
|
this._state = snapshot;
|
|
216
447
|
this._violations.push(...violations);
|
|
217
448
|
this._logViolations(toolName, violations, violatedDescs, "blocked");
|
|
449
|
+
// `allowed` tracks `blocked` alone, not every stopping outcome:
|
|
450
|
+
// Python computes `not any(action == "blocked")`, so a redirect
|
|
451
|
+
// stays `allowed: true` because the agent flow continues down the
|
|
452
|
+
// safe path. `stopOriginal` is the field that says the original
|
|
453
|
+
// call must not run.
|
|
454
|
+
const derived = deriveVerdict(detViolations);
|
|
218
455
|
return {
|
|
219
|
-
|
|
220
|
-
allowed:
|
|
456
|
+
...derived,
|
|
457
|
+
allowed: !derived.blocked,
|
|
221
458
|
message: violations[0],
|
|
222
459
|
violations,
|
|
223
460
|
detViolations,
|
|
@@ -236,7 +473,7 @@ export class Sponsio {
|
|
|
236
473
|
this._logAllow(toolName);
|
|
237
474
|
}
|
|
238
475
|
return {
|
|
239
|
-
|
|
476
|
+
...deriveVerdict(detViolations),
|
|
240
477
|
allowed: true,
|
|
241
478
|
message: "",
|
|
242
479
|
violations: [],
|
|
@@ -257,6 +494,63 @@ export class Sponsio {
|
|
|
257
494
|
/** Drop accumulated turn spans (used between sessions in tests). */
|
|
258
495
|
resetSession() {
|
|
259
496
|
this._turnSpans = [];
|
|
497
|
+
this._runSpans = [];
|
|
498
|
+
this._sessionId = newSessionId();
|
|
499
|
+
this._startedAt = Date.now();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* This run, in the shape the cloud stores.
|
|
503
|
+
*
|
|
504
|
+
* Exposed so a caller can see exactly what would be uploaded before
|
|
505
|
+
* uploading anything, which is the question a customer under a data
|
|
506
|
+
* agreement asks first.
|
|
507
|
+
*/
|
|
508
|
+
viewModel() {
|
|
509
|
+
return buildViewModel({
|
|
510
|
+
agentId: this.agentId,
|
|
511
|
+
mode: this.mode,
|
|
512
|
+
sessionId: this._sessionId,
|
|
513
|
+
startedAt: this._startedAt,
|
|
514
|
+
contracts: this._contracts,
|
|
515
|
+
spans: this._runSpans,
|
|
516
|
+
privacyLevel: privacy.resolve(),
|
|
517
|
+
...(this._rulebookStamp === undefined
|
|
518
|
+
? {}
|
|
519
|
+
: { rulebookStamp: this._rulebookStamp }),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Upload this run.
|
|
524
|
+
*
|
|
525
|
+
* Best-effort by construction: it resolves false rather than throwing,
|
|
526
|
+
* because losing telemetry must never change what an agent does, and a
|
|
527
|
+
* rejected upload at the end of a run is not a reason to fail the run.
|
|
528
|
+
* Silent only about the network; a misconfiguration is worth saying
|
|
529
|
+
* once, since the symptom otherwise is a customer with no runs and no
|
|
530
|
+
* explanation.
|
|
531
|
+
*/
|
|
532
|
+
async uploadSession(opts = {}) {
|
|
533
|
+
const client = opts.client ?? new CloudClient();
|
|
534
|
+
if (!client.configured)
|
|
535
|
+
return false;
|
|
536
|
+
// A per-customer key resolves to its own project and must not name
|
|
537
|
+
// another, so passing nothing is both correct and safest here.
|
|
538
|
+
const project = opts.project === undefined ? readProject() : opts.project;
|
|
539
|
+
try {
|
|
540
|
+
await client.ingestSession(project ?? null, {
|
|
541
|
+
key: this._sessionId,
|
|
542
|
+
live: false,
|
|
543
|
+
vm: this.viewModel(),
|
|
544
|
+
});
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
if ((process.env.SPONSIO_QUIET ?? "") !== "1") {
|
|
549
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
550
|
+
process.stderr.write(`sponsio: run not uploaded (${msg})\n`);
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
260
554
|
}
|
|
261
555
|
/**
|
|
262
556
|
* Render the end-of-session view to ``out`` (defaults to stderr) —
|
|
@@ -279,9 +573,183 @@ export class Sponsio {
|
|
|
279
573
|
callCounts: { ...this._state.callCounts },
|
|
280
574
|
callWithCounts: { ...this._state.callWithCounts },
|
|
281
575
|
lastTool: this._state.lastTool,
|
|
576
|
+
lastToolAliases: [...this._state.lastToolAliases],
|
|
282
577
|
consecutiveCounts: { ...this._state.consecutiveCounts },
|
|
283
578
|
tokenCounts: { ...this._state.tokenCounts },
|
|
284
579
|
delegationDepth: this._state.delegationDepth,
|
|
580
|
+
currentCtx: { ...this._state.currentCtx },
|
|
581
|
+
now: this._state.now,
|
|
582
|
+
lastTs: { ...this._state.lastTs },
|
|
583
|
+
trueAtPrev: new Set(this._state.trueAtPrev),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Push external facts into the grounding context. Mirrors Python's
|
|
588
|
+
* ``BaseGuard.observe_context`` — every subsequent event sees the
|
|
589
|
+
* keys as ``ctx(k, v)`` atoms, so contracts like
|
|
590
|
+
* ``ctx_required("wire", "caller_id", ["alice"])`` can fire.
|
|
591
|
+
*
|
|
592
|
+
* Emits a synthetic ``context_update`` event into the trace so
|
|
593
|
+
* patterns that anchor on the moment a fact was set (notably
|
|
594
|
+
* ``time_since(ctx(approval.role, alice))``) start their clock here.
|
|
595
|
+
*/
|
|
596
|
+
observeContext(ctx) {
|
|
597
|
+
const event = {
|
|
598
|
+
tool: "",
|
|
599
|
+
event_type: "context_update",
|
|
600
|
+
args: ctx,
|
|
601
|
+
};
|
|
602
|
+
const valuation = groundEvent(event, this._state, this._contentAtoms);
|
|
603
|
+
this._trace.push(valuation);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Convenience helper for the approval-active gate: pushes
|
|
607
|
+
* ``approval.role`` / ``approval.decision`` (+ optional ``approver``
|
|
608
|
+
* / ``reason``) via ``observeContext``. Pairs with the
|
|
609
|
+
* ``approval_active`` pattern.
|
|
610
|
+
*/
|
|
611
|
+
observeApproval(opts) {
|
|
612
|
+
const ctx = {
|
|
613
|
+
"approval.role": opts.role,
|
|
614
|
+
"approval.decision": opts.decision ?? "allow",
|
|
615
|
+
};
|
|
616
|
+
if (opts.approver)
|
|
617
|
+
ctx["approval.approver"] = opts.approver;
|
|
618
|
+
if (opts.reason)
|
|
619
|
+
ctx["approval.reason"] = opts.reason;
|
|
620
|
+
this.observeContext(ctx);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Record an LLM response into the trace. Drives ``max_length``,
|
|
624
|
+
* ``no_pii``, ``no_keywords`` (any pattern that grounds against
|
|
625
|
+
* ``llm_said`` / ``response_words`` / ``response_chars``). Mirrors
|
|
626
|
+
* the Python ``llm_response`` event path.
|
|
627
|
+
*
|
|
628
|
+
* Returns a ``CheckResult`` so callers can branch on observe-mode
|
|
629
|
+
* violations the same way they branch on ``guardBefore``. In
|
|
630
|
+
* enforce mode, ``blocked: true`` rolls back the synthetic event.
|
|
631
|
+
*/
|
|
632
|
+
/**
|
|
633
|
+
* Record a claim-verification verdict in the trace and check the
|
|
634
|
+
* contracts that reference it.
|
|
635
|
+
*
|
|
636
|
+
* Grounds `claim_emitted(pred)`, `evidence_verdict(pred, V)` and
|
|
637
|
+
* `evidence_action(pred, a)` at one timestep, so contracts built from
|
|
638
|
+
* `claimRequiresEvidence` / `underdeterminedMustClarify` can decide.
|
|
639
|
+
* No verdict is computed here: the atoms restate what
|
|
640
|
+
* `/v1/evidence/verify` answered.
|
|
641
|
+
*
|
|
642
|
+
* Parity with feeding `EvidenceResult.to_event()` through Python's
|
|
643
|
+
* trace path.
|
|
644
|
+
*/
|
|
645
|
+
observeEvidence(predicate, verdict, action) {
|
|
646
|
+
return this._checkSynthetic({
|
|
647
|
+
tool: "",
|
|
648
|
+
event_type: "evidence",
|
|
649
|
+
key: predicate,
|
|
650
|
+
args: { verdict, action },
|
|
651
|
+
}, `<evidence:${predicate}>`);
|
|
652
|
+
}
|
|
653
|
+
observeResponse(content, opts = {}) {
|
|
654
|
+
const args = {};
|
|
655
|
+
if (opts.segment)
|
|
656
|
+
args.segment = opts.segment;
|
|
657
|
+
return this._checkSynthetic({ tool: "", event_type: "llm_response", content, args }, "<llm_response>");
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Check one synthetic (non-tool-call) event against every contract.
|
|
661
|
+
*
|
|
662
|
+
* The output lane and the evidence lane both push an event that is not
|
|
663
|
+
* a tool call and then judge the trace. Sharing the body keeps their
|
|
664
|
+
* verdicts identical: a second copy is how one lane ends up with a
|
|
665
|
+
* rollback the other lacks.
|
|
666
|
+
*
|
|
667
|
+
* `label` names the subject in violation messages and session logs.
|
|
668
|
+
*/
|
|
669
|
+
_checkSynthetic(event, label) {
|
|
670
|
+
const snapshot = this._snapshotState();
|
|
671
|
+
const valuation = groundEvent(event, this._state, this._contentAtoms);
|
|
672
|
+
this._trace.push(valuation);
|
|
673
|
+
const violations = [];
|
|
674
|
+
const violatedDescs = [];
|
|
675
|
+
const detViolations = [];
|
|
676
|
+
let anyStopping = false;
|
|
677
|
+
for (const contract of this._contracts) {
|
|
678
|
+
const result = evaluate(contract.formula, this._trace);
|
|
679
|
+
if (!result) {
|
|
680
|
+
const effMode = contract.mode ?? this.mode;
|
|
681
|
+
// No substitution on the output lane: there is no call to swap,
|
|
682
|
+
// so a redirect strategy still stops here. `escalate` keeps its
|
|
683
|
+
// own action, which does not stop.
|
|
684
|
+
const action = effMode === "observe"
|
|
685
|
+
? "observed"
|
|
686
|
+
: contract.strategy === "escalate"
|
|
687
|
+
? "escalated"
|
|
688
|
+
: "blocked";
|
|
689
|
+
if (isStoppingAction(action))
|
|
690
|
+
anyStopping = true;
|
|
691
|
+
const verb = effMode === "observe" ? "WOULD-BLOCK" : "BLOCKED";
|
|
692
|
+
const msg = `${verb}: ${this.agentId}.${label} — det constraint violated: ${contract.desc}`;
|
|
693
|
+
violations.push(msg);
|
|
694
|
+
violatedDescs.push(contract.desc);
|
|
695
|
+
detViolations.push({
|
|
696
|
+
desc: contract.desc,
|
|
697
|
+
message: msg,
|
|
698
|
+
action,
|
|
699
|
+
ruleId: contract.patternName || contract.desc,
|
|
700
|
+
agentMsg: `LLM response was rejected by policy ` +
|
|
701
|
+
`(${contract.patternName || contract.desc}): ${contract.desc}.`,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const hasViolations = violations.length > 0;
|
|
706
|
+
const stopping = anyStopping;
|
|
707
|
+
// The output lane builds no span tree, so without this a rule that
|
|
708
|
+
// fires on what the model said was enforced locally and never
|
|
709
|
+
// uploaded: at `full` a TypeScript run showed half the run a Python
|
|
710
|
+
// one showed. A bare turn is enough for the cloud, which reads the
|
|
711
|
+
// step's type, tool and verdict and not the tree under it.
|
|
712
|
+
const outTurn = new AgentTurnSpan(this.agentId, label);
|
|
713
|
+
outTurn.totalContractsChecked = this._contracts.length;
|
|
714
|
+
outTurn.detViolations = violations.length;
|
|
715
|
+
outTurn.blocked = stopping;
|
|
716
|
+
outTurn.finish(hasViolations ? "violated" : "ok");
|
|
717
|
+
this._runSpans.push({
|
|
718
|
+
span: outTurn,
|
|
719
|
+
violations: [...detViolations],
|
|
720
|
+
type: "output",
|
|
721
|
+
});
|
|
722
|
+
if (stopping) {
|
|
723
|
+
this._trace.pop();
|
|
724
|
+
this._state = snapshot;
|
|
725
|
+
this._violations.push(...violations);
|
|
726
|
+
this._logViolations(label, violations, violatedDescs, "blocked");
|
|
727
|
+
// `allowed` tracks `blocked` alone, not every stopping outcome:
|
|
728
|
+
// Python computes `not any(action == "blocked")`, so a redirect
|
|
729
|
+
// stays `allowed: true` because the agent flow continues down the
|
|
730
|
+
// safe path. `stopOriginal` is the field that says the original
|
|
731
|
+
// call must not run.
|
|
732
|
+
const derived = deriveVerdict(detViolations);
|
|
733
|
+
return {
|
|
734
|
+
...derived,
|
|
735
|
+
allowed: !derived.blocked,
|
|
736
|
+
message: violations[0],
|
|
737
|
+
violations,
|
|
738
|
+
detViolations,
|
|
739
|
+
stoViolations: [],
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (hasViolations) {
|
|
743
|
+
this._violations.push(...violations);
|
|
744
|
+
this._logViolations(label, violations, violatedDescs, "observed");
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
...deriveVerdict(detViolations),
|
|
748
|
+
allowed: true,
|
|
749
|
+
message: "",
|
|
750
|
+
violations: [],
|
|
751
|
+
detViolations,
|
|
752
|
+
stoViolations: [],
|
|
285
753
|
};
|
|
286
754
|
}
|
|
287
755
|
_logViolations(toolName, messages, descs, action) {
|
|
@@ -312,122 +780,32 @@ export class Sponsio {
|
|
|
312
780
|
});
|
|
313
781
|
}
|
|
314
782
|
/**
|
|
315
|
-
* Record tool output after execution
|
|
783
|
+
* Record tool output after execution.
|
|
316
784
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
785
|
+
* This build ships **no sto pipeline** (the engine is deterministic-only).
|
|
786
|
+
* Stochastic (LLM-judged) atoms are the extension point that would
|
|
787
|
+
* populate ``stoViolations``; with no external implementation and no
|
|
788
|
+
* sto contracts loaded (sto entries in yaml are rejected at config
|
|
789
|
+
* load), ``stoViolations`` is always ``[]`` and ``allowed: true``.
|
|
322
790
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
791
|
+
* The method stays ``async`` for API parity: an external subclass
|
|
792
|
+
* or wrapper can run the judge pipeline and surface real
|
|
793
|
+
* ``stoViolations`` without changing the call site. In **enforce
|
|
794
|
+
* mode** such subclasses can flip ``blocked: true`` on the first
|
|
795
|
+
* sto violation; the tool output has already executed, so the
|
|
796
|
+
* caller is responsible for routing the result (retry with
|
|
797
|
+
* feedback, redirect to safe, etc.).
|
|
328
798
|
*/
|
|
329
|
-
async guardAfter(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const flatMessages = [];
|
|
335
|
-
for (const sto of this._stoContracts) {
|
|
336
|
-
try {
|
|
337
|
-
const result = await sto.evaluator.evaluate({
|
|
338
|
-
toolName,
|
|
339
|
-
output,
|
|
340
|
-
context: this._stoContext,
|
|
341
|
-
});
|
|
342
|
-
if (result.passed) {
|
|
343
|
-
this._logSto(sto.desc, "allowed", result.score, result.evidence);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
const verb = this.mode === "observe" ? "WOULD-BLOCK" : "BLOCKED";
|
|
347
|
-
const msg = `${verb}: ${this.agentId}.${toolName} — sto constraint ` +
|
|
348
|
-
`violated: ${sto.desc} (score=${result.score.toFixed(2)})`;
|
|
349
|
-
// Sto outcomes default to retry-style phrasing — the model
|
|
350
|
-
// is being asked to regenerate addressing the failed
|
|
351
|
-
// property, not to abandon the action altogether. retryHint
|
|
352
|
-
// carries the evaluator's suggestion when available.
|
|
353
|
-
stoViolations.push({
|
|
354
|
-
desc: sto.desc,
|
|
355
|
-
message: msg,
|
|
356
|
-
ruleId: sto.desc,
|
|
357
|
-
agentMsg: `Your output failed the \`${sto.desc}\` check ` +
|
|
358
|
-
`(score=${result.score.toFixed(2)}). Regenerate addressing the issue.`,
|
|
359
|
-
retryHint: result.evidence || undefined,
|
|
360
|
-
});
|
|
361
|
-
flatMessages.push(msg);
|
|
362
|
-
this._logSto(sto.desc, this.mode === "observe" ? "observed" : "blocked", result.score, result.evidence);
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
366
|
-
const handling = this._judgeFallback;
|
|
367
|
-
if (handling === "skip") {
|
|
368
|
-
console.warn(`[sponsio] sto ${sto.desc}: judge error (${errMsg}) — skipping`);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const verb = this.mode === "observe" ? "WOULD-BLOCK" : "BLOCKED";
|
|
372
|
-
if (handling === "deny") {
|
|
373
|
-
const msg = `${verb}: ${this.agentId}.${toolName} — sto judge error ` +
|
|
374
|
-
`(${errMsg}); fallback_mode=deny`;
|
|
375
|
-
stoViolations.push({ desc: sto.desc, message: msg });
|
|
376
|
-
flatMessages.push(msg);
|
|
377
|
-
this._logSto(sto.desc, this.mode === "observe" ? "observed" : "blocked", undefined, `judge-error: ${errMsg}`);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
// allow (default) — log a pass so the operator can see the
|
|
381
|
-
// judge was exercised, but do not create a violation.
|
|
382
|
-
console.warn(`[sponsio] sto ${sto.desc}: judge error (${errMsg}) — allowing`);
|
|
383
|
-
this._logSto(sto.desc, "allowed", undefined, `judge-error-allowed: ${errMsg}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (stoViolations.length === 0) {
|
|
388
|
-
return emptyAllow();
|
|
389
|
-
}
|
|
390
|
-
this._violations.push(...flatMessages);
|
|
391
|
-
if (this.mode === "enforce") {
|
|
392
|
-
return {
|
|
393
|
-
blocked: true,
|
|
394
|
-
allowed: false,
|
|
395
|
-
message: flatMessages[0],
|
|
396
|
-
violations: flatMessages,
|
|
397
|
-
detViolations: [],
|
|
398
|
-
stoViolations,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
blocked: false,
|
|
403
|
-
allowed: true,
|
|
404
|
-
message: "",
|
|
405
|
-
violations: [],
|
|
406
|
-
detViolations: [],
|
|
407
|
-
stoViolations,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
_logSto(desc, action, score, evidence) {
|
|
411
|
-
if (!this._logger)
|
|
412
|
-
return;
|
|
413
|
-
const record = {
|
|
414
|
-
ts: Date.now() / 1000,
|
|
415
|
-
agent_id: this.agentId,
|
|
416
|
-
action,
|
|
417
|
-
pipeline: "sto",
|
|
418
|
-
constraint: desc,
|
|
419
|
-
result: {
|
|
420
|
-
action,
|
|
421
|
-
message: evidence ?? "",
|
|
422
|
-
},
|
|
423
|
-
};
|
|
424
|
-
if (score !== undefined) {
|
|
425
|
-
record.sto = evidence ? { score, evidence } : { score };
|
|
426
|
-
}
|
|
427
|
-
this._logger.log(record);
|
|
799
|
+
async guardAfter(_toolName, _output = "") {
|
|
800
|
+
// This build is deterministic-only, the sto pipeline is not
|
|
801
|
+
// implemented. Always return clean-pass; external subclasses can
|
|
802
|
+
// override.
|
|
803
|
+
return emptyAllow();
|
|
428
804
|
}
|
|
429
805
|
/**
|
|
430
|
-
* Reset guard state for a new session.
|
|
806
|
+
* Reset guard state for a new session. Clears the trace, the
|
|
807
|
+
* grounding accumulators (including ``currentCtx``, ``lastTs``,
|
|
808
|
+
* event clock), the violation log, and the sto-context snapshot.
|
|
431
809
|
*/
|
|
432
810
|
reset() {
|
|
433
811
|
this._trace = [];
|
|
@@ -437,11 +815,15 @@ export class Sponsio {
|
|
|
437
815
|
this._contentAtoms = collectContentAtoms(this._contracts.map((c) => c.formula));
|
|
438
816
|
}
|
|
439
817
|
/**
|
|
440
|
-
* Stash per-turn context for the sto pipeline.
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
818
|
+
* Stash per-turn context for the sto pipeline.
|
|
819
|
+
*
|
|
820
|
+
* Not supported in this build (the engine is deterministic-only).
|
|
821
|
+
* Atoms that need grounding (``relevance`` -> ``query``,
|
|
822
|
+
* ``hallucination_free`` -> ``source``, ``scope_respect`` ->
|
|
823
|
+
* ``scope`` override, ``metric_integrity`` -> ``history``) would
|
|
824
|
+
* read from this snapshot on the next ``guardAfter`` call. This
|
|
825
|
+
* build accepts the call as a no-op so shared agent code can stay
|
|
826
|
+
* framework-neutral; the snapshot is never consumed.
|
|
445
827
|
*
|
|
446
828
|
* Merges with any previously-set context; pass ``{ query: undefined }``
|
|
447
829
|
* to explicitly clear a field. ``reset()`` clears the whole snapshot.
|
|
@@ -499,12 +881,13 @@ function resolveMode(ctorMode, yamlMode) {
|
|
|
499
881
|
}
|
|
500
882
|
let _skippedWarned = false;
|
|
501
883
|
// Note: ``resolveJudge`` / ``buildStoContract`` / ``isJudgeClient``
|
|
502
|
-
// helpers were removed when the sto pipeline became
|
|
503
|
-
//
|
|
504
|
-
// specs flow through the ``yamlSkipped`` warning path in
|
|
884
|
+
// helpers were removed when the sto pipeline became an external
|
|
885
|
+
// extension point. This build never constructs a judge or evaluator;
|
|
886
|
+
// yaml sto specs flow through the ``yamlSkipped`` warning path in
|
|
887
|
+
// the ctor.
|
|
505
888
|
function emptyAllow() {
|
|
506
889
|
return {
|
|
507
|
-
|
|
890
|
+
...deriveVerdict([]),
|
|
508
891
|
allowed: true,
|
|
509
892
|
message: "",
|
|
510
893
|
violations: [],
|
|
@@ -533,8 +916,12 @@ function warnOnceAboutSkipped(skipped) {
|
|
|
533
916
|
if (unknown > 0) {
|
|
534
917
|
bits.push(`${unknown} unrecognised contract entr${unknown === 1 ? "y" : "ies"}`);
|
|
535
918
|
}
|
|
919
|
+
const stoNote = sto > 0
|
|
920
|
+
? " Sto (LLM-judge) contracts are not supported in this build (the engine is deterministic-only)."
|
|
921
|
+
: "";
|
|
536
922
|
console.warn("[sponsio] skipped unsupported yaml items: " +
|
|
537
923
|
bits.join("; ") +
|
|
538
|
-
".
|
|
924
|
+
"." +
|
|
925
|
+
stoNote);
|
|
539
926
|
}
|
|
540
927
|
//# sourceMappingURL=index.js.map
|