@kindgi/guardrails 0.0.0-bootstrap.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +78 -1
  3. package/dist/action-handler.d.ts +82 -0
  4. package/dist/action-handler.d.ts.map +1 -0
  5. package/dist/action-handler.js +120 -0
  6. package/dist/action-handler.js.map +1 -0
  7. package/dist/checks.d.ts +9 -0
  8. package/dist/checks.d.ts.map +1 -0
  9. package/dist/checks.js +235 -0
  10. package/dist/checks.js.map +1 -0
  11. package/dist/define-check.d.ts +93 -0
  12. package/dist/define-check.d.ts.map +1 -0
  13. package/dist/define-check.js +110 -0
  14. package/dist/define-check.js.map +1 -0
  15. package/dist/define.d.ts +27 -0
  16. package/dist/define.d.ts.map +1 -0
  17. package/dist/define.js +126 -0
  18. package/dist/define.js.map +1 -0
  19. package/dist/engine.d.ts +49 -0
  20. package/dist/engine.d.ts.map +1 -0
  21. package/dist/engine.js +198 -0
  22. package/dist/engine.js.map +1 -0
  23. package/dist/errors.d.ts +91 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +4 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/execution-strategy.d.ts +80 -0
  28. package/dist/execution-strategy.d.ts.map +1 -0
  29. package/dist/execution-strategy.js +96 -0
  30. package/dist/execution-strategy.js.map +1 -0
  31. package/dist/guardrail.schema.json +261 -0
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/judge.d.ts +31 -0
  37. package/dist/judge.d.ts.map +1 -0
  38. package/dist/judge.js +171 -0
  39. package/dist/judge.js.map +1 -0
  40. package/dist/types.d.ts +407 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +11 -0
  43. package/dist/types.js.map +1 -0
  44. package/package.json +64 -4
  45. package/src/action-handler.ts +179 -0
  46. package/src/checks.ts +236 -0
  47. package/src/define-check.ts +207 -0
  48. package/src/define.ts +146 -0
  49. package/src/engine.ts +271 -0
  50. package/src/errors.ts +107 -0
  51. package/src/execution-strategy.ts +184 -0
  52. package/src/guardrail.schema.json +261 -0
  53. package/src/index.ts +79 -0
  54. package/src/judge.ts +221 -0
  55. package/src/types.ts +455 -0
package/src/define.ts ADDED
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { createSpecRegistry } from '@kindgi/schema';
5
+ import type { SpecRegistry } from '@kindgi/schema';
6
+ import type { Result } from '@kindgi/types';
7
+
8
+ import type {
9
+ InvalidCheckConfigError,
10
+ InvalidGuardrailError,
11
+ UnknownCheckError,
12
+ } from './errors.js';
13
+ import guardrailSchema from './guardrail.schema.json' with { type: 'json' };
14
+ import type { CheckRegistry, Guardrail } from './types.js';
15
+
16
+ const GUARDRAIL_SCHEMA_ID = 'https://kindgi.com/schemas/v1/guardrail.schema.json';
17
+
18
+ let cachedRegistry: SpecRegistry | undefined;
19
+ function schemaRegistry(): SpecRegistry {
20
+ if (cachedRegistry !== undefined) return cachedRegistry;
21
+ const built = createSpecRegistry([guardrailSchema]);
22
+ if (built.kind === 'err') {
23
+ throw new Error(`@kindgi/guardrails: bundled schema failed to compile: ${built.error.message}`);
24
+ }
25
+ cachedRegistry = built.value;
26
+ return cachedRegistry;
27
+ }
28
+
29
+ /**
30
+ * Validate a guardrail declaration + resolve its check.
31
+ *
32
+ * Fails if:
33
+ * 1. The declaration doesn't match `@kindgi/specs/guardrail.schema.json`.
34
+ * 2. The `check` id isn't in the registry.
35
+ * 3. The registered check's `kind` differs from the guardrail's `kind`.
36
+ * 4. The check has a config validator and `guardrail.config` fails it.
37
+ *
38
+ * Every failure surfaces at pack-init — before any run touches the guardrail.
39
+ */
40
+ export function defineGuardrail(
41
+ spec: Guardrail,
42
+ checks: CheckRegistry,
43
+ ): Result<Guardrail, InvalidGuardrailError | UnknownCheckError | InvalidCheckConfigError> {
44
+ const r = schemaRegistry().validate<Guardrail>(GUARDRAIL_SCHEMA_ID, spec);
45
+ if (r.kind === 'err') {
46
+ const err = r.error;
47
+ if (err.code !== 'validation-error') {
48
+ throw new Error(`@kindgi/guardrails: unexpected schema error ${err.code}: ${err.message}`);
49
+ }
50
+ const issues = err.errors.map((e) => {
51
+ const ajv = e as { instancePath?: unknown; message?: unknown };
52
+ return {
53
+ path: typeof ajv.instancePath === 'string' ? ajv.instancePath : '',
54
+ message: typeof ajv.message === 'string' ? ajv.message : 'validation failed',
55
+ };
56
+ });
57
+ return {
58
+ kind: 'err',
59
+ error: { code: 'invalid-guardrail', message: err.message, issues },
60
+ };
61
+ }
62
+
63
+ const check = checks.get(spec.check);
64
+ if (check === undefined) {
65
+ return {
66
+ kind: 'err',
67
+ error: {
68
+ code: 'unknown-check',
69
+ message: `Guardrail "${spec.id}" references unregistered check "${spec.check}"`,
70
+ guardrailId: spec.id,
71
+ checkId: spec.check,
72
+ },
73
+ };
74
+ }
75
+
76
+ if (check.kind !== spec.kind) {
77
+ return {
78
+ kind: 'err',
79
+ error: {
80
+ code: 'invalid-guardrail',
81
+ message: `Guardrail "${spec.id}" kind "${spec.kind}" does not match check "${spec.check}" kind "${check.kind}"`,
82
+ issues: [{ path: '/kind', message: `expected "${check.kind}"` }],
83
+ },
84
+ };
85
+ }
86
+
87
+ if (check.validateConfig !== undefined && spec.config !== undefined) {
88
+ const reason = check.validateConfig(spec.config);
89
+ if (reason !== undefined) {
90
+ return {
91
+ kind: 'err',
92
+ error: {
93
+ code: 'invalid-check-config',
94
+ message: `Guardrail "${spec.id}" config invalid: ${reason}`,
95
+ guardrailId: spec.id,
96
+ reason,
97
+ },
98
+ };
99
+ }
100
+ }
101
+
102
+ return { kind: 'ok', value: spec };
103
+ }
104
+
105
+ /**
106
+ * Validate an arbitrary wire spec against `@kindgi/specs/guardrail.schema.json`.
107
+ * Unlike `defineGuardrail`, this does NOT resolve the `check` reference
108
+ * against a `CheckRegistry` — used by transport layers (e.g. the
109
+ * `@kindgi/api` guardrail routes) that accept metadata-only guardrail
110
+ * registrations. The check implementation must already be available to
111
+ * the server that evaluates the guardrail.
112
+ */
113
+ export function validateGuardrailSpec(spec: unknown): Result<Guardrail, InvalidGuardrailError> {
114
+ if (spec === null || typeof spec !== 'object') {
115
+ return {
116
+ kind: 'err',
117
+ error: {
118
+ code: 'invalid-guardrail',
119
+ message: 'Guardrail spec must be an object',
120
+ issues: [{ path: '', message: 'must be an object' }],
121
+ },
122
+ };
123
+ }
124
+ const r = schemaRegistry().validate<Guardrail>(GUARDRAIL_SCHEMA_ID, spec);
125
+ if (r.kind === 'err') {
126
+ const err = r.error;
127
+ if (err.code !== 'validation-error') {
128
+ throw new Error(`@kindgi/guardrails: unexpected schema error ${err.code}: ${err.message}`);
129
+ }
130
+ const issues = err.errors.map((e) => {
131
+ const ajv = e as { instancePath?: unknown; message?: unknown };
132
+ return {
133
+ path: typeof ajv.instancePath === 'string' ? ajv.instancePath : '',
134
+ message: typeof ajv.message === 'string' ? ajv.message : 'validation failed',
135
+ };
136
+ });
137
+ return {
138
+ kind: 'err',
139
+ error: { code: 'invalid-guardrail', message: err.message, issues },
140
+ };
141
+ }
142
+ return { kind: 'ok', value: spec as Guardrail };
143
+ }
144
+
145
+ /** The `$id` of the JSON Schema this loader validates against. */
146
+ export const GUARDRAIL_SCHEMA_URI = GUARDRAIL_SCHEMA_ID;
package/src/engine.ts ADDED
@@ -0,0 +1,271 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Timestamp } from '@kindgi/types';
5
+
6
+ import type { GuardrailError, JudgeMissingError, ScopeMismatchError } from './errors.js';
7
+ import {
8
+ type ExecutionStrategy,
9
+ type ExecutionStrategyRegistry,
10
+ createExecutionStrategyRegistry,
11
+ externalStrategy,
12
+ makeLlmJudgeStrategy,
13
+ zeroLlmStrategy,
14
+ } from './execution-strategy.js';
15
+ import { type LlmJudgeConfig, invokeJudge } from './judge.js';
16
+ import type {
17
+ CheckRegistry,
18
+ CheckResult,
19
+ EvaluationBindings,
20
+ EvaluationResult,
21
+ Guardrail,
22
+ RunTrace,
23
+ Scope,
24
+ } from './types.js';
25
+
26
+ /**
27
+ * Default strategy registry seeded with the built-ins (`zero-llm`,
28
+ * `llm-judge`, `external`). Cached so repeated `evaluateGuardrail`
29
+ * calls don't reinstantiate the registry.
30
+ */
31
+ let defaultRegistry: ExecutionStrategyRegistry | undefined;
32
+ function getDefaultStrategyRegistry(): ExecutionStrategyRegistry {
33
+ if (defaultRegistry === undefined) {
34
+ defaultRegistry = createExecutionStrategyRegistry([
35
+ zeroLlmStrategy,
36
+ makeLlmJudgeStrategy((config, capability, trace, bindings) =>
37
+ invokeJudge(config as LlmJudgeConfig, capability, trace, bindings),
38
+ ),
39
+ externalStrategy,
40
+ ]);
41
+ }
42
+ return defaultRegistry;
43
+ }
44
+
45
+ /** Expose the built-in registry so callers can extend it with adapter strategies. */
46
+ export function builtInStrategies(): readonly ExecutionStrategy[] {
47
+ return getDefaultStrategyRegistry().list();
48
+ }
49
+
50
+ /**
51
+ * Evaluate one guardrail against a run trace. Handles:
52
+ * - Scope check (mode + agent + flow + tenant filters).
53
+ * - Dispatch by `kind` through `bindings.strategies` (or the built-ins):
54
+ * zero-llm → check.evaluate; llm-judge → invokeJudge; external → an
55
+ * `invalid-guardrail` error unless the caller registered its own
56
+ * `external` strategy.
57
+ * - Action derivation from `guardrail.action.on-violation`.
58
+ * - Compliance evidence emission on violation (if bindings.compliance
59
+ * is set and the trace carries a `projectId`).
60
+ * - Action-handler invocation on violation (if bindings.actions is set);
61
+ * an action with no registered handler returns `unknown-action`.
62
+ *
63
+ * Returns:
64
+ * - `{ kind: 'ok', value: EvaluationResult }` — guardrail applied + evaluated.
65
+ * - `{ kind: 'skip', reason }` — guardrail didn't apply (scope mismatch).
66
+ * - `{ kind: 'err', error }` — evaluation failed (unknown kind or check,
67
+ * judge missing or unroutable), or the violation's action has no
68
+ * handler in `bindings.actions` (`unknown-action`, after the violation
69
+ * was recorded).
70
+ */
71
+ export type EvaluationOutcome =
72
+ | { readonly kind: 'ok'; readonly value: EvaluationResult }
73
+ | { readonly kind: 'skip'; readonly reason: ScopeMismatchError }
74
+ | { readonly kind: 'err'; readonly error: GuardrailError | JudgeMissingError };
75
+
76
+ export async function evaluateGuardrail(
77
+ guardrail: Guardrail,
78
+ checks: CheckRegistry,
79
+ trace: RunTrace,
80
+ bindings: EvaluationBindings = {},
81
+ ): Promise<EvaluationOutcome> {
82
+ const scope = checkScope(guardrail, trace);
83
+ if (scope !== null) return { kind: 'skip', reason: scope };
84
+
85
+ // Check-registry lookup is delegated to each strategy — `zero-llm`
86
+ // resolves the check inside its evaluate; strategies like
87
+ // `sandbox-code` or `external` don't consult the CheckRegistry at
88
+ // all. Keeps the engine strategy-agnostic.
89
+
90
+ const strategies = bindings.strategies ?? getDefaultStrategyRegistry();
91
+ const strategy = strategies.get(guardrail.kind);
92
+ if (strategy === undefined) {
93
+ return {
94
+ kind: 'err',
95
+ error: {
96
+ code: 'invalid-guardrail',
97
+ message: `Guardrail "${guardrail.id}" declares unknown kind "${guardrail.kind}" — no execution strategy registered`,
98
+ issues: [{ path: '/kind', message: `unknown kind: ${guardrail.kind}` }],
99
+ },
100
+ };
101
+ }
102
+ const strategyOutcome = await strategy.evaluate(guardrail, checks, trace, bindings);
103
+ if (strategyOutcome.kind === 'err') {
104
+ const err = strategyOutcome.error;
105
+ return {
106
+ kind: 'err',
107
+ error: {
108
+ code: err.code,
109
+ message: err.message,
110
+ ...(err.issues !== undefined && { issues: err.issues }),
111
+ guardrailId: guardrail.id,
112
+ } as GuardrailError | JudgeMissingError,
113
+ };
114
+ }
115
+ const result: CheckResult = strategyOutcome.value;
116
+
117
+ const action: EvaluationResult['action'] = result.passed
118
+ ? 'noop'
119
+ : guardrail.action['on-violation'];
120
+ const severity = guardrail.severity ?? 'error';
121
+ const evaluation: EvaluationResult = {
122
+ guardrailId: guardrail.id,
123
+ result,
124
+ action,
125
+ severity,
126
+ at: new Date().toISOString() as Timestamp,
127
+ };
128
+
129
+ if (!result.passed && bindings.compliance !== undefined && trace.projectId !== undefined) {
130
+ // Compliance emit needs `projectId` (every evidence record is
131
+ // project-scoped). Skip emit when the trace carries no project
132
+ // scope.
133
+ await bindings.compliance.emit({
134
+ tenantId: trace.tenantId,
135
+ projectId: trace.projectId,
136
+ kind: 'guardrail-violation',
137
+ outcome: 'failed',
138
+ payload: {
139
+ version: 1,
140
+ guardrailId: guardrail.id,
141
+ guardrailName: guardrail.name,
142
+ checkId: guardrail.check,
143
+ checkKind: guardrail.kind,
144
+ severity,
145
+ action: guardrail.action['on-violation'],
146
+ reason: result.reason,
147
+ judgeResponse: result.judgeResponse,
148
+ attributes: result.attributes,
149
+ },
150
+ ...(trace.runId !== undefined && {
151
+ provenanceRef: { runId: trace.runId },
152
+ }),
153
+ });
154
+ }
155
+
156
+ // Invoke the registered action handler. Handler errors are logged into
157
+ // the evaluation attributes but don't roll back the evaluation itself —
158
+ // the violation is still recorded. `on-violation` is an open string, so
159
+ // an action nobody registered a handler for fails here, by name.
160
+ if (bindings.actions !== undefined && !result.passed) {
161
+ const handler = bindings.actions.get(action);
162
+ if (handler === undefined) {
163
+ return {
164
+ kind: 'err',
165
+ error: {
166
+ code: 'unknown-action',
167
+ message: `Guardrail "${guardrail.id}" fired action "${action}", but no action handler is registered for it`,
168
+ guardrailId: guardrail.id,
169
+ action,
170
+ },
171
+ };
172
+ }
173
+ const applied = await handler.apply({ guardrail, evaluation, trace });
174
+ if (applied.kind === 'err') {
175
+ // Attach handler error to the evaluation attributes; don't fail.
176
+ (evaluation as { result: CheckResult }).result = {
177
+ ...evaluation.result,
178
+ attributes: {
179
+ ...(evaluation.result.attributes ?? {}),
180
+ actionHandlerError: applied.error,
181
+ },
182
+ };
183
+ }
184
+ }
185
+
186
+ return { kind: 'ok', value: evaluation };
187
+ }
188
+
189
+ /**
190
+ * Evaluate every guardrail in the list against a trace. Order-independent —
191
+ * each check is a pure function over the trace. Returns one outcome per
192
+ * guardrail.
193
+ */
194
+ export async function evaluateAll(
195
+ guardrails: readonly Guardrail[],
196
+ checks: CheckRegistry,
197
+ trace: RunTrace,
198
+ bindings: EvaluationBindings = {},
199
+ ): Promise<readonly EvaluationOutcome[]> {
200
+ return Promise.all(guardrails.map((inv) => evaluateGuardrail(inv, checks, trace, bindings)));
201
+ }
202
+
203
+ /**
204
+ * Extract only the violations from a batch of outcomes — convenience for
205
+ * callers that want to apply enforcement actions.
206
+ */
207
+ export function violations(outcomes: readonly EvaluationOutcome[]): readonly EvaluationResult[] {
208
+ const out: EvaluationResult[] = [];
209
+ for (const o of outcomes) {
210
+ if (o.kind === 'ok' && !o.value.result.passed) out.push(o.value);
211
+ }
212
+ return out;
213
+ }
214
+
215
+ function checkScope(guardrail: Guardrail, trace: RunTrace): ScopeMismatchError | null {
216
+ const scope: Scope = guardrail.scope ?? {};
217
+ const when = scope.when ?? 'always';
218
+ if (when === 'ci-only' && trace.mode !== 'ci') {
219
+ return {
220
+ code: 'scope-mismatch',
221
+ message: `guardrail "${guardrail.id}" is ci-only`,
222
+ guardrailId: guardrail.id,
223
+ reason: 'wrong-mode',
224
+ };
225
+ }
226
+ if (when === 'runtime-only' && trace.mode !== 'runtime') {
227
+ return {
228
+ code: 'scope-mismatch',
229
+ message: `guardrail "${guardrail.id}" is runtime-only`,
230
+ guardrailId: guardrail.id,
231
+ reason: 'wrong-mode',
232
+ };
233
+ }
234
+ if (
235
+ scope.agents !== undefined &&
236
+ scope.agents.length > 0 &&
237
+ (trace.agentId === undefined || !scope.agents.includes(trace.agentId))
238
+ ) {
239
+ return {
240
+ code: 'scope-mismatch',
241
+ message: `guardrail "${guardrail.id}" does not apply to agent ${String(trace.agentId)}`,
242
+ guardrailId: guardrail.id,
243
+ reason: 'wrong-agent',
244
+ };
245
+ }
246
+ if (
247
+ scope.flows !== undefined &&
248
+ scope.flows.length > 0 &&
249
+ (trace.flowId === undefined || !scope.flows.includes(trace.flowId))
250
+ ) {
251
+ return {
252
+ code: 'scope-mismatch',
253
+ message: `guardrail "${guardrail.id}" does not apply to flow ${String(trace.flowId)}`,
254
+ guardrailId: guardrail.id,
255
+ reason: 'wrong-flow',
256
+ };
257
+ }
258
+ if (
259
+ scope.tenants !== undefined &&
260
+ scope.tenants.length > 0 &&
261
+ !scope.tenants.includes(trace.tenantId)
262
+ ) {
263
+ return {
264
+ code: 'scope-mismatch',
265
+ message: `guardrail "${guardrail.id}" does not apply to tenant ${trace.tenantId}`,
266
+ guardrailId: guardrail.id,
267
+ reason: 'wrong-tenant',
268
+ };
269
+ }
270
+ return null;
271
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { GuardrailId } from '@kindgi/types';
5
+
6
+ /**
7
+ * Errors emitted by @kindgi/guardrails. Every variant carries a `code`
8
+ * for pattern matching; messages are human-readable, not API contract.
9
+ */
10
+ export type GuardrailError =
11
+ | InvalidGuardrailError
12
+ | InvalidCheckDefinitionError
13
+ | UnknownCheckError
14
+ | InvalidCheckConfigError
15
+ | JudgeMissingError
16
+ | JudgeRoutingError
17
+ | ScopeMismatchError
18
+ | UnknownActionError;
19
+
20
+ /**
21
+ * The judge model couldn't be routed via `@kindgi/capabilities`. Wraps
22
+ * the underlying capability error (usually `capability-unsatisfiable` —
23
+ * no registered provider matches the judge's declared capability).
24
+ */
25
+ export interface JudgeRoutingError {
26
+ readonly code: 'judge-routing-failed';
27
+ readonly message: string;
28
+ readonly guardrailId: GuardrailId;
29
+ readonly cause: unknown;
30
+ }
31
+
32
+ /**
33
+ * The declaration is invalid. From `defineGuardrail` /
34
+ * `validateGuardrailSpec`: it fails the schema, or (`defineGuardrail`)
35
+ * its registered check has another kind. From the engine: its kind has
36
+ * no registered strategy, it is `llm-judge` without `judgeCapabilities`,
37
+ * or it is `external` and only the built-in `external` strategy is
38
+ * registered.
39
+ */
40
+ export interface InvalidGuardrailError {
41
+ readonly code: 'invalid-guardrail';
42
+ readonly message: string;
43
+ readonly issues: readonly { readonly path: string; readonly message: string }[];
44
+ }
45
+
46
+ /** The guardrail references a check id that isn't registered. */
47
+ export interface UnknownCheckError {
48
+ readonly code: 'unknown-check';
49
+ readonly message: string;
50
+ readonly guardrailId: GuardrailId;
51
+ readonly checkId: string;
52
+ }
53
+
54
+ /**
55
+ * The guardrail fired an action that no handler in
56
+ * `EvaluationBindings.actions` handles. `on-violation` is an open
57
+ * string, so an unknown name is accepted at define time and surfaces
58
+ * here, when the guardrail fires. The violation itself was already
59
+ * recorded (and compliance evidence emitted) before the lookup.
60
+ */
61
+ export interface UnknownActionError {
62
+ readonly code: 'unknown-action';
63
+ readonly message: string;
64
+ readonly guardrailId: GuardrailId;
65
+ readonly action: string;
66
+ }
67
+
68
+ /** The guardrail's `config` failed the check's own validator. */
69
+ export interface InvalidCheckConfigError {
70
+ readonly code: 'invalid-check-config';
71
+ readonly message: string;
72
+ readonly guardrailId: GuardrailId;
73
+ readonly reason: string;
74
+ }
75
+
76
+ /**
77
+ * `defineCheck` couldn't build the check — either the supplied Zod
78
+ * schema failed to convert via `z.toJSONSchema()`, or the resulting
79
+ * JSON Schema failed to compile as Draft 2020-12.
80
+ */
81
+ export interface InvalidCheckDefinitionError {
82
+ readonly code: 'invalid-check-definition';
83
+ readonly message: string;
84
+ readonly checkId: string;
85
+ readonly cause: unknown;
86
+ }
87
+
88
+ /**
89
+ * An `llm-judge` guardrail was evaluated without the required bindings
90
+ * (providerRegistry or judgeProvider). Runtime-only error.
91
+ */
92
+ export interface JudgeMissingError {
93
+ readonly code: 'judge-missing';
94
+ readonly message: string;
95
+ readonly guardrailId: GuardrailId;
96
+ }
97
+
98
+ /**
99
+ * Not really an error — surfaced so callers can distinguish "guardrail
100
+ * didn't apply to this trace" from "guardrail applied and passed."
101
+ */
102
+ export interface ScopeMismatchError {
103
+ readonly code: 'scope-mismatch';
104
+ readonly message: string;
105
+ readonly guardrailId: GuardrailId;
106
+ readonly reason: 'wrong-mode' | 'wrong-agent' | 'wrong-flow' | 'wrong-tenant';
107
+ }
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Capability } from '@kindgi/capabilities';
5
+
6
+ import type {
7
+ CheckRegistry,
8
+ CheckResult,
9
+ EvaluationBindings,
10
+ Guardrail,
11
+ RunTrace,
12
+ } from './types.js';
13
+
14
+ /**
15
+ * A strategy that knows how to evaluate one `Guardrail.kind`. Registered
16
+ * once at boot; the engine dispatches by looking up `guardrail.kind`
17
+ * against the strategy registry.
18
+ *
19
+ * Built-in strategies: `zero-llm` (delegates to `CheckRegistry`),
20
+ * `llm-judge` (routes through capabilities + `invokeJudge`), and
21
+ * `external` (returns an "evaluated elsewhere" error).
22
+ *
23
+ * Adapter packages register their own strategies without touching the
24
+ * engine — e.g. a strategy that runs check code in a sandbox, or one
25
+ * that delegates to an external policy engine.
26
+ */
27
+ export interface ExecutionStrategy {
28
+ readonly kind: string;
29
+ /** Human-friendly name for diagnostics. */
30
+ readonly name?: string;
31
+ /**
32
+ * Evaluate one guardrail against a trace. Returns the CheckResult the
33
+ * engine finalizes into an EvaluationResult; or an error object shaped
34
+ * to be surfaced directly.
35
+ */
36
+ evaluate(
37
+ guardrail: Guardrail,
38
+ checks: CheckRegistry,
39
+ trace: RunTrace,
40
+ bindings: EvaluationBindings,
41
+ ): Promise<StrategyResult>;
42
+ }
43
+
44
+ /**
45
+ * Discriminated result a strategy returns. `ok` produces a CheckResult;
46
+ * `err` surfaces a structured error with a JSON-Pointer-style path so
47
+ * the engine can attach it to the guardrail id it came from.
48
+ */
49
+ export type StrategyResult =
50
+ | { readonly kind: 'ok'; readonly value: CheckResult }
51
+ | { readonly kind: 'err'; readonly error: StrategyError };
52
+
53
+ export interface StrategyError {
54
+ /** Discriminant matching the engine's GuardrailError union or a sub-code. */
55
+ readonly code: string;
56
+ readonly message: string;
57
+ readonly issues?: readonly { readonly path: string; readonly message: string }[];
58
+ /** Filled in by the engine — the guardrail id currently evaluating. */
59
+ readonly guardrailId?: string;
60
+ }
61
+
62
+ /**
63
+ * Registry the engine consults at dispatch time. Register once; every
64
+ * subsequent `evaluateGuardrail` call sees the additional strategy.
65
+ */
66
+ export interface ExecutionStrategyRegistry {
67
+ register(strategy: ExecutionStrategy): void;
68
+ get(kind: string): ExecutionStrategy | undefined;
69
+ list(): readonly ExecutionStrategy[];
70
+ }
71
+
72
+ export function createExecutionStrategyRegistry(
73
+ seed: readonly ExecutionStrategy[] = [],
74
+ ): ExecutionStrategyRegistry {
75
+ const strategies = new Map<string, ExecutionStrategy>();
76
+ for (const s of seed) strategies.set(s.kind, s);
77
+ return {
78
+ register(strategy): void {
79
+ strategies.set(strategy.kind, strategy);
80
+ },
81
+ get(kind): ExecutionStrategy | undefined {
82
+ return strategies.get(kind);
83
+ },
84
+ list(): readonly ExecutionStrategy[] {
85
+ return [...strategies.values()];
86
+ },
87
+ };
88
+ }
89
+
90
+ // ============ built-in strategies ============
91
+
92
+ /** `zero-llm` — delegate to the registered `Check` via CheckRegistry. */
93
+ export const zeroLlmStrategy: ExecutionStrategy = {
94
+ kind: 'zero-llm',
95
+ name: 'Zero-LLM check',
96
+ async evaluate(guardrail, checks, trace, bindings): Promise<StrategyResult> {
97
+ const check = checks.get(guardrail.check);
98
+ if (check === undefined) {
99
+ return {
100
+ kind: 'err',
101
+ error: {
102
+ code: 'unknown-check',
103
+ message: `Guardrail "${guardrail.id}" references unregistered check "${guardrail.check}"`,
104
+ },
105
+ };
106
+ }
107
+ const config = (guardrail.config ?? {}) as Readonly<Record<string, unknown>>;
108
+ const result = await check.evaluate(config, trace, bindings);
109
+ return { kind: 'ok', value: result };
110
+ },
111
+ };
112
+
113
+ /**
114
+ * `external` — placeholder for guardrails evaluated by an external
115
+ * service: it always returns an `invalid-guardrail` error. Callers that
116
+ * want external evaluation register their own `external` strategy that
117
+ * routes to their service.
118
+ */
119
+ export const externalStrategy: ExecutionStrategy = {
120
+ kind: 'external',
121
+ name: 'External (not implemented at engine layer)',
122
+ async evaluate(): Promise<StrategyResult> {
123
+ return {
124
+ kind: 'err',
125
+ error: {
126
+ code: 'invalid-guardrail',
127
+ message:
128
+ 'external-kind guardrails are evaluated outside the engine — register an execution strategy for kind `external`',
129
+ issues: [
130
+ {
131
+ path: '/kind',
132
+ message: 'external kind requires a caller-registered execution strategy',
133
+ },
134
+ ],
135
+ },
136
+ };
137
+ },
138
+ };
139
+
140
+ /**
141
+ * Factory for the built-in `llm-judge` strategy. Takes the judge
142
+ * function as a parameter so this module does not import judge.ts; the
143
+ * engine passes `invokeJudge`.
144
+ */
145
+ export function makeLlmJudgeStrategy(
146
+ invokeJudge: (
147
+ config: unknown,
148
+ capability: Capability,
149
+ trace: RunTrace,
150
+ bindings: EvaluationBindings,
151
+ ) => Promise<
152
+ CheckResult | { readonly error: { readonly code: string; readonly message: string } }
153
+ >,
154
+ ): ExecutionStrategy {
155
+ return {
156
+ kind: 'llm-judge',
157
+ name: 'LLM judge',
158
+ async evaluate(guardrail, _checks, trace, bindings): Promise<StrategyResult> {
159
+ if (guardrail.judgeCapabilities === undefined) {
160
+ return {
161
+ kind: 'err',
162
+ error: {
163
+ code: 'invalid-guardrail',
164
+ message: `llm-judge guardrail "${guardrail.id}" missing judgeCapabilities`,
165
+ issues: [{ path: '/judgeCapabilities', message: 'required for kind=llm-judge' }],
166
+ },
167
+ };
168
+ }
169
+ const judged = await invokeJudge(
170
+ guardrail.config ?? {},
171
+ guardrail.judgeCapabilities,
172
+ trace,
173
+ bindings,
174
+ );
175
+ if ('error' in judged) {
176
+ return {
177
+ kind: 'err',
178
+ error: { code: judged.error.code, message: judged.error.message },
179
+ };
180
+ }
181
+ return { kind: 'ok', value: judged };
182
+ },
183
+ };
184
+ }