@edictum/core 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.
- package/dist/chunk-CRPQFRYJ.mjs +238 -0
- package/dist/chunk-CRPQFRYJ.mjs.map +1 -0
- package/dist/chunk-IXMXZGJG.mjs +30 -0
- package/dist/chunk-IXMXZGJG.mjs.map +1 -0
- package/dist/chunk-X5E2YY35.mjs +1299 -0
- package/dist/chunk-X5E2YY35.mjs.map +1 -0
- package/dist/dry-run-54PYIM6T.mjs +199 -0
- package/dist/dry-run-54PYIM6T.mjs.map +1 -0
- package/dist/index.cjs +3774 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1201 -0
- package/dist/index.d.ts +1201 -0
- package/dist/index.mjs +1890 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runner-ASI4JIW2.mjs +10 -0
- package/dist/runner-ASI4JIW2.mjs.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EdictumConfigError,
|
|
3
|
+
EdictumDenied,
|
|
4
|
+
EdictumToolError,
|
|
5
|
+
SideEffect,
|
|
6
|
+
createEnvelope
|
|
7
|
+
} from "./chunk-CRPQFRYJ.mjs";
|
|
8
|
+
|
|
9
|
+
// src/approval.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import * as readline from "readline";
|
|
12
|
+
|
|
13
|
+
// src/redaction.ts
|
|
14
|
+
var RedactionPolicy = class _RedactionPolicy {
|
|
15
|
+
static DEFAULT_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
16
|
+
"password",
|
|
17
|
+
"secret",
|
|
18
|
+
"token",
|
|
19
|
+
"api_key",
|
|
20
|
+
"apikey",
|
|
21
|
+
"api-key",
|
|
22
|
+
"authorization",
|
|
23
|
+
"auth",
|
|
24
|
+
"credentials",
|
|
25
|
+
"private_key",
|
|
26
|
+
"privatekey",
|
|
27
|
+
"access_token",
|
|
28
|
+
"refresh_token",
|
|
29
|
+
"client_secret",
|
|
30
|
+
"connection_string",
|
|
31
|
+
"database_url",
|
|
32
|
+
"db_password",
|
|
33
|
+
"ssh_key",
|
|
34
|
+
"passphrase"
|
|
35
|
+
]);
|
|
36
|
+
static BASH_REDACTION_PATTERNS = [
|
|
37
|
+
[
|
|
38
|
+
String.raw`(export\s+\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)\w*=)\S+`,
|
|
39
|
+
"$1[REDACTED]"
|
|
40
|
+
],
|
|
41
|
+
[String.raw`(-p\s*|--password[= ])\S+`, "$1[REDACTED]"],
|
|
42
|
+
[String.raw`(://\w+:)\S+(@)`, "$1[REDACTED]$2"]
|
|
43
|
+
];
|
|
44
|
+
static SECRET_VALUE_PATTERNS = [
|
|
45
|
+
String.raw`^(sk-[a-zA-Z0-9]{20,})`,
|
|
46
|
+
String.raw`^(AKIA[A-Z0-9]{16})`,
|
|
47
|
+
String.raw`^(eyJ[a-zA-Z0-9_-]{20,}\.)`,
|
|
48
|
+
String.raw`^(ghp_[a-zA-Z0-9]{36})`,
|
|
49
|
+
String.raw`^(xox[bpas]-[a-zA-Z0-9-]{10,})`
|
|
50
|
+
];
|
|
51
|
+
static MAX_PAYLOAD_SIZE = 32768;
|
|
52
|
+
static MAX_REGEX_INPUT = 1e4;
|
|
53
|
+
static MAX_PATTERN_LENGTH = 1e4;
|
|
54
|
+
_keys;
|
|
55
|
+
_patterns;
|
|
56
|
+
_compiledPatterns;
|
|
57
|
+
_compiledSecretPatterns;
|
|
58
|
+
_detectValues;
|
|
59
|
+
constructor(sensitiveKeys, customPatterns, detectSecretValues = true) {
|
|
60
|
+
const baseKeys = sensitiveKeys ? /* @__PURE__ */ new Set([..._RedactionPolicy.DEFAULT_SENSITIVE_KEYS, ...sensitiveKeys]) : new Set(_RedactionPolicy.DEFAULT_SENSITIVE_KEYS);
|
|
61
|
+
this._keys = new Set([...baseKeys].map((k) => k.toLowerCase()));
|
|
62
|
+
if (customPatterns) {
|
|
63
|
+
for (const [pattern] of customPatterns) {
|
|
64
|
+
if (pattern.length > _RedactionPolicy.MAX_PATTERN_LENGTH) {
|
|
65
|
+
throw new EdictumConfigError(
|
|
66
|
+
`Custom redaction pattern exceeds ${_RedactionPolicy.MAX_PATTERN_LENGTH} characters`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this._patterns = [
|
|
72
|
+
...customPatterns ?? [],
|
|
73
|
+
..._RedactionPolicy.BASH_REDACTION_PATTERNS
|
|
74
|
+
];
|
|
75
|
+
this._compiledPatterns = this._patterns.map(
|
|
76
|
+
([pattern, replacement]) => [new RegExp(pattern, "g"), replacement]
|
|
77
|
+
);
|
|
78
|
+
this._compiledSecretPatterns = _RedactionPolicy.SECRET_VALUE_PATTERNS.map(
|
|
79
|
+
(p) => new RegExp(p)
|
|
80
|
+
);
|
|
81
|
+
this._detectValues = detectSecretValues;
|
|
82
|
+
}
|
|
83
|
+
/** Recursively redact sensitive data from tool arguments. */
|
|
84
|
+
redactArgs(args) {
|
|
85
|
+
if (args !== null && typeof args === "object" && !Array.isArray(args)) {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, value] of Object.entries(
|
|
88
|
+
args
|
|
89
|
+
)) {
|
|
90
|
+
result[key] = this._isSensitiveKey(key) ? "[REDACTED]" : this.redactArgs(value);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(args)) {
|
|
95
|
+
return args.map((item) => this.redactArgs(item));
|
|
96
|
+
}
|
|
97
|
+
if (typeof args === "string") {
|
|
98
|
+
if (this._detectValues && this._looksLikeSecret(args)) {
|
|
99
|
+
return "[REDACTED]";
|
|
100
|
+
}
|
|
101
|
+
if (args.length > 1e3) {
|
|
102
|
+
return args.slice(0, 997) + "...";
|
|
103
|
+
}
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
return args;
|
|
107
|
+
}
|
|
108
|
+
/** Check if a key name indicates sensitive data. */
|
|
109
|
+
_isSensitiveKey(key) {
|
|
110
|
+
const k = key.toLowerCase();
|
|
111
|
+
if (this._keys.has(k)) return true;
|
|
112
|
+
const normalized = key.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
113
|
+
if (normalized !== k && this._keys.has(normalized)) return true;
|
|
114
|
+
const parts = normalized.split(/[_\-]/);
|
|
115
|
+
return parts.some(
|
|
116
|
+
(part) => part === "token" || part === "key" || part === "secret" || part === "password" || part === "credential"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
/** Check if a string value looks like a known secret format. */
|
|
120
|
+
_looksLikeSecret(value) {
|
|
121
|
+
const capped = value.length > _RedactionPolicy.MAX_REGEX_INPUT ? value.slice(0, _RedactionPolicy.MAX_REGEX_INPUT) : value;
|
|
122
|
+
for (const regex of this._compiledSecretPatterns) {
|
|
123
|
+
if (regex.test(capped)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
/** Apply redaction patterns to a bash command string. */
|
|
130
|
+
redactBashCommand(command) {
|
|
131
|
+
const capped = command.length > _RedactionPolicy.MAX_REGEX_INPUT ? command.slice(0, _RedactionPolicy.MAX_REGEX_INPUT) : command;
|
|
132
|
+
let result = capped;
|
|
133
|
+
for (const [regex, replacement] of this._compiledPatterns) {
|
|
134
|
+
regex.lastIndex = 0;
|
|
135
|
+
result = result.replace(regex, replacement);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/** Apply redaction patterns and truncate a result string. */
|
|
140
|
+
redactResult(result, maxLength = 500) {
|
|
141
|
+
const capped = result.length > _RedactionPolicy.MAX_REGEX_INPUT ? result.slice(0, _RedactionPolicy.MAX_REGEX_INPUT) : result;
|
|
142
|
+
let redacted = capped;
|
|
143
|
+
for (const [regex, replacement] of this._compiledPatterns) {
|
|
144
|
+
regex.lastIndex = 0;
|
|
145
|
+
redacted = redacted.replace(regex, replacement);
|
|
146
|
+
}
|
|
147
|
+
if (redacted.length > maxLength) {
|
|
148
|
+
redacted = redacted.slice(0, maxLength - 3) + "...";
|
|
149
|
+
}
|
|
150
|
+
return redacted;
|
|
151
|
+
}
|
|
152
|
+
/** Cap total serialized size of audit payload. Returns a new object if truncated. */
|
|
153
|
+
capPayload(data) {
|
|
154
|
+
const serialized = JSON.stringify(data);
|
|
155
|
+
if (serialized.length > _RedactionPolicy.MAX_PAYLOAD_SIZE) {
|
|
156
|
+
const { resultSummary: _rs, toolArgs: _ta, ...rest } = data;
|
|
157
|
+
void _rs;
|
|
158
|
+
void _ta;
|
|
159
|
+
return {
|
|
160
|
+
...rest,
|
|
161
|
+
_truncated: true,
|
|
162
|
+
toolArgs: { _redacted: "payload exceeded 32KB" }
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/approval.ts
|
|
170
|
+
function sanitizeForTerminal(s) {
|
|
171
|
+
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x1f\x7f]/g, "");
|
|
172
|
+
}
|
|
173
|
+
var ApprovalStatus = {
|
|
174
|
+
PENDING: "pending",
|
|
175
|
+
APPROVED: "approved",
|
|
176
|
+
DENIED: "denied",
|
|
177
|
+
TIMEOUT: "timeout"
|
|
178
|
+
};
|
|
179
|
+
function createApprovalRequest(fields) {
|
|
180
|
+
const request = {
|
|
181
|
+
approvalId: fields.approvalId,
|
|
182
|
+
toolName: fields.toolName,
|
|
183
|
+
toolArgs: Object.freeze({ ...fields.toolArgs }),
|
|
184
|
+
message: fields.message,
|
|
185
|
+
timeout: fields.timeout,
|
|
186
|
+
timeoutEffect: fields.timeoutEffect,
|
|
187
|
+
principal: fields.principal !== null ? Object.freeze({ ...fields.principal }) : null,
|
|
188
|
+
metadata: Object.freeze({ ...fields.metadata }),
|
|
189
|
+
createdAt: fields.createdAt ?? /* @__PURE__ */ new Date()
|
|
190
|
+
};
|
|
191
|
+
return Object.freeze(request);
|
|
192
|
+
}
|
|
193
|
+
function createApprovalDecision(fields) {
|
|
194
|
+
const decision = {
|
|
195
|
+
approved: fields.approved,
|
|
196
|
+
approver: fields.approver ?? null,
|
|
197
|
+
reason: fields.reason ?? null,
|
|
198
|
+
status: fields.status ?? ApprovalStatus.PENDING,
|
|
199
|
+
timestamp: fields.timestamp ?? /* @__PURE__ */ new Date()
|
|
200
|
+
};
|
|
201
|
+
return Object.freeze(decision);
|
|
202
|
+
}
|
|
203
|
+
var LocalApprovalBackend = class {
|
|
204
|
+
_pending = /* @__PURE__ */ new Map();
|
|
205
|
+
async requestApproval(toolName, toolArgs, message, options) {
|
|
206
|
+
const approvalId = randomUUID();
|
|
207
|
+
const request = createApprovalRequest({
|
|
208
|
+
approvalId,
|
|
209
|
+
toolName,
|
|
210
|
+
toolArgs,
|
|
211
|
+
message,
|
|
212
|
+
timeout: options?.timeout ?? 300,
|
|
213
|
+
timeoutEffect: options?.timeoutEffect ?? "deny",
|
|
214
|
+
principal: options?.principal ?? null,
|
|
215
|
+
metadata: options?.metadata ?? {}
|
|
216
|
+
});
|
|
217
|
+
this._pending.set(approvalId, request);
|
|
218
|
+
const redaction = new RedactionPolicy();
|
|
219
|
+
const safeArgs = redaction.redactArgs(toolArgs);
|
|
220
|
+
process.stdout.write(`[APPROVAL REQUIRED] ${sanitizeForTerminal(message)}
|
|
221
|
+
`);
|
|
222
|
+
process.stdout.write(` Tool: ${sanitizeForTerminal(toolName)}
|
|
223
|
+
`);
|
|
224
|
+
process.stdout.write(` Args: ${sanitizeForTerminal(JSON.stringify(safeArgs))}
|
|
225
|
+
`);
|
|
226
|
+
process.stdout.write(` ID: ${approvalId}
|
|
227
|
+
`);
|
|
228
|
+
return request;
|
|
229
|
+
}
|
|
230
|
+
async waitForDecision(approvalId, timeout) {
|
|
231
|
+
const request = this._pending.get(approvalId);
|
|
232
|
+
const effectiveTimeout = timeout ?? (request ? request.timeout : 300);
|
|
233
|
+
try {
|
|
234
|
+
const response = await this._readStdin(approvalId, effectiveTimeout);
|
|
235
|
+
const approved = ["y", "yes", "approve"].includes(
|
|
236
|
+
response.trim().toLowerCase()
|
|
237
|
+
);
|
|
238
|
+
const status = approved ? ApprovalStatus.APPROVED : ApprovalStatus.DENIED;
|
|
239
|
+
return createApprovalDecision({
|
|
240
|
+
approved,
|
|
241
|
+
approver: "local",
|
|
242
|
+
status
|
|
243
|
+
});
|
|
244
|
+
} catch {
|
|
245
|
+
const timeoutEffect = request ? request.timeoutEffect : "deny";
|
|
246
|
+
const approved = timeoutEffect === "allow";
|
|
247
|
+
return createApprovalDecision({
|
|
248
|
+
approved,
|
|
249
|
+
status: ApprovalStatus.TIMEOUT
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Read a single line from stdin with a timeout. */
|
|
254
|
+
_readStdin(approvalId, timeoutSeconds) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const rl = readline.createInterface({
|
|
257
|
+
input: process.stdin,
|
|
258
|
+
output: process.stdout
|
|
259
|
+
});
|
|
260
|
+
const timer = setTimeout(() => {
|
|
261
|
+
rl.close();
|
|
262
|
+
reject(new Error("Approval timed out"));
|
|
263
|
+
}, timeoutSeconds * 1e3);
|
|
264
|
+
rl.question(`Approve? [y/N] (id: ${approvalId}): `, (answer) => {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
rl.close();
|
|
267
|
+
resolve(answer);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/audit.ts
|
|
274
|
+
import { appendFile } from "fs/promises";
|
|
275
|
+
var AuditAction = {
|
|
276
|
+
CALL_DENIED: "call_denied",
|
|
277
|
+
CALL_WOULD_DENY: "call_would_deny",
|
|
278
|
+
CALL_ALLOWED: "call_allowed",
|
|
279
|
+
CALL_EXECUTED: "call_executed",
|
|
280
|
+
CALL_FAILED: "call_failed",
|
|
281
|
+
POSTCONDITION_WARNING: "postcondition_warning",
|
|
282
|
+
CALL_APPROVAL_REQUESTED: "call_approval_requested",
|
|
283
|
+
CALL_APPROVAL_GRANTED: "call_approval_granted",
|
|
284
|
+
CALL_APPROVAL_DENIED: "call_approval_denied",
|
|
285
|
+
CALL_APPROVAL_TIMEOUT: "call_approval_timeout"
|
|
286
|
+
};
|
|
287
|
+
function createAuditEvent(f = {}) {
|
|
288
|
+
return {
|
|
289
|
+
schemaVersion: f.schemaVersion ?? "0.3.0",
|
|
290
|
+
timestamp: f.timestamp ?? /* @__PURE__ */ new Date(),
|
|
291
|
+
runId: f.runId ?? "",
|
|
292
|
+
callId: f.callId ?? "",
|
|
293
|
+
callIndex: f.callIndex ?? 0,
|
|
294
|
+
parentCallId: f.parentCallId ?? null,
|
|
295
|
+
toolName: f.toolName ?? "",
|
|
296
|
+
toolArgs: f.toolArgs ?? {},
|
|
297
|
+
sideEffect: f.sideEffect ?? "",
|
|
298
|
+
environment: f.environment ?? "",
|
|
299
|
+
principal: f.principal ?? null,
|
|
300
|
+
action: f.action ?? AuditAction.CALL_DENIED,
|
|
301
|
+
decisionSource: f.decisionSource ?? null,
|
|
302
|
+
decisionName: f.decisionName ?? null,
|
|
303
|
+
reason: f.reason ?? null,
|
|
304
|
+
hooksEvaluated: f.hooksEvaluated ?? [],
|
|
305
|
+
contractsEvaluated: f.contractsEvaluated ?? [],
|
|
306
|
+
toolSuccess: f.toolSuccess ?? null,
|
|
307
|
+
postconditionsPassed: f.postconditionsPassed ?? null,
|
|
308
|
+
durationMs: f.durationMs ?? 0,
|
|
309
|
+
error: f.error ?? null,
|
|
310
|
+
resultSummary: f.resultSummary ?? null,
|
|
311
|
+
sessionAttemptCount: f.sessionAttemptCount ?? 0,
|
|
312
|
+
sessionExecutionCount: f.sessionExecutionCount ?? 0,
|
|
313
|
+
mode: f.mode ?? "enforce",
|
|
314
|
+
policyVersion: f.policyVersion ?? null,
|
|
315
|
+
policyError: f.policyError ?? false
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
var MarkEvictedError = class extends Error {
|
|
319
|
+
constructor(message) {
|
|
320
|
+
super(message);
|
|
321
|
+
this.name = "MarkEvictedError";
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
var CompositeSink = class {
|
|
325
|
+
_sinks;
|
|
326
|
+
constructor(sinks) {
|
|
327
|
+
if (sinks.length === 0) throw new Error("CompositeSink requires at least one sink");
|
|
328
|
+
this._sinks = [...sinks];
|
|
329
|
+
}
|
|
330
|
+
get sinks() {
|
|
331
|
+
return [...this._sinks];
|
|
332
|
+
}
|
|
333
|
+
async emit(event) {
|
|
334
|
+
const errors = [];
|
|
335
|
+
for (const sink of this._sinks) {
|
|
336
|
+
try {
|
|
337
|
+
await sink.emit(event);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (errors.length > 0) {
|
|
343
|
+
throw new AggregateError(errors, "CompositeSink: one or more sinks failed");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function _toPlain(event) {
|
|
348
|
+
const { timestamp, ...rest } = event;
|
|
349
|
+
return { ...rest, timestamp: timestamp.toISOString() };
|
|
350
|
+
}
|
|
351
|
+
var StdoutAuditSink = class {
|
|
352
|
+
_redaction;
|
|
353
|
+
constructor(redaction) {
|
|
354
|
+
this._redaction = redaction ?? new RedactionPolicy();
|
|
355
|
+
}
|
|
356
|
+
async emit(event) {
|
|
357
|
+
process.stdout.write(JSON.stringify(this._redaction.capPayload(_toPlain(event))) + "\n");
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var FileAuditSink = class {
|
|
361
|
+
_path;
|
|
362
|
+
_redaction;
|
|
363
|
+
constructor(path, redaction) {
|
|
364
|
+
this._path = path;
|
|
365
|
+
this._redaction = redaction ?? new RedactionPolicy();
|
|
366
|
+
}
|
|
367
|
+
async emit(event) {
|
|
368
|
+
const data = this._redaction.capPayload(_toPlain(event));
|
|
369
|
+
await appendFile(this._path, JSON.stringify(data) + "\n", "utf-8");
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var CollectingAuditSink = class {
|
|
373
|
+
_events = [];
|
|
374
|
+
_maxEvents;
|
|
375
|
+
_totalEmitted = 0;
|
|
376
|
+
constructor(maxEvents = 5e4) {
|
|
377
|
+
if (maxEvents < 1) throw new Error(`max_events must be >= 1, got ${maxEvents}`);
|
|
378
|
+
this._maxEvents = maxEvents;
|
|
379
|
+
}
|
|
380
|
+
async emit(event) {
|
|
381
|
+
this._events.push(event);
|
|
382
|
+
this._totalEmitted += 1;
|
|
383
|
+
if (this._events.length > this._maxEvents) this._events = this._events.slice(-this._maxEvents);
|
|
384
|
+
}
|
|
385
|
+
get events() {
|
|
386
|
+
return [...this._events];
|
|
387
|
+
}
|
|
388
|
+
mark() {
|
|
389
|
+
return this._totalEmitted;
|
|
390
|
+
}
|
|
391
|
+
sinceMark(m) {
|
|
392
|
+
if (m > this._totalEmitted) {
|
|
393
|
+
throw new Error(`Mark ${m} is ahead of total emitted (${this._totalEmitted})`);
|
|
394
|
+
}
|
|
395
|
+
const evictedCount = this._totalEmitted - this._events.length;
|
|
396
|
+
if (m < evictedCount) {
|
|
397
|
+
throw new MarkEvictedError(
|
|
398
|
+
`Mark ${m} references evicted events (buffer starts at ${evictedCount}, max_events=${this._maxEvents})`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return [...this._events.slice(m - evictedCount)];
|
|
402
|
+
}
|
|
403
|
+
last() {
|
|
404
|
+
if (this._events.length === 0) throw new Error("No events collected");
|
|
405
|
+
const last = this._events[this._events.length - 1];
|
|
406
|
+
if (!last) throw new Error("No events collected");
|
|
407
|
+
return last;
|
|
408
|
+
}
|
|
409
|
+
filter(action) {
|
|
410
|
+
return this._events.filter((e) => e.action === action);
|
|
411
|
+
}
|
|
412
|
+
clear() {
|
|
413
|
+
this._events = [];
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/contracts.ts
|
|
418
|
+
var Verdict = {
|
|
419
|
+
/**
|
|
420
|
+
* Contract passed — tool call is acceptable.
|
|
421
|
+
*/
|
|
422
|
+
pass_() {
|
|
423
|
+
return Object.freeze({ passed: true, message: null, metadata: Object.freeze({}) });
|
|
424
|
+
},
|
|
425
|
+
/**
|
|
426
|
+
* Contract failed with an actionable message (truncated to 500 chars).
|
|
427
|
+
*
|
|
428
|
+
* Make it SPECIFIC and INSTRUCTIVE — the agent uses it to self-correct.
|
|
429
|
+
*/
|
|
430
|
+
fail(message, metadata = {}) {
|
|
431
|
+
const truncated = message.length > 500 ? message.slice(0, 497) + "..." : message;
|
|
432
|
+
return Object.freeze({
|
|
433
|
+
passed: false,
|
|
434
|
+
message: truncated,
|
|
435
|
+
metadata: Object.freeze({ ...metadata })
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/hooks.ts
|
|
441
|
+
var HookResult = {
|
|
442
|
+
ALLOW: "allow",
|
|
443
|
+
DENY: "deny"
|
|
444
|
+
};
|
|
445
|
+
var HookDecision = {
|
|
446
|
+
allow() {
|
|
447
|
+
return Object.freeze({ result: HookResult.ALLOW, reason: null });
|
|
448
|
+
},
|
|
449
|
+
deny(reason) {
|
|
450
|
+
const truncated = reason.length > 500 ? reason.slice(0, 497) + "..." : reason;
|
|
451
|
+
return Object.freeze({ result: HookResult.DENY, reason: truncated });
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/pipeline.ts
|
|
456
|
+
function createPreDecision(partial) {
|
|
457
|
+
return {
|
|
458
|
+
action: partial.action,
|
|
459
|
+
reason: partial.reason ?? null,
|
|
460
|
+
decisionSource: partial.decisionSource ?? null,
|
|
461
|
+
decisionName: partial.decisionName ?? null,
|
|
462
|
+
hooksEvaluated: partial.hooksEvaluated ?? [],
|
|
463
|
+
contractsEvaluated: partial.contractsEvaluated ?? [],
|
|
464
|
+
observed: partial.observed ?? false,
|
|
465
|
+
policyError: partial.policyError ?? false,
|
|
466
|
+
observeResults: partial.observeResults ?? [],
|
|
467
|
+
approvalTimeout: partial.approvalTimeout ?? 300,
|
|
468
|
+
approvalTimeoutEffect: partial.approvalTimeoutEffect ?? "deny",
|
|
469
|
+
approvalMessage: partial.approvalMessage ?? null
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function createPostDecision(partial) {
|
|
473
|
+
return {
|
|
474
|
+
toolSuccess: partial.toolSuccess,
|
|
475
|
+
postconditionsPassed: partial.postconditionsPassed ?? true,
|
|
476
|
+
warnings: partial.warnings ?? [],
|
|
477
|
+
contractsEvaluated: partial.contractsEvaluated ?? [],
|
|
478
|
+
policyError: partial.policyError ?? false,
|
|
479
|
+
redactedResponse: partial.redactedResponse ?? null,
|
|
480
|
+
outputSuppressed: partial.outputSuppressed ?? false
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function hasPolicyError(contractsEvaluated) {
|
|
484
|
+
return contractsEvaluated.some((c) => {
|
|
485
|
+
const meta = c["metadata"];
|
|
486
|
+
return meta?.["policy_error"] === true;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
var GovernancePipeline = class {
|
|
490
|
+
_guard;
|
|
491
|
+
constructor(guard) {
|
|
492
|
+
this._guard = guard;
|
|
493
|
+
}
|
|
494
|
+
async preExecute(envelope, session) {
|
|
495
|
+
const hooksEvaluated = [];
|
|
496
|
+
const contractsEvaluated = [];
|
|
497
|
+
let hasObservedDeny = false;
|
|
498
|
+
let toolNameForBatch;
|
|
499
|
+
if (envelope.toolName in this._guard.limits.maxCallsPerTool) {
|
|
500
|
+
toolNameForBatch = envelope.toolName;
|
|
501
|
+
}
|
|
502
|
+
const counters = await session.batchGetCounters({
|
|
503
|
+
includeTool: toolNameForBatch
|
|
504
|
+
});
|
|
505
|
+
const attemptCount = counters["attempts"] ?? 0;
|
|
506
|
+
if (attemptCount >= this._guard.limits.maxAttempts) {
|
|
507
|
+
return createPreDecision({
|
|
508
|
+
action: "deny",
|
|
509
|
+
reason: `Attempt limit reached (${this._guard.limits.maxAttempts}). Agent may be stuck in a retry loop. Stop and reassess.`,
|
|
510
|
+
decisionSource: "attempt_limit",
|
|
511
|
+
decisionName: "max_attempts",
|
|
512
|
+
hooksEvaluated,
|
|
513
|
+
contractsEvaluated
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
for (const hookReg of this._guard.getHooks("before", envelope)) {
|
|
517
|
+
if (hookReg.when && !hookReg.when(envelope)) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
let decision;
|
|
521
|
+
try {
|
|
522
|
+
decision = await hookReg.callback(envelope);
|
|
523
|
+
} catch (exc) {
|
|
524
|
+
decision = HookDecision.deny(`Hook error: ${exc}`);
|
|
525
|
+
}
|
|
526
|
+
const hookRecord = {
|
|
527
|
+
name: hookReg.callback.name || "anonymous",
|
|
528
|
+
result: decision.result,
|
|
529
|
+
reason: decision.reason
|
|
530
|
+
};
|
|
531
|
+
hooksEvaluated.push(hookRecord);
|
|
532
|
+
if (decision.result === HookResult.DENY) {
|
|
533
|
+
return createPreDecision({
|
|
534
|
+
action: "deny",
|
|
535
|
+
reason: decision.reason,
|
|
536
|
+
decisionSource: "hook",
|
|
537
|
+
decisionName: hookRecord["name"],
|
|
538
|
+
hooksEvaluated,
|
|
539
|
+
contractsEvaluated,
|
|
540
|
+
policyError: (decision.reason ?? "").includes("Hook error:")
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (const contract of this._guard.getPreconditions(envelope)) {
|
|
545
|
+
let verdict;
|
|
546
|
+
try {
|
|
547
|
+
verdict = await contract.check(envelope);
|
|
548
|
+
} catch (exc) {
|
|
549
|
+
verdict = Verdict.fail(`Precondition error: ${exc}`, {
|
|
550
|
+
policy_error: true
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const contractRecord = {
|
|
554
|
+
name: contract.name,
|
|
555
|
+
type: "precondition",
|
|
556
|
+
passed: verdict.passed,
|
|
557
|
+
message: verdict.message
|
|
558
|
+
};
|
|
559
|
+
if (verdict.metadata && Object.keys(verdict.metadata).length > 0) {
|
|
560
|
+
contractRecord["metadata"] = verdict.metadata;
|
|
561
|
+
}
|
|
562
|
+
contractsEvaluated.push(contractRecord);
|
|
563
|
+
if (!verdict.passed) {
|
|
564
|
+
if (contract.mode === "observe") {
|
|
565
|
+
contractRecord["observed"] = true;
|
|
566
|
+
hasObservedDeny = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const source = contract.source ?? "precondition";
|
|
570
|
+
const pe2 = hasPolicyError(contractsEvaluated);
|
|
571
|
+
const effect = contract.effect ?? "deny";
|
|
572
|
+
if (effect === "approve") {
|
|
573
|
+
return createPreDecision({
|
|
574
|
+
action: "pending_approval",
|
|
575
|
+
reason: verdict.message,
|
|
576
|
+
decisionSource: source,
|
|
577
|
+
decisionName: contract.name,
|
|
578
|
+
hooksEvaluated,
|
|
579
|
+
contractsEvaluated,
|
|
580
|
+
policyError: pe2,
|
|
581
|
+
approvalTimeout: contract.timeout ?? 300,
|
|
582
|
+
approvalTimeoutEffect: contract.timeoutEffect ?? "deny",
|
|
583
|
+
approvalMessage: verdict.message
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return createPreDecision({
|
|
587
|
+
action: "deny",
|
|
588
|
+
reason: verdict.message,
|
|
589
|
+
decisionSource: source,
|
|
590
|
+
decisionName: contract.name,
|
|
591
|
+
hooksEvaluated,
|
|
592
|
+
contractsEvaluated,
|
|
593
|
+
policyError: pe2
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const contract of this._guard.getSandboxContracts(envelope)) {
|
|
598
|
+
let verdict;
|
|
599
|
+
try {
|
|
600
|
+
verdict = await contract.check(envelope);
|
|
601
|
+
} catch (exc) {
|
|
602
|
+
verdict = Verdict.fail(`Sandbox contract error: ${exc}`, {
|
|
603
|
+
policy_error: true
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const contractRecord = {
|
|
607
|
+
name: contract.name,
|
|
608
|
+
type: "sandbox",
|
|
609
|
+
passed: verdict.passed,
|
|
610
|
+
message: verdict.message
|
|
611
|
+
};
|
|
612
|
+
if (verdict.metadata && Object.keys(verdict.metadata).length > 0) {
|
|
613
|
+
contractRecord["metadata"] = verdict.metadata;
|
|
614
|
+
}
|
|
615
|
+
contractsEvaluated.push(contractRecord);
|
|
616
|
+
if (!verdict.passed) {
|
|
617
|
+
if (contract.mode === "observe") {
|
|
618
|
+
contractRecord["observed"] = true;
|
|
619
|
+
hasObservedDeny = true;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const source = contract.source ?? "yaml_sandbox";
|
|
623
|
+
const pe2 = hasPolicyError(contractsEvaluated);
|
|
624
|
+
const effect = contract.effect ?? "deny";
|
|
625
|
+
if (effect === "approve") {
|
|
626
|
+
return createPreDecision({
|
|
627
|
+
action: "pending_approval",
|
|
628
|
+
reason: verdict.message,
|
|
629
|
+
decisionSource: source,
|
|
630
|
+
decisionName: contract.name,
|
|
631
|
+
hooksEvaluated,
|
|
632
|
+
contractsEvaluated,
|
|
633
|
+
policyError: pe2,
|
|
634
|
+
approvalTimeout: contract.timeout ?? 300,
|
|
635
|
+
approvalTimeoutEffect: contract.timeoutEffect ?? "deny",
|
|
636
|
+
approvalMessage: verdict.message
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return createPreDecision({
|
|
640
|
+
action: "deny",
|
|
641
|
+
reason: verdict.message,
|
|
642
|
+
decisionSource: source,
|
|
643
|
+
decisionName: contract.name,
|
|
644
|
+
hooksEvaluated,
|
|
645
|
+
contractsEvaluated,
|
|
646
|
+
policyError: pe2
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const contract of this._guard.getSessionContracts()) {
|
|
651
|
+
let verdict;
|
|
652
|
+
try {
|
|
653
|
+
verdict = await contract.check(session);
|
|
654
|
+
} catch (exc) {
|
|
655
|
+
verdict = Verdict.fail(`Session contract error: ${exc}`, {
|
|
656
|
+
policy_error: true
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const contractRecord = {
|
|
660
|
+
name: contract.name,
|
|
661
|
+
type: "session_contract",
|
|
662
|
+
passed: verdict.passed,
|
|
663
|
+
message: verdict.message
|
|
664
|
+
};
|
|
665
|
+
if (verdict.metadata && Object.keys(verdict.metadata).length > 0) {
|
|
666
|
+
contractRecord["metadata"] = verdict.metadata;
|
|
667
|
+
}
|
|
668
|
+
contractsEvaluated.push(contractRecord);
|
|
669
|
+
if (!verdict.passed) {
|
|
670
|
+
const source = contract.source ?? "session_contract";
|
|
671
|
+
const pe2 = hasPolicyError(contractsEvaluated);
|
|
672
|
+
return createPreDecision({
|
|
673
|
+
action: "deny",
|
|
674
|
+
reason: verdict.message,
|
|
675
|
+
decisionSource: source,
|
|
676
|
+
decisionName: contract.name,
|
|
677
|
+
hooksEvaluated,
|
|
678
|
+
contractsEvaluated,
|
|
679
|
+
policyError: pe2
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const execCount = counters["execs"] ?? 0;
|
|
684
|
+
if (execCount >= this._guard.limits.maxToolCalls) {
|
|
685
|
+
return createPreDecision({
|
|
686
|
+
action: "deny",
|
|
687
|
+
reason: `Execution limit reached (${this._guard.limits.maxToolCalls} calls). Summarize progress and stop.`,
|
|
688
|
+
decisionSource: "operation_limit",
|
|
689
|
+
decisionName: "max_tool_calls",
|
|
690
|
+
hooksEvaluated,
|
|
691
|
+
contractsEvaluated
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (envelope.toolName in this._guard.limits.maxCallsPerTool) {
|
|
695
|
+
const toolKey = `tool:${envelope.toolName}`;
|
|
696
|
+
const toolCount = counters[toolKey] ?? 0;
|
|
697
|
+
const toolLimit = this._guard.limits.maxCallsPerTool[envelope.toolName] ?? 0;
|
|
698
|
+
if (toolCount >= toolLimit) {
|
|
699
|
+
return createPreDecision({
|
|
700
|
+
action: "deny",
|
|
701
|
+
reason: `Per-tool limit: ${envelope.toolName} called ${toolCount} times (limit: ${toolLimit}).`,
|
|
702
|
+
decisionSource: "operation_limit",
|
|
703
|
+
decisionName: `max_calls_per_tool:${envelope.toolName}`,
|
|
704
|
+
hooksEvaluated,
|
|
705
|
+
contractsEvaluated
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const pe = hasPolicyError(contractsEvaluated);
|
|
710
|
+
const observeResults = await this._evaluateObserveContracts(
|
|
711
|
+
envelope,
|
|
712
|
+
session
|
|
713
|
+
);
|
|
714
|
+
return createPreDecision({
|
|
715
|
+
action: "allow",
|
|
716
|
+
hooksEvaluated,
|
|
717
|
+
contractsEvaluated,
|
|
718
|
+
observed: hasObservedDeny,
|
|
719
|
+
policyError: pe,
|
|
720
|
+
observeResults
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
async postExecute(envelope, toolResponse, toolSuccess) {
|
|
724
|
+
const warnings = [];
|
|
725
|
+
const contractsEvaluated = [];
|
|
726
|
+
let redactedResponse = null;
|
|
727
|
+
let outputSuppressed = false;
|
|
728
|
+
for (const contract of this._guard.getPostconditions(envelope)) {
|
|
729
|
+
let verdict;
|
|
730
|
+
try {
|
|
731
|
+
verdict = await contract.check(envelope, toolResponse);
|
|
732
|
+
} catch (exc) {
|
|
733
|
+
verdict = Verdict.fail(`Postcondition error: ${exc}`, {
|
|
734
|
+
policy_error: true
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
const contractRecord = {
|
|
738
|
+
name: contract.name,
|
|
739
|
+
type: "postcondition",
|
|
740
|
+
passed: verdict.passed,
|
|
741
|
+
message: verdict.message
|
|
742
|
+
};
|
|
743
|
+
if (verdict.metadata && Object.keys(verdict.metadata).length > 0) {
|
|
744
|
+
contractRecord["metadata"] = verdict.metadata;
|
|
745
|
+
}
|
|
746
|
+
contractsEvaluated.push(contractRecord);
|
|
747
|
+
if (!verdict.passed) {
|
|
748
|
+
const effect = contract.effect ?? "warn";
|
|
749
|
+
const contractMode = contract.mode;
|
|
750
|
+
const isSafe = envelope.sideEffect === SideEffect.PURE || envelope.sideEffect === SideEffect.READ;
|
|
751
|
+
if (contractMode === "observe") {
|
|
752
|
+
contractRecord["observed"] = true;
|
|
753
|
+
warnings.push(`\u26A0\uFE0F [observe] ${verdict.message}`);
|
|
754
|
+
} else if (effect === "redact" && isSafe) {
|
|
755
|
+
const patterns = contract.redactPatterns ?? [];
|
|
756
|
+
const source = redactedResponse !== null ? redactedResponse : toolResponse;
|
|
757
|
+
let text = source != null ? String(source) : "";
|
|
758
|
+
if (patterns.length > 0) {
|
|
759
|
+
for (const pat of patterns) {
|
|
760
|
+
const globalPat = pat.global ? pat : new RegExp(pat.source, pat.flags + "g");
|
|
761
|
+
text = text.replace(globalPat, "[REDACTED]");
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
const policy = new RedactionPolicy();
|
|
765
|
+
text = policy.redactResult(text, text.length + 100);
|
|
766
|
+
}
|
|
767
|
+
redactedResponse = text;
|
|
768
|
+
warnings.push(
|
|
769
|
+
`\u26A0\uFE0F Content redacted by ${contract.name}.`
|
|
770
|
+
);
|
|
771
|
+
} else if (effect === "deny" && isSafe) {
|
|
772
|
+
redactedResponse = `[OUTPUT SUPPRESSED] ${verdict.message}`;
|
|
773
|
+
outputSuppressed = true;
|
|
774
|
+
warnings.push(
|
|
775
|
+
`\u26A0\uFE0F Output suppressed by ${contract.name}.`
|
|
776
|
+
);
|
|
777
|
+
} else if ((effect === "redact" || effect === "deny") && !isSafe) {
|
|
778
|
+
warnings.push(
|
|
779
|
+
`\u26A0\uFE0F ${verdict.message} Tool already executed \u2014 assess before proceeding.`
|
|
780
|
+
);
|
|
781
|
+
} else if (isSafe) {
|
|
782
|
+
warnings.push(
|
|
783
|
+
`\u26A0\uFE0F ${verdict.message} Consider retrying.`
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
warnings.push(
|
|
787
|
+
`\u26A0\uFE0F ${verdict.message} Tool already executed \u2014 assess before proceeding.`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const hookReg of this._guard.getHooks("after", envelope)) {
|
|
793
|
+
if (hookReg.when && !hookReg.when(envelope)) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
await hookReg.callback(envelope, toolResponse);
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
for (const contract of this._guard.getObservePostconditions(envelope)) {
|
|
802
|
+
let verdict;
|
|
803
|
+
try {
|
|
804
|
+
verdict = await contract.check(envelope, toolResponse);
|
|
805
|
+
} catch (exc) {
|
|
806
|
+
verdict = Verdict.fail(
|
|
807
|
+
`Observe-mode postcondition error: ${exc}`,
|
|
808
|
+
{ policy_error: true }
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
const record = {
|
|
812
|
+
name: contract.name,
|
|
813
|
+
type: "postcondition",
|
|
814
|
+
passed: verdict.passed,
|
|
815
|
+
message: verdict.message,
|
|
816
|
+
observed: true,
|
|
817
|
+
source: contract.source ?? "yaml_postcondition"
|
|
818
|
+
};
|
|
819
|
+
if (verdict.metadata && Object.keys(verdict.metadata).length > 0) {
|
|
820
|
+
record["metadata"] = verdict.metadata;
|
|
821
|
+
}
|
|
822
|
+
contractsEvaluated.push(record);
|
|
823
|
+
if (!verdict.passed) {
|
|
824
|
+
warnings.push(`\u26A0\uFE0F [observe] ${verdict.message}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const postconditionsPassed = contractsEvaluated.length > 0 ? contractsEvaluated.every(
|
|
828
|
+
(c) => c["passed"] === true || c["observed"] === true
|
|
829
|
+
) : true;
|
|
830
|
+
const pe = hasPolicyError(contractsEvaluated);
|
|
831
|
+
return createPostDecision({
|
|
832
|
+
toolSuccess,
|
|
833
|
+
postconditionsPassed,
|
|
834
|
+
warnings,
|
|
835
|
+
contractsEvaluated,
|
|
836
|
+
policyError: pe,
|
|
837
|
+
redactedResponse,
|
|
838
|
+
outputSuppressed
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Evaluate observe-mode contracts without affecting the real decision.
|
|
843
|
+
*
|
|
844
|
+
* Observe-mode contracts are identified by mode === "observe" on the
|
|
845
|
+
* internal contract. Results are returned as dicts for audit emission
|
|
846
|
+
* but never block calls.
|
|
847
|
+
*/
|
|
848
|
+
async _evaluateObserveContracts(envelope, session) {
|
|
849
|
+
const results = [];
|
|
850
|
+
for (const contract of this._guard.getObservePreconditions(
|
|
851
|
+
envelope
|
|
852
|
+
)) {
|
|
853
|
+
let verdict;
|
|
854
|
+
try {
|
|
855
|
+
verdict = await contract.check(envelope);
|
|
856
|
+
} catch (exc) {
|
|
857
|
+
verdict = Verdict.fail(
|
|
858
|
+
`Observe-mode precondition error: ${exc}`,
|
|
859
|
+
{ policy_error: true }
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
results.push({
|
|
863
|
+
name: contract.name,
|
|
864
|
+
type: "precondition",
|
|
865
|
+
passed: verdict.passed,
|
|
866
|
+
message: verdict.message,
|
|
867
|
+
source: contract.source ?? "yaml_precondition"
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
for (const contract of this._guard.getObserveSandboxContracts(
|
|
871
|
+
envelope
|
|
872
|
+
)) {
|
|
873
|
+
let verdict;
|
|
874
|
+
try {
|
|
875
|
+
verdict = await contract.check(envelope);
|
|
876
|
+
} catch (exc) {
|
|
877
|
+
verdict = Verdict.fail(
|
|
878
|
+
`Observe-mode sandbox error: ${exc}`,
|
|
879
|
+
{ policy_error: true }
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
results.push({
|
|
883
|
+
name: contract.name,
|
|
884
|
+
type: "sandbox",
|
|
885
|
+
passed: verdict.passed,
|
|
886
|
+
message: verdict.message,
|
|
887
|
+
source: contract.source ?? "yaml_sandbox"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
for (const contract of this._guard.getObserveSessionContracts()) {
|
|
891
|
+
let verdict;
|
|
892
|
+
try {
|
|
893
|
+
verdict = await contract.check(session);
|
|
894
|
+
} catch (exc) {
|
|
895
|
+
verdict = Verdict.fail(
|
|
896
|
+
`Observe-mode session contract error: ${exc}`,
|
|
897
|
+
{ policy_error: true }
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
results.push({
|
|
901
|
+
name: contract.name,
|
|
902
|
+
type: "session_contract",
|
|
903
|
+
passed: verdict.passed,
|
|
904
|
+
message: verdict.message,
|
|
905
|
+
source: contract.source ?? "yaml_session"
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return results;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/session.ts
|
|
913
|
+
function hasBatchGet(backend) {
|
|
914
|
+
return "batchGet" in backend;
|
|
915
|
+
}
|
|
916
|
+
var MAX_ID_LENGTH = 1e4;
|
|
917
|
+
function _validateStorageKeyComponent(value, label) {
|
|
918
|
+
if (!value) {
|
|
919
|
+
throw new EdictumConfigError(`Invalid ${label}: ${JSON.stringify(value)}`);
|
|
920
|
+
}
|
|
921
|
+
if (value.length > MAX_ID_LENGTH) {
|
|
922
|
+
throw new EdictumConfigError(`Invalid ${label}: exceeds ${MAX_ID_LENGTH} characters`);
|
|
923
|
+
}
|
|
924
|
+
for (let i = 0; i < value.length; i++) {
|
|
925
|
+
const code = value.charCodeAt(i);
|
|
926
|
+
if (code < 32 || code === 127) {
|
|
927
|
+
throw new EdictumConfigError(`Invalid ${label}: ${JSON.stringify(value)}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
var Session = class {
|
|
932
|
+
_sid;
|
|
933
|
+
_backend;
|
|
934
|
+
constructor(sessionId, backend) {
|
|
935
|
+
_validateStorageKeyComponent(sessionId, "session_id");
|
|
936
|
+
this._sid = sessionId;
|
|
937
|
+
this._backend = backend;
|
|
938
|
+
}
|
|
939
|
+
get sessionId() {
|
|
940
|
+
return this._sid;
|
|
941
|
+
}
|
|
942
|
+
/** Increment attempt counter. Called in PreToolUse (before governance). */
|
|
943
|
+
async incrementAttempts() {
|
|
944
|
+
return await this._backend.increment(`s:${this._sid}:attempts`);
|
|
945
|
+
}
|
|
946
|
+
async attemptCount() {
|
|
947
|
+
return Number(await this._backend.get(`s:${this._sid}:attempts`) ?? 0);
|
|
948
|
+
}
|
|
949
|
+
/** Record a tool execution. Called in PostToolUse. */
|
|
950
|
+
async recordExecution(toolName, success) {
|
|
951
|
+
_validateStorageKeyComponent(toolName, "tool_name");
|
|
952
|
+
await this._backend.increment(`s:${this._sid}:execs`);
|
|
953
|
+
await this._backend.increment(`s:${this._sid}:tool:${toolName}`);
|
|
954
|
+
if (success) {
|
|
955
|
+
await this._backend.delete(`s:${this._sid}:consec_fail`);
|
|
956
|
+
} else {
|
|
957
|
+
await this._backend.increment(`s:${this._sid}:consec_fail`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async executionCount() {
|
|
961
|
+
return Number(await this._backend.get(`s:${this._sid}:execs`) ?? 0);
|
|
962
|
+
}
|
|
963
|
+
async toolExecutionCount(tool) {
|
|
964
|
+
_validateStorageKeyComponent(tool, "tool_name");
|
|
965
|
+
return Number(
|
|
966
|
+
await this._backend.get(`s:${this._sid}:tool:${tool}`) ?? 0
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
async consecutiveFailures() {
|
|
970
|
+
return Number(
|
|
971
|
+
await this._backend.get(`s:${this._sid}:consec_fail`) ?? 0
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Pre-fetch multiple session counters in a single backend call.
|
|
976
|
+
*
|
|
977
|
+
* Returns a record with keys: "attempts", "execs", and optionally
|
|
978
|
+
* "tool:{name}" if includeTool is provided.
|
|
979
|
+
*
|
|
980
|
+
* Uses batchGet() on the backend when available (single HTTP round
|
|
981
|
+
* trip for ServerBackend). Falls back to individual get() calls for
|
|
982
|
+
* backends without batchGet support.
|
|
983
|
+
*/
|
|
984
|
+
async batchGetCounters(options) {
|
|
985
|
+
const keys = [
|
|
986
|
+
`s:${this._sid}:attempts`,
|
|
987
|
+
`s:${this._sid}:execs`
|
|
988
|
+
];
|
|
989
|
+
const keyLabels = ["attempts", "execs"];
|
|
990
|
+
if (options?.includeTool != null) {
|
|
991
|
+
_validateStorageKeyComponent(options.includeTool, "tool_name");
|
|
992
|
+
keys.push(`s:${this._sid}:tool:${options.includeTool}`);
|
|
993
|
+
keyLabels.push(`tool:${options.includeTool}`);
|
|
994
|
+
}
|
|
995
|
+
let raw;
|
|
996
|
+
if (hasBatchGet(this._backend)) {
|
|
997
|
+
raw = await this._backend.batchGet(keys);
|
|
998
|
+
} else {
|
|
999
|
+
raw = {};
|
|
1000
|
+
for (const key of keys) {
|
|
1001
|
+
raw[key] = await this._backend.get(key);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const result = {};
|
|
1005
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1006
|
+
const label = keyLabels[i] ?? "";
|
|
1007
|
+
const key = keys[i] ?? "";
|
|
1008
|
+
result[label] = Number(raw[key] ?? 0);
|
|
1009
|
+
}
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// src/runner.ts
|
|
1015
|
+
function defaultSuccessCheck(_toolName, result) {
|
|
1016
|
+
if (result == null) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof result === "object" && !Array.isArray(result)) {
|
|
1020
|
+
const dict = result;
|
|
1021
|
+
if (dict["is_error"]) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof result === "string") {
|
|
1026
|
+
const lower = result.slice(0, 7).toLowerCase();
|
|
1027
|
+
if (lower.startsWith("error:") || lower.startsWith("fatal:")) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
async function _emitRunPreAudit(guard, envelope, session, action, pre) {
|
|
1034
|
+
const event = createAuditEvent({
|
|
1035
|
+
action,
|
|
1036
|
+
runId: envelope.runId,
|
|
1037
|
+
callId: envelope.callId,
|
|
1038
|
+
toolName: envelope.toolName,
|
|
1039
|
+
toolArgs: guard.redaction.redactArgs(envelope.args),
|
|
1040
|
+
sideEffect: envelope.sideEffect,
|
|
1041
|
+
environment: envelope.environment,
|
|
1042
|
+
principal: envelope.principal ? { ...envelope.principal } : null,
|
|
1043
|
+
decisionSource: pre.decisionSource,
|
|
1044
|
+
decisionName: pre.decisionName,
|
|
1045
|
+
reason: pre.reason,
|
|
1046
|
+
hooksEvaluated: pre.hooksEvaluated,
|
|
1047
|
+
contractsEvaluated: pre.contractsEvaluated,
|
|
1048
|
+
sessionAttemptCount: await session.attemptCount(),
|
|
1049
|
+
sessionExecutionCount: await session.executionCount(),
|
|
1050
|
+
mode: guard.mode,
|
|
1051
|
+
policyVersion: guard.policyVersion,
|
|
1052
|
+
policyError: pre.policyError
|
|
1053
|
+
});
|
|
1054
|
+
await guard.auditSink.emit(event);
|
|
1055
|
+
}
|
|
1056
|
+
async function run(guard, toolName, args, toolCallable, options) {
|
|
1057
|
+
const sessionId = options?.sessionId ?? guard.sessionId;
|
|
1058
|
+
const session = new Session(sessionId, guard.backend);
|
|
1059
|
+
const pipeline = new GovernancePipeline(guard);
|
|
1060
|
+
const env = options?.environment ?? guard.environment;
|
|
1061
|
+
let principal = options?.principal ?? void 0;
|
|
1062
|
+
if (principal === void 0) {
|
|
1063
|
+
const resolved = guard._resolvePrincipal(toolName, args);
|
|
1064
|
+
if (resolved != null) {
|
|
1065
|
+
principal = resolved;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const envelope = createEnvelope(toolName, args, {
|
|
1069
|
+
runId: sessionId,
|
|
1070
|
+
environment: env,
|
|
1071
|
+
registry: guard.toolRegistry,
|
|
1072
|
+
principal: principal ?? null
|
|
1073
|
+
});
|
|
1074
|
+
await session.incrementAttempts();
|
|
1075
|
+
try {
|
|
1076
|
+
const pre = await pipeline.preExecute(envelope, session);
|
|
1077
|
+
if (pre.action === "pending_approval") {
|
|
1078
|
+
if (guard._approvalBackend == null) {
|
|
1079
|
+
throw new EdictumDenied(
|
|
1080
|
+
`Approval required but no approval backend configured: ${pre.reason}`,
|
|
1081
|
+
pre.decisionSource,
|
|
1082
|
+
pre.decisionName
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const principalDict = envelope.principal ? { ...envelope.principal } : null;
|
|
1086
|
+
const approvalRequest = await guard._approvalBackend.requestApproval(
|
|
1087
|
+
envelope.toolName,
|
|
1088
|
+
envelope.args,
|
|
1089
|
+
pre.approvalMessage ?? pre.reason ?? "",
|
|
1090
|
+
{
|
|
1091
|
+
timeout: pre.approvalTimeout,
|
|
1092
|
+
timeoutEffect: pre.approvalTimeoutEffect,
|
|
1093
|
+
principal: principalDict
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
await _emitRunPreAudit(
|
|
1097
|
+
guard,
|
|
1098
|
+
envelope,
|
|
1099
|
+
session,
|
|
1100
|
+
AuditAction.CALL_APPROVAL_REQUESTED,
|
|
1101
|
+
pre
|
|
1102
|
+
);
|
|
1103
|
+
const decision = await guard._approvalBackend.waitForDecision(
|
|
1104
|
+
approvalRequest.approvalId,
|
|
1105
|
+
pre.approvalTimeout
|
|
1106
|
+
);
|
|
1107
|
+
let approved = false;
|
|
1108
|
+
if (decision.status === ApprovalStatus.TIMEOUT) {
|
|
1109
|
+
await _emitRunPreAudit(
|
|
1110
|
+
guard,
|
|
1111
|
+
envelope,
|
|
1112
|
+
session,
|
|
1113
|
+
AuditAction.CALL_APPROVAL_TIMEOUT,
|
|
1114
|
+
pre
|
|
1115
|
+
);
|
|
1116
|
+
if (pre.approvalTimeoutEffect === "allow") {
|
|
1117
|
+
approved = true;
|
|
1118
|
+
}
|
|
1119
|
+
} else if (!decision.approved) {
|
|
1120
|
+
await _emitRunPreAudit(
|
|
1121
|
+
guard,
|
|
1122
|
+
envelope,
|
|
1123
|
+
session,
|
|
1124
|
+
AuditAction.CALL_APPROVAL_DENIED,
|
|
1125
|
+
pre
|
|
1126
|
+
);
|
|
1127
|
+
} else {
|
|
1128
|
+
approved = true;
|
|
1129
|
+
await _emitRunPreAudit(
|
|
1130
|
+
guard,
|
|
1131
|
+
envelope,
|
|
1132
|
+
session,
|
|
1133
|
+
AuditAction.CALL_APPROVAL_GRANTED,
|
|
1134
|
+
pre
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
if (approved) {
|
|
1138
|
+
if (guard._onAllow) {
|
|
1139
|
+
try {
|
|
1140
|
+
guard._onAllow(envelope);
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
const denyReason = decision.reason ?? pre.reason ?? "";
|
|
1146
|
+
if (guard._onDeny) {
|
|
1147
|
+
try {
|
|
1148
|
+
guard._onDeny(envelope, denyReason, pre.decisionName);
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
throw new EdictumDenied(
|
|
1153
|
+
decision.reason ?? pre.reason ?? "denied",
|
|
1154
|
+
pre.decisionSource,
|
|
1155
|
+
pre.decisionName
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const realDeny = pre.action === "deny" && !pre.observed;
|
|
1160
|
+
if (pre.action === "pending_approval") {
|
|
1161
|
+
} else if (realDeny) {
|
|
1162
|
+
const auditAction = guard.mode === "observe" ? AuditAction.CALL_WOULD_DENY : AuditAction.CALL_DENIED;
|
|
1163
|
+
await _emitRunPreAudit(guard, envelope, session, auditAction, pre);
|
|
1164
|
+
if (guard.mode === "enforce") {
|
|
1165
|
+
if (guard._onDeny) {
|
|
1166
|
+
try {
|
|
1167
|
+
guard._onDeny(envelope, pre.reason ?? "", pre.decisionName);
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
throw new EdictumDenied(
|
|
1172
|
+
pre.reason ?? "denied",
|
|
1173
|
+
pre.decisionSource,
|
|
1174
|
+
pre.decisionName
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
} else {
|
|
1178
|
+
for (const cr of pre.contractsEvaluated) {
|
|
1179
|
+
if (cr["observed"] && !cr["passed"]) {
|
|
1180
|
+
const observedEvent = createAuditEvent({
|
|
1181
|
+
action: AuditAction.CALL_WOULD_DENY,
|
|
1182
|
+
runId: envelope.runId,
|
|
1183
|
+
callId: envelope.callId,
|
|
1184
|
+
toolName: envelope.toolName,
|
|
1185
|
+
toolArgs: guard.redaction.redactArgs(envelope.args),
|
|
1186
|
+
sideEffect: envelope.sideEffect,
|
|
1187
|
+
environment: envelope.environment,
|
|
1188
|
+
principal: envelope.principal ? { ...envelope.principal } : null,
|
|
1189
|
+
decisionSource: "precondition",
|
|
1190
|
+
decisionName: cr["name"],
|
|
1191
|
+
reason: cr["message"],
|
|
1192
|
+
mode: "observe",
|
|
1193
|
+
policyVersion: guard.policyVersion,
|
|
1194
|
+
policyError: pre.policyError
|
|
1195
|
+
});
|
|
1196
|
+
await guard.auditSink.emit(observedEvent);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
await _emitRunPreAudit(
|
|
1200
|
+
guard,
|
|
1201
|
+
envelope,
|
|
1202
|
+
session,
|
|
1203
|
+
AuditAction.CALL_ALLOWED,
|
|
1204
|
+
pre
|
|
1205
|
+
);
|
|
1206
|
+
if (guard._onAllow) {
|
|
1207
|
+
try {
|
|
1208
|
+
guard._onAllow(envelope);
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
for (const sr of pre.observeResults) {
|
|
1214
|
+
const observeAction = sr["passed"] ? AuditAction.CALL_ALLOWED : AuditAction.CALL_WOULD_DENY;
|
|
1215
|
+
const observeEvent = createAuditEvent({
|
|
1216
|
+
action: observeAction,
|
|
1217
|
+
runId: envelope.runId,
|
|
1218
|
+
callId: envelope.callId,
|
|
1219
|
+
toolName: envelope.toolName,
|
|
1220
|
+
toolArgs: guard.redaction.redactArgs(envelope.args),
|
|
1221
|
+
sideEffect: envelope.sideEffect,
|
|
1222
|
+
environment: envelope.environment,
|
|
1223
|
+
principal: envelope.principal ? { ...envelope.principal } : null,
|
|
1224
|
+
decisionSource: sr["source"],
|
|
1225
|
+
decisionName: sr["name"],
|
|
1226
|
+
reason: sr["message"],
|
|
1227
|
+
mode: "observe",
|
|
1228
|
+
policyVersion: guard.policyVersion
|
|
1229
|
+
});
|
|
1230
|
+
await guard.auditSink.emit(observeEvent);
|
|
1231
|
+
}
|
|
1232
|
+
let result;
|
|
1233
|
+
let toolSuccess;
|
|
1234
|
+
try {
|
|
1235
|
+
result = toolCallable(envelope.args);
|
|
1236
|
+
if (result != null && typeof result === "object" && typeof result.then === "function") {
|
|
1237
|
+
result = await result;
|
|
1238
|
+
}
|
|
1239
|
+
if (guard._successCheck) {
|
|
1240
|
+
toolSuccess = guard._successCheck(toolName, result);
|
|
1241
|
+
} else {
|
|
1242
|
+
toolSuccess = defaultSuccessCheck(toolName, result);
|
|
1243
|
+
}
|
|
1244
|
+
} catch (e) {
|
|
1245
|
+
result = String(e);
|
|
1246
|
+
toolSuccess = false;
|
|
1247
|
+
}
|
|
1248
|
+
const post = await pipeline.postExecute(envelope, result, toolSuccess);
|
|
1249
|
+
await session.recordExecution(toolName, toolSuccess);
|
|
1250
|
+
const postAction = toolSuccess ? AuditAction.CALL_EXECUTED : AuditAction.CALL_FAILED;
|
|
1251
|
+
const postEvent = createAuditEvent({
|
|
1252
|
+
action: postAction,
|
|
1253
|
+
runId: envelope.runId,
|
|
1254
|
+
callId: envelope.callId,
|
|
1255
|
+
toolName: envelope.toolName,
|
|
1256
|
+
toolArgs: guard.redaction.redactArgs(envelope.args),
|
|
1257
|
+
sideEffect: envelope.sideEffect,
|
|
1258
|
+
environment: envelope.environment,
|
|
1259
|
+
principal: envelope.principal ? { ...envelope.principal } : null,
|
|
1260
|
+
toolSuccess,
|
|
1261
|
+
postconditionsPassed: post.postconditionsPassed,
|
|
1262
|
+
contractsEvaluated: post.contractsEvaluated,
|
|
1263
|
+
sessionAttemptCount: await session.attemptCount(),
|
|
1264
|
+
sessionExecutionCount: await session.executionCount(),
|
|
1265
|
+
mode: guard.mode,
|
|
1266
|
+
policyVersion: guard.policyVersion,
|
|
1267
|
+
policyError: post.policyError
|
|
1268
|
+
});
|
|
1269
|
+
await guard.auditSink.emit(postEvent);
|
|
1270
|
+
if (!toolSuccess) {
|
|
1271
|
+
throw new EdictumToolError(String(result));
|
|
1272
|
+
}
|
|
1273
|
+
return post.redactedResponse != null ? post.redactedResponse : result;
|
|
1274
|
+
} finally {
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
export {
|
|
1279
|
+
Verdict,
|
|
1280
|
+
HookResult,
|
|
1281
|
+
HookDecision,
|
|
1282
|
+
Session,
|
|
1283
|
+
RedactionPolicy,
|
|
1284
|
+
ApprovalStatus,
|
|
1285
|
+
LocalApprovalBackend,
|
|
1286
|
+
AuditAction,
|
|
1287
|
+
createAuditEvent,
|
|
1288
|
+
MarkEvictedError,
|
|
1289
|
+
CompositeSink,
|
|
1290
|
+
StdoutAuditSink,
|
|
1291
|
+
FileAuditSink,
|
|
1292
|
+
CollectingAuditSink,
|
|
1293
|
+
createPreDecision,
|
|
1294
|
+
createPostDecision,
|
|
1295
|
+
GovernancePipeline,
|
|
1296
|
+
defaultSuccessCheck,
|
|
1297
|
+
run
|
|
1298
|
+
};
|
|
1299
|
+
//# sourceMappingURL=chunk-X5E2YY35.mjs.map
|