@replayci/replay 0.1.4 → 0.1.6
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/index.cjs +3816 -36
- package/dist/index.d.cts +1022 -3
- package/dist/index.d.ts +1022 -3
- package/dist/index.js +3809 -36
- package/package.json +2 -4
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,13 +17,29 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
MemoryStore: () => MemoryStore,
|
|
34
|
+
ReplayConfigError: () => ReplayConfigError,
|
|
35
|
+
ReplayContractError: () => ReplayContractError,
|
|
36
|
+
ReplayKillError: () => ReplayKillError,
|
|
37
|
+
RuntimeClient: () => RuntimeClient,
|
|
38
|
+
RuntimeClientError: () => RuntimeClientError,
|
|
39
|
+
createRuntimeClient: () => createRuntimeClient,
|
|
23
40
|
observe: () => observe,
|
|
24
41
|
prepareContracts: () => prepareContracts,
|
|
42
|
+
replay: () => replay,
|
|
25
43
|
validate: () => validate
|
|
26
44
|
});
|
|
27
45
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -61,6 +79,7 @@ var CaptureBuffer = class {
|
|
|
61
79
|
remoteDisabled = false;
|
|
62
80
|
closed = false;
|
|
63
81
|
droppedOverflowTotal = 0;
|
|
82
|
+
hasSucceededOnce = false;
|
|
64
83
|
lastFlushAttemptMs = 0;
|
|
65
84
|
lastFlushSuccessMs = 0;
|
|
66
85
|
lastFlushErrorMs = 0;
|
|
@@ -127,7 +146,7 @@ var CaptureBuffer = class {
|
|
|
127
146
|
}
|
|
128
147
|
flush() {
|
|
129
148
|
if (this.closed || this.remoteDisabled) {
|
|
130
|
-
return Promise.resolve();
|
|
149
|
+
return Promise.resolve({ captured: 0, sent: 0, active: true, errors: [] });
|
|
131
150
|
}
|
|
132
151
|
if (this.flushPromise) {
|
|
133
152
|
return this.flushPromise;
|
|
@@ -137,6 +156,7 @@ var CaptureBuffer = class {
|
|
|
137
156
|
type: "flush_error",
|
|
138
157
|
error: err instanceof Error ? err.message : String(err)
|
|
139
158
|
});
|
|
159
|
+
return { captured: 0, sent: 0, active: true, errors: [err instanceof Error ? err.message : String(err)] };
|
|
140
160
|
}).finally(() => {
|
|
141
161
|
if (this.flushPromise === flushPromise) {
|
|
142
162
|
this.flushPromise = void 0;
|
|
@@ -165,11 +185,11 @@ var CaptureBuffer = class {
|
|
|
165
185
|
}
|
|
166
186
|
async flushOnce() {
|
|
167
187
|
if (this.closed || this.remoteDisabled || this.queue.length === 0) {
|
|
168
|
-
return;
|
|
188
|
+
return { captured: 0, sent: 0, active: true, errors: [] };
|
|
169
189
|
}
|
|
170
190
|
const now = this.now();
|
|
171
191
|
if (this.circuitOpenUntil > now) {
|
|
172
|
-
return;
|
|
192
|
+
return { captured: 0, sent: 0, active: true, errors: [] };
|
|
173
193
|
}
|
|
174
194
|
if (this.circuitOpenUntil !== 0) {
|
|
175
195
|
this.circuitOpenUntil = 0;
|
|
@@ -177,8 +197,9 @@ var CaptureBuffer = class {
|
|
|
177
197
|
}
|
|
178
198
|
const batch = this.queue.splice(0, Math.min(this.queue.length, MAX_BATCH_SIZE));
|
|
179
199
|
if (batch.length === 0) {
|
|
180
|
-
return;
|
|
200
|
+
return { captured: 0, sent: 0, active: true, errors: [] };
|
|
181
201
|
}
|
|
202
|
+
const captured = batch.length;
|
|
182
203
|
this.lastFlushAttemptMs = this.now();
|
|
183
204
|
emitStateChange(this.onStateChange, { type: "flush_attempt" });
|
|
184
205
|
let payload = "";
|
|
@@ -186,7 +207,7 @@ var CaptureBuffer = class {
|
|
|
186
207
|
payload = JSON.stringify({ captures: batch });
|
|
187
208
|
} catch {
|
|
188
209
|
this.handleFailure("JSON serialization failed");
|
|
189
|
-
return;
|
|
210
|
+
return { captured, sent: 0, active: true, errors: ["JSON serialization failed"] };
|
|
190
211
|
}
|
|
191
212
|
const controller = new AbortController();
|
|
192
213
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
@@ -209,19 +230,24 @@ var CaptureBuffer = class {
|
|
|
209
230
|
this.circuitOpenUntil = Number.MAX_SAFE_INTEGER;
|
|
210
231
|
emitDiagnostics(this.diagnostics, { type: "remote_disabled" });
|
|
211
232
|
emitStateChange(this.onStateChange, { type: "remote_disabled" });
|
|
212
|
-
return;
|
|
233
|
+
return { captured, sent: 0, active: true, errors: ["remote_disabled"] };
|
|
213
234
|
}
|
|
214
235
|
if (!response.ok) {
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
const msg = `HTTP ${response.status}`;
|
|
237
|
+
this.handleFailure(msg);
|
|
238
|
+
return { captured, sent: 0, active: true, errors: [msg] };
|
|
217
239
|
}
|
|
218
240
|
this.failureCount = 0;
|
|
219
241
|
this.circuitOpenUntil = 0;
|
|
242
|
+
this.hasSucceededOnce = true;
|
|
220
243
|
this.lastFlushSuccessMs = this.now();
|
|
221
244
|
this.lastFlushErrorMsg = null;
|
|
222
245
|
emitStateChange(this.onStateChange, { type: "flush_success", batch_size: batch.length });
|
|
246
|
+
return { captured, sent: captured, active: true, errors: [] };
|
|
223
247
|
} catch (err) {
|
|
224
|
-
|
|
248
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
249
|
+
this.handleFailure(msg);
|
|
250
|
+
return { captured, sent: 0, active: true, errors: [msg] };
|
|
225
251
|
} finally {
|
|
226
252
|
clearTimeout(timeout);
|
|
227
253
|
}
|
|
@@ -231,10 +257,12 @@ var CaptureBuffer = class {
|
|
|
231
257
|
const errorStr = errorMsg ?? "unknown error";
|
|
232
258
|
this.lastFlushErrorMs = this.now();
|
|
233
259
|
this.lastFlushErrorMsg = errorStr;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
if (this.hasSucceededOnce) {
|
|
261
|
+
emitDiagnostics(this.diagnostics, {
|
|
262
|
+
type: "flush_error",
|
|
263
|
+
error: errorStr
|
|
264
|
+
});
|
|
265
|
+
}
|
|
238
266
|
emitStateChange(this.onStateChange, { type: "flush_error", error: errorStr });
|
|
239
267
|
if (this.failureCount >= CIRCUIT_BREAKER_FAILURE_LIMIT) {
|
|
240
268
|
this.circuitOpenUntil = this.now() + CIRCUIT_BREAKER_MS;
|
|
@@ -546,10 +574,12 @@ var ReplayConfigurationError = class extends Error {
|
|
|
546
574
|
};
|
|
547
575
|
var ReplayInternalError = class extends Error {
|
|
548
576
|
cause;
|
|
577
|
+
sessionId;
|
|
549
578
|
constructor(message, options) {
|
|
550
579
|
super(message);
|
|
551
580
|
this.name = "ReplayInternalError";
|
|
552
581
|
this.cause = options?.cause;
|
|
582
|
+
this.sessionId = options?.sessionId;
|
|
553
583
|
}
|
|
554
584
|
};
|
|
555
585
|
|
|
@@ -1434,8 +1464,35 @@ function ensureDir(dir) {
|
|
|
1434
1464
|
|
|
1435
1465
|
// src/observe.ts
|
|
1436
1466
|
var REPLAY_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
|
|
1467
|
+
var REPLAY_ATTACHED = /* @__PURE__ */ Symbol.for("replayci.replay_attached");
|
|
1437
1468
|
var DEFAULT_AGENT = "default";
|
|
1438
1469
|
var IDLE_HEARTBEAT_MS = 3e4;
|
|
1470
|
+
function defaultDiagnosticsHandler(event) {
|
|
1471
|
+
switch (event.type) {
|
|
1472
|
+
case "observe_inactive":
|
|
1473
|
+
if (event.reason_code === "missing_api_key") {
|
|
1474
|
+
console.warn(
|
|
1475
|
+
"[replayci] No API key provided. observe() is inactive \u2014 calls will not be captured. Set REPLAYCI_API_KEY or pass apiKey to observe()."
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
break;
|
|
1479
|
+
case "activation_warning":
|
|
1480
|
+
console.warn(`[replayci] ${event.message}`);
|
|
1481
|
+
break;
|
|
1482
|
+
case "flush_error":
|
|
1483
|
+
console.warn(`[replayci] flush failed: ${event.error}`);
|
|
1484
|
+
break;
|
|
1485
|
+
case "flush_empty":
|
|
1486
|
+
console.warn(
|
|
1487
|
+
"[replayci] flush(): No calls were captured. Ensure your LLM calls use the client returned by observe()."
|
|
1488
|
+
);
|
|
1489
|
+
break;
|
|
1490
|
+
case "circuit_open":
|
|
1491
|
+
break;
|
|
1492
|
+
default:
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1439
1496
|
function observe(client, opts = {}) {
|
|
1440
1497
|
assertSupportedNodeRuntime();
|
|
1441
1498
|
const sessionId = generateSessionId();
|
|
@@ -1445,38 +1502,45 @@ function observe(client, opts = {}) {
|
|
|
1445
1502
|
if (isDisabled(opts)) {
|
|
1446
1503
|
return createInactiveHandle(client, sessionId, agent, "disabled", void 0, now, opts.diagnostics, opts.stateDir);
|
|
1447
1504
|
}
|
|
1505
|
+
const diagnosticsHandler = opts.diagnostics ?? defaultDiagnosticsHandler;
|
|
1448
1506
|
const apiKey = resolveApiKey(opts);
|
|
1507
|
+
if (apiKey && !/^rci_(live|test)_/.test(apiKey)) {
|
|
1508
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1509
|
+
type: "activation_warning",
|
|
1510
|
+
message: "API key format looks wrong (expected 'rci_live_...' or 'rci_test_...'). Verify your key at app.replayci.com/settings."
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1449
1513
|
if (!apiKey) {
|
|
1450
|
-
return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now,
|
|
1514
|
+
return createInactiveHandle(client, sessionId, agent, "missing_api_key", void 0, now, diagnosticsHandler, opts.stateDir);
|
|
1451
1515
|
}
|
|
1452
|
-
const provider = detectProviderSafely(client,
|
|
1516
|
+
const provider = detectProviderSafely(client, diagnosticsHandler);
|
|
1453
1517
|
if (!provider) {
|
|
1454
|
-
return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now,
|
|
1518
|
+
return createInactiveHandle(client, sessionId, agent, "unsupported_client", "Could not detect provider.", now, diagnosticsHandler, opts.stateDir);
|
|
1455
1519
|
}
|
|
1456
1520
|
const patchTarget = resolvePatchTarget(client, provider);
|
|
1457
1521
|
if (!patchTarget) {
|
|
1458
|
-
emitDiagnostic(
|
|
1522
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1459
1523
|
type: "unsupported_client",
|
|
1460
1524
|
mode: "observe",
|
|
1461
1525
|
detail: `Unsupported ${provider} client shape.`
|
|
1462
1526
|
});
|
|
1463
|
-
return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now,
|
|
1527
|
+
return createInactiveHandle(client, sessionId, agent, "unsupported_client", `Unsupported ${provider} client shape.`, now, diagnosticsHandler, opts.stateDir);
|
|
1464
1528
|
}
|
|
1465
|
-
if (isWrapped(client, patchTarget.target)) {
|
|
1466
|
-
emitDiagnostic(
|
|
1529
|
+
if (isWrapped(client, patchTarget.target) || isReplayAttached(client)) {
|
|
1530
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1467
1531
|
type: "double_wrap",
|
|
1468
1532
|
mode: "observe"
|
|
1469
1533
|
});
|
|
1470
|
-
return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now,
|
|
1534
|
+
return createInactiveHandle(client, sessionId, agent, "double_wrap", void 0, now, diagnosticsHandler, opts.stateDir);
|
|
1471
1535
|
}
|
|
1472
1536
|
const patchabilityError = getPatchabilityError(patchTarget.target, patchTarget.methodName);
|
|
1473
1537
|
if (patchabilityError) {
|
|
1474
|
-
emitDiagnostic(
|
|
1538
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1475
1539
|
type: "unsupported_client",
|
|
1476
1540
|
mode: "observe",
|
|
1477
1541
|
detail: patchabilityError
|
|
1478
1542
|
});
|
|
1479
|
-
return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now,
|
|
1543
|
+
return createInactiveHandle(client, sessionId, agent, "patch_target_unwritable", patchabilityError, now, diagnosticsHandler, opts.stateDir);
|
|
1480
1544
|
}
|
|
1481
1545
|
const captureLevel = normalizeCaptureLevel(opts.captureLevel);
|
|
1482
1546
|
const patchTargetName = `${provider}.${provider === "openai" ? "chat.completions.create" : "messages.create"}`;
|
|
@@ -1534,7 +1598,7 @@ function observe(client, opts = {}) {
|
|
|
1534
1598
|
const detail = err instanceof Error ? err.message : "Failed to write health snapshot";
|
|
1535
1599
|
lastHealthStoreErrorAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1536
1600
|
lastHealthStoreError = detail;
|
|
1537
|
-
emitDiagnostic(
|
|
1601
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1538
1602
|
type: "health_store_error",
|
|
1539
1603
|
detail
|
|
1540
1604
|
});
|
|
@@ -1574,7 +1638,7 @@ function observe(client, opts = {}) {
|
|
|
1574
1638
|
runtimeState.consecutive_failures = 0;
|
|
1575
1639
|
runtimeState.circuit_open_until = null;
|
|
1576
1640
|
runtimeState.queue_size = buffer.size;
|
|
1577
|
-
emitDiagnostic(
|
|
1641
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1578
1642
|
type: "flush_succeeded",
|
|
1579
1643
|
batch_size: event.batch_size
|
|
1580
1644
|
});
|
|
@@ -1610,7 +1674,7 @@ function observe(client, opts = {}) {
|
|
|
1610
1674
|
maxBuffer: opts.maxBuffer,
|
|
1611
1675
|
flushMs: opts.flushMs,
|
|
1612
1676
|
timeoutMs: opts.timeoutMs,
|
|
1613
|
-
diagnostics:
|
|
1677
|
+
diagnostics: diagnosticsHandler,
|
|
1614
1678
|
onStateChange: onBufferStateChange
|
|
1615
1679
|
});
|
|
1616
1680
|
registerBeforeExit(buffer);
|
|
@@ -1634,7 +1698,7 @@ function observe(client, opts = {}) {
|
|
|
1634
1698
|
persistHealthEvent();
|
|
1635
1699
|
safeRunJanitor(sessionsDir, sessionId);
|
|
1636
1700
|
startHeartbeat();
|
|
1637
|
-
emitDiagnostic(
|
|
1701
|
+
emitDiagnostic(diagnosticsHandler, {
|
|
1638
1702
|
type: "observe_activated",
|
|
1639
1703
|
session_id: sessionId,
|
|
1640
1704
|
provider,
|
|
@@ -1657,7 +1721,7 @@ function observe(client, opts = {}) {
|
|
|
1657
1721
|
sessionId,
|
|
1658
1722
|
runtimeState,
|
|
1659
1723
|
persistHealthEvent,
|
|
1660
|
-
diagnostics:
|
|
1724
|
+
diagnostics: diagnosticsHandler
|
|
1661
1725
|
});
|
|
1662
1726
|
return response;
|
|
1663
1727
|
});
|
|
@@ -1667,8 +1731,12 @@ function observe(client, opts = {}) {
|
|
|
1667
1731
|
let restored = false;
|
|
1668
1732
|
return {
|
|
1669
1733
|
client,
|
|
1670
|
-
flush() {
|
|
1671
|
-
|
|
1734
|
+
async flush() {
|
|
1735
|
+
const result = await buffer.flush();
|
|
1736
|
+
if (result.captured === 0 && result.errors.length === 0) {
|
|
1737
|
+
emitDiagnostic(diagnosticsHandler, { type: "flush_empty" });
|
|
1738
|
+
}
|
|
1739
|
+
return result;
|
|
1672
1740
|
},
|
|
1673
1741
|
restore() {
|
|
1674
1742
|
if (restored) {
|
|
@@ -1707,7 +1775,7 @@ function observe(client, opts = {}) {
|
|
|
1707
1775
|
};
|
|
1708
1776
|
} catch (err) {
|
|
1709
1777
|
const detail = err instanceof Error ? err.message : "Unknown internal error";
|
|
1710
|
-
return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics, opts.stateDir);
|
|
1778
|
+
return createInactiveHandle(client, sessionId, agent, "internal_error", detail, now, opts.diagnostics ?? defaultDiagnosticsHandler, opts.stateDir);
|
|
1711
1779
|
}
|
|
1712
1780
|
}
|
|
1713
1781
|
function createInactiveHandle(client, sessionId, agent, reasonCode, detail, activatedAt, diagnostics, stateDir) {
|
|
@@ -1734,7 +1802,7 @@ function createInactiveHandle(client, sessionId, agent, reasonCode, detail, acti
|
|
|
1734
1802
|
return {
|
|
1735
1803
|
client,
|
|
1736
1804
|
flush() {
|
|
1737
|
-
return Promise.resolve();
|
|
1805
|
+
return Promise.resolve({ captured: 0, sent: 0, active: false, errors: [] });
|
|
1738
1806
|
},
|
|
1739
1807
|
restore() {
|
|
1740
1808
|
},
|
|
@@ -2061,6 +2129,9 @@ function isWrapped(client, target) {
|
|
|
2061
2129
|
client[REPLAY_WRAPPED] || target[REPLAY_WRAPPED]
|
|
2062
2130
|
);
|
|
2063
2131
|
}
|
|
2132
|
+
function isReplayAttached(client) {
|
|
2133
|
+
return Boolean(client[REPLAY_ATTACHED]);
|
|
2134
|
+
}
|
|
2064
2135
|
function setWrapped(client, target) {
|
|
2065
2136
|
try {
|
|
2066
2137
|
client[REPLAY_WRAPPED] = true;
|
|
@@ -2126,7 +2197,16 @@ var import_contracts_core3 = require("@replayci/contracts-core");
|
|
|
2126
2197
|
var import_contracts_core2 = require("@replayci/contracts-core");
|
|
2127
2198
|
var import_node_fs2 = require("fs");
|
|
2128
2199
|
var import_node_path2 = require("path");
|
|
2200
|
+
|
|
2201
|
+
// src/safeRegex.ts
|
|
2202
|
+
var import_re2 = __toESM(require("re2"), 1);
|
|
2203
|
+
function safeRegex(pattern) {
|
|
2204
|
+
return new import_re2.default(pattern);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/contracts.ts
|
|
2129
2208
|
var CONTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2209
|
+
var SESSION_YAML_NAMES = /* @__PURE__ */ new Set(["session.yaml", "session.yml"]);
|
|
2130
2210
|
var MAX_REGEX_BYTES = 1024;
|
|
2131
2211
|
var NESTED_QUANTIFIER_RE = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)(?:[+*]|\{\d+(?:,\d*)?\})/;
|
|
2132
2212
|
function loadContracts(input) {
|
|
@@ -2218,7 +2298,7 @@ function collectContractFiles(inputPath) {
|
|
|
2218
2298
|
if (entry.isDirectory()) {
|
|
2219
2299
|
return collectContractFiles(fullPath);
|
|
2220
2300
|
}
|
|
2221
|
-
if (entry.isFile() && CONTRACT_EXTENSIONS.has((0, import_node_path2.extname)(entry.name).toLowerCase())) {
|
|
2301
|
+
if (entry.isFile() && CONTRACT_EXTENSIONS.has((0, import_node_path2.extname)(entry.name).toLowerCase()) && !SESSION_YAML_NAMES.has(entry.name.toLowerCase())) {
|
|
2222
2302
|
return [fullPath];
|
|
2223
2303
|
}
|
|
2224
2304
|
return [];
|
|
@@ -2268,7 +2348,19 @@ function normalizeInlineContract(input) {
|
|
|
2268
2348
|
...expectedToolCalls.length > 0 ? { expected_tool_calls: expectedToolCalls } : {},
|
|
2269
2349
|
...isMatchMode(source.tool_call_match_mode) ? {
|
|
2270
2350
|
tool_call_match_mode: source.tool_call_match_mode
|
|
2271
|
-
} : {}
|
|
2351
|
+
} : {},
|
|
2352
|
+
// replay() fields — pass through when present on the Contract type
|
|
2353
|
+
...source.response_format_invariants != null ? { response_format_invariants: source.response_format_invariants } : {},
|
|
2354
|
+
...source.policy != null ? { policy: source.policy } : {},
|
|
2355
|
+
...source.execution_constraints != null ? { execution_constraints: source.execution_constraints } : {},
|
|
2356
|
+
...Array.isArray(source.preconditions) ? { preconditions: source.preconditions } : {},
|
|
2357
|
+
...Array.isArray(source.forbids_after) ? { forbids_after: source.forbids_after } : {},
|
|
2358
|
+
...source.session_limits != null ? { session_limits: source.session_limits } : {},
|
|
2359
|
+
...Array.isArray(source.argument_value_invariants) ? { argument_value_invariants: source.argument_value_invariants } : {},
|
|
2360
|
+
...source.transitions != null ? { transitions: source.transitions } : {},
|
|
2361
|
+
...source.gate != null ? { gate: source.gate } : {},
|
|
2362
|
+
...source.evidence_class != null ? { evidence_class: source.evidence_class } : {},
|
|
2363
|
+
...source.commit_requirement != null ? { commit_requirement: source.commit_requirement } : {}
|
|
2272
2364
|
};
|
|
2273
2365
|
validateSafeRegexes(contract);
|
|
2274
2366
|
return contract;
|
|
@@ -2303,6 +2395,12 @@ function validateSafeRegexes(contract) {
|
|
|
2303
2395
|
invariants: expectedToolCall.argument_invariants ?? []
|
|
2304
2396
|
});
|
|
2305
2397
|
}
|
|
2398
|
+
if (contract.argument_value_invariants) {
|
|
2399
|
+
invariantGroups.push({
|
|
2400
|
+
label: "argument_value_invariants",
|
|
2401
|
+
invariants: contract.argument_value_invariants
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2306
2404
|
for (const group of invariantGroups) {
|
|
2307
2405
|
for (const invariant of group.invariants) {
|
|
2308
2406
|
if (typeof invariant.regex !== "string") {
|
|
@@ -2319,7 +2417,7 @@ function validateSafeRegexes(contract) {
|
|
|
2319
2417
|
);
|
|
2320
2418
|
}
|
|
2321
2419
|
try {
|
|
2322
|
-
void
|
|
2420
|
+
void safeRegex(invariant.regex);
|
|
2323
2421
|
} catch (error) {
|
|
2324
2422
|
throw new ReplayConfigurationError(
|
|
2325
2423
|
`Invalid regex in ${contractLabel} (${group.label}, ${invariant.path}): ${formatErrorMessage(error)}`
|
|
@@ -2410,7 +2508,7 @@ function toToolOrder(value, hasExpectedTools) {
|
|
|
2410
2508
|
return hasExpectedTools ? "any" : void 0;
|
|
2411
2509
|
}
|
|
2412
2510
|
function isSideEffect(value) {
|
|
2413
|
-
return value === "read" || value === "write" || value === "destructive";
|
|
2511
|
+
return value === "read" || value === "write" || value === "destructive" || value === "admin" || value === "financial";
|
|
2414
2512
|
}
|
|
2415
2513
|
function formatErrorMessage(error) {
|
|
2416
2514
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2813,9 +2911,3691 @@ function toRecord6(value) {
|
|
|
2813
2911
|
function toString6(value) {
|
|
2814
2912
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2815
2913
|
}
|
|
2914
|
+
|
|
2915
|
+
// src/replay.ts
|
|
2916
|
+
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
2917
|
+
var import_node_fs3 = require("fs");
|
|
2918
|
+
var import_node_path3 = require("path");
|
|
2919
|
+
var import_contracts_core6 = require("@replayci/contracts-core");
|
|
2920
|
+
|
|
2921
|
+
// src/redaction.ts
|
|
2922
|
+
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
2923
|
+
|
|
2924
|
+
// ../../artifacts/schema/redaction-patterns.json
|
|
2925
|
+
var redaction_patterns_default = {
|
|
2926
|
+
schema_version: "1.0",
|
|
2927
|
+
fingerprint_algorithm: "sha256",
|
|
2928
|
+
patterns: [
|
|
2929
|
+
{
|
|
2930
|
+
name: "slack_token",
|
|
2931
|
+
detect: "xox[baprs]-[A-Za-z0-9-]+",
|
|
2932
|
+
detect_flags: "g",
|
|
2933
|
+
redact: "xox[baprs]-[A-Za-z0-9-]+",
|
|
2934
|
+
redact_flags: "g",
|
|
2935
|
+
replacement: "[REDACTED]"
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
name: "bearer_token",
|
|
2939
|
+
detect: "Bearer\\s+[A-Za-z0-9._-]{10,}",
|
|
2940
|
+
detect_flags: "g",
|
|
2941
|
+
redact: "Bearer\\s+[A-Za-z0-9._-]{10,}",
|
|
2942
|
+
redact_flags: "g",
|
|
2943
|
+
replacement: "Bearer [REDACTED]"
|
|
2944
|
+
},
|
|
2945
|
+
{
|
|
2946
|
+
name: "connection_string",
|
|
2947
|
+
detect: "(?:postgresql|mysql|mongodb(?:\\+srv)?)://[^\\s]+@[^\\s]+",
|
|
2948
|
+
detect_flags: "g",
|
|
2949
|
+
redact: "((?:postgresql|mysql|mongodb(?:\\+srv)?)://)[^\\s]+@([^\\s]+)",
|
|
2950
|
+
redact_flags: "g",
|
|
2951
|
+
replacement: "$1[REDACTED]@$2"
|
|
2952
|
+
},
|
|
2953
|
+
{
|
|
2954
|
+
name: "openai_api_key",
|
|
2955
|
+
detect: "sk-(?:proj-)?[A-Za-z0-9]{10,}",
|
|
2956
|
+
detect_flags: "g",
|
|
2957
|
+
redact: "sk-(?:proj-)?[A-Za-z0-9]{10,}",
|
|
2958
|
+
redact_flags: "g",
|
|
2959
|
+
replacement: "[REDACTED]"
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
name: "anthropic_api_key",
|
|
2963
|
+
detect: "sk-ant-[A-Za-z0-9_-]{20,}",
|
|
2964
|
+
detect_flags: "g",
|
|
2965
|
+
redact: "sk-ant-[A-Za-z0-9_-]{20,}",
|
|
2966
|
+
redact_flags: "g",
|
|
2967
|
+
replacement: "[REDACTED]"
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
name: "api_key_header",
|
|
2971
|
+
detect: "(?:api[_-]key|x-api-key)\\s*[:=]\\s*[A-Za-z0-9._-]{10,}",
|
|
2972
|
+
detect_flags: "gi",
|
|
2973
|
+
redact: "((?:api[_-]key|x-api-key)\\s*[:=]\\s*)[A-Za-z0-9._-]{10,}",
|
|
2974
|
+
redact_flags: "gi",
|
|
2975
|
+
replacement: "$1[REDACTED]"
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
name: "private_key",
|
|
2979
|
+
detect: "-----BEGIN\\s[\\w\\s]*PRIVATE\\sKEY-----",
|
|
2980
|
+
detect_flags: "g",
|
|
2981
|
+
redact: "-----BEGIN\\s[\\w\\s]*PRIVATE\\sKEY-----[\\s\\S]*?-----END\\s[\\w\\s]*PRIVATE\\sKEY-----",
|
|
2982
|
+
redact_flags: "g",
|
|
2983
|
+
replacement: "[REDACTED_PRIVATE_KEY]"
|
|
2984
|
+
}
|
|
2985
|
+
]
|
|
2986
|
+
};
|
|
2987
|
+
|
|
2988
|
+
// src/redaction.ts
|
|
2989
|
+
function sha256Hex(s) {
|
|
2990
|
+
return import_node_crypto2.default.createHash("sha256").update(s).digest("hex");
|
|
2991
|
+
}
|
|
2992
|
+
var compiledPatterns = redaction_patterns_default.patterns.map((p) => ({
|
|
2993
|
+
name: p.name,
|
|
2994
|
+
detectRe: new RegExp(p.detect, p.detect_flags),
|
|
2995
|
+
redactRe: new RegExp(p.redact, p.redact_flags),
|
|
2996
|
+
replacement: p.replacement
|
|
2997
|
+
}));
|
|
2998
|
+
var PATTERN_FINGERPRINT = sha256Hex(
|
|
2999
|
+
JSON.stringify(redaction_patterns_default.patterns)
|
|
3000
|
+
);
|
|
3001
|
+
function detectFindings(s) {
|
|
3002
|
+
const findings = [];
|
|
3003
|
+
for (const pattern of compiledPatterns) {
|
|
3004
|
+
for (const m of s.matchAll(pattern.detectRe)) {
|
|
3005
|
+
findings.push({ kind: pattern.name, sample_hash: sha256Hex(m[0]) });
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return findings;
|
|
3009
|
+
}
|
|
3010
|
+
function redactString(s) {
|
|
3011
|
+
let out = s;
|
|
3012
|
+
for (const pattern of compiledPatterns) {
|
|
3013
|
+
out = out.replace(pattern.redactRe, pattern.replacement);
|
|
3014
|
+
}
|
|
3015
|
+
return out;
|
|
3016
|
+
}
|
|
3017
|
+
function redactCapture(input) {
|
|
3018
|
+
const findings = detectFindings(input);
|
|
3019
|
+
const redacted = redactString(input);
|
|
3020
|
+
return {
|
|
3021
|
+
redacted,
|
|
3022
|
+
findings,
|
|
3023
|
+
redacted_any: redacted !== input,
|
|
3024
|
+
pattern_fingerprint: PATTERN_FINGERPRINT
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/errors/replay.ts
|
|
3029
|
+
var ReplayContractError = class extends Error {
|
|
3030
|
+
decision;
|
|
3031
|
+
contractFile;
|
|
3032
|
+
failures;
|
|
3033
|
+
constructor(message, decision, contractFile, failures) {
|
|
3034
|
+
super(message);
|
|
3035
|
+
this.name = "ReplayContractError";
|
|
3036
|
+
this.decision = decision;
|
|
3037
|
+
this.contractFile = contractFile;
|
|
3038
|
+
this.failures = failures;
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
var ReplayKillError = class extends Error {
|
|
3042
|
+
sessionId;
|
|
3043
|
+
killedAt;
|
|
3044
|
+
constructor(sessionId, killedAt) {
|
|
3045
|
+
super(`Session ${sessionId} was killed at ${killedAt}`);
|
|
3046
|
+
this.name = "ReplayKillError";
|
|
3047
|
+
this.sessionId = sessionId;
|
|
3048
|
+
this.killedAt = killedAt;
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
var ReplayConfigError = class extends ReplayConfigurationError {
|
|
3052
|
+
condition;
|
|
3053
|
+
details;
|
|
3054
|
+
constructor(condition, details) {
|
|
3055
|
+
super(`ReplayConfigError: ${condition} \u2014 ${details}`);
|
|
3056
|
+
this.name = "ReplayConfigError";
|
|
3057
|
+
this.condition = condition;
|
|
3058
|
+
this.details = details;
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
|
|
3062
|
+
// src/gate.ts
|
|
3063
|
+
function applyGateDecision(decision, response, provider, gateMode, onBlock) {
|
|
3064
|
+
if (decision.action === "allow") {
|
|
3065
|
+
return response;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
onBlock?.(decision);
|
|
3069
|
+
} catch {
|
|
3070
|
+
}
|
|
3071
|
+
if (gateMode === "reject_all") {
|
|
3072
|
+
throw buildContractError(decision);
|
|
3073
|
+
}
|
|
3074
|
+
const allowedCalls = getAllowedCalls(decision.tool_calls, decision.blocked);
|
|
3075
|
+
if (gateMode === "strip_partial") {
|
|
3076
|
+
if (allowedCalls.length === 0) {
|
|
3077
|
+
throw buildContractError(decision);
|
|
3078
|
+
}
|
|
3079
|
+
return stripBlockedCalls(response, allowedCalls, provider);
|
|
3080
|
+
}
|
|
3081
|
+
if (allowedCalls.length === 0) {
|
|
3082
|
+
return normalizeStrippedResponse(response, provider);
|
|
3083
|
+
}
|
|
3084
|
+
return stripBlockedCalls(response, allowedCalls, provider);
|
|
3085
|
+
}
|
|
3086
|
+
function normalizeStrippedResponse(response, provider) {
|
|
3087
|
+
if (provider === "openai") {
|
|
3088
|
+
return normalizeOpenAIStripped(response);
|
|
3089
|
+
}
|
|
3090
|
+
return normalizeAnthropicStripped(response);
|
|
3091
|
+
}
|
|
3092
|
+
function getAllowedCalls(allCalls, blocked) {
|
|
3093
|
+
const blockedPool = blocked.map((b) => ({ name: b.tool_name, args: b.arguments }));
|
|
3094
|
+
return allCalls.filter((call) => {
|
|
3095
|
+
const exactIdx = blockedPool.findIndex(
|
|
3096
|
+
(b) => b.name === call.name && b.args === call.arguments
|
|
3097
|
+
);
|
|
3098
|
+
if (exactIdx !== -1) {
|
|
3099
|
+
blockedPool.splice(exactIdx, 1);
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3102
|
+
const nameIdx = blockedPool.findIndex(
|
|
3103
|
+
(b) => b.name === call.name && b.args === ""
|
|
3104
|
+
);
|
|
3105
|
+
if (nameIdx !== -1) {
|
|
3106
|
+
blockedPool.splice(nameIdx, 1);
|
|
3107
|
+
return false;
|
|
3108
|
+
}
|
|
3109
|
+
return true;
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
function stripBlockedCalls(response, allowedCalls, provider) {
|
|
3113
|
+
const allowedIds = new Set(allowedCalls.map((c) => c.id));
|
|
3114
|
+
if (provider === "openai") {
|
|
3115
|
+
return stripOpenAICalls(response, allowedIds);
|
|
3116
|
+
}
|
|
3117
|
+
return stripAnthropicCalls(response, allowedIds);
|
|
3118
|
+
}
|
|
3119
|
+
function stripOpenAICalls(response, allowedIds) {
|
|
3120
|
+
const record = toRecord7(response);
|
|
3121
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
3122
|
+
if (choices.length === 0) return response;
|
|
3123
|
+
const firstChoice = toRecord7(choices[0]);
|
|
3124
|
+
const message = toRecord7(firstChoice.message);
|
|
3125
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
3126
|
+
const filtered = toolCalls.filter((tc) => {
|
|
3127
|
+
const id = toRecord7(tc).id;
|
|
3128
|
+
return typeof id === "string" && allowedIds.has(id);
|
|
3129
|
+
});
|
|
3130
|
+
return {
|
|
3131
|
+
...record,
|
|
3132
|
+
replay_modified: true,
|
|
3133
|
+
choices: [{
|
|
3134
|
+
...firstChoice,
|
|
3135
|
+
message: {
|
|
3136
|
+
...message,
|
|
3137
|
+
tool_calls: filtered
|
|
3138
|
+
}
|
|
3139
|
+
}]
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
function stripAnthropicCalls(response, allowedIds) {
|
|
3143
|
+
const record = toRecord7(response);
|
|
3144
|
+
const content = Array.isArray(record.content) ? record.content : [];
|
|
3145
|
+
const filtered = content.filter((block) => {
|
|
3146
|
+
const b = toRecord7(block);
|
|
3147
|
+
if (b.type !== "tool_use") return true;
|
|
3148
|
+
const id = b.id;
|
|
3149
|
+
return typeof id === "string" && allowedIds.has(id);
|
|
3150
|
+
});
|
|
3151
|
+
return {
|
|
3152
|
+
...record,
|
|
3153
|
+
replay_modified: true,
|
|
3154
|
+
content: filtered
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
function normalizeOpenAIStripped(response) {
|
|
3158
|
+
const record = toRecord7(response);
|
|
3159
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
3160
|
+
if (choices.length === 0) return { ...record, replay_modified: true };
|
|
3161
|
+
const firstChoice = toRecord7(choices[0]);
|
|
3162
|
+
const message = toRecord7(firstChoice.message);
|
|
3163
|
+
const content = typeof message.content === "string" && message.content.length > 0 ? message.content : "[replay: all tool calls blocked]";
|
|
3164
|
+
const finishReason = firstChoice.finish_reason === "tool_calls" ? "stop" : firstChoice.finish_reason;
|
|
3165
|
+
return {
|
|
3166
|
+
...record,
|
|
3167
|
+
replay_modified: true,
|
|
3168
|
+
choices: [{
|
|
3169
|
+
...firstChoice,
|
|
3170
|
+
finish_reason: finishReason,
|
|
3171
|
+
message: {
|
|
3172
|
+
...message,
|
|
3173
|
+
content,
|
|
3174
|
+
tool_calls: void 0
|
|
3175
|
+
}
|
|
3176
|
+
}]
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
function normalizeAnthropicStripped(response) {
|
|
3180
|
+
const record = toRecord7(response);
|
|
3181
|
+
const contentBlocks = Array.isArray(record.content) ? record.content : [];
|
|
3182
|
+
const textBlocks = contentBlocks.filter((b) => toRecord7(b).type === "text");
|
|
3183
|
+
const stopReason = record.stop_reason === "tool_use" ? "end_turn" : record.stop_reason;
|
|
3184
|
+
const content = textBlocks.length > 0 ? textBlocks : [{ type: "text", text: "[replay: all tool calls blocked]" }];
|
|
3185
|
+
return {
|
|
3186
|
+
...record,
|
|
3187
|
+
replay_modified: true,
|
|
3188
|
+
stop_reason: stopReason,
|
|
3189
|
+
content
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
function buildContractError(decision) {
|
|
3193
|
+
if (decision.action !== "block") {
|
|
3194
|
+
throw new Error("Cannot build contract error from allow decision");
|
|
3195
|
+
}
|
|
3196
|
+
const first = decision.blocked[0];
|
|
3197
|
+
return new ReplayContractError(
|
|
3198
|
+
`Tool call blocked: ${first?.tool_name ?? "unknown"} \u2014 ${first?.reason ?? "unknown"}`,
|
|
3199
|
+
decision,
|
|
3200
|
+
first?.contract_file ?? "",
|
|
3201
|
+
first?.failures ?? []
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
function toRecord7(value) {
|
|
3205
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// src/responseFormat.ts
|
|
3209
|
+
function extractResponseMetadata(response, provider) {
|
|
3210
|
+
if (provider === "openai") {
|
|
3211
|
+
return extractOpenAIMetadata(response);
|
|
3212
|
+
}
|
|
3213
|
+
return extractAnthropicMetadata(response);
|
|
3214
|
+
}
|
|
3215
|
+
function extractOpenAIMetadata(response) {
|
|
3216
|
+
const record = toRecord8(response);
|
|
3217
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
3218
|
+
if (choices.length === 0) {
|
|
3219
|
+
return { finish_reason: null, content: null, tool_calls_present: false, has_content: false };
|
|
3220
|
+
}
|
|
3221
|
+
const firstChoice = toRecord8(choices[0]);
|
|
3222
|
+
const message = toRecord8(firstChoice.message);
|
|
3223
|
+
const finishReason = typeof firstChoice.finish_reason === "string" ? firstChoice.finish_reason : null;
|
|
3224
|
+
const content = typeof message.content === "string" ? message.content : null;
|
|
3225
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
3226
|
+
return {
|
|
3227
|
+
finish_reason: finishReason,
|
|
3228
|
+
content,
|
|
3229
|
+
tool_calls_present: toolCalls.length > 0,
|
|
3230
|
+
has_content: content !== null && content.length > 0
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
function extractAnthropicMetadata(response) {
|
|
3234
|
+
const record = toRecord8(response);
|
|
3235
|
+
const stopReason = typeof record.stop_reason === "string" ? record.stop_reason : null;
|
|
3236
|
+
const contentBlocks = Array.isArray(record.content) ? record.content : [];
|
|
3237
|
+
const textBlocks = contentBlocks.filter(
|
|
3238
|
+
(block) => toRecord8(block).type === "text"
|
|
3239
|
+
);
|
|
3240
|
+
const toolUseBlocks = contentBlocks.filter(
|
|
3241
|
+
(block) => toRecord8(block).type === "tool_use"
|
|
3242
|
+
);
|
|
3243
|
+
const textContent = textBlocks.map((block) => {
|
|
3244
|
+
const text = toRecord8(block).text;
|
|
3245
|
+
return typeof text === "string" ? text : "";
|
|
3246
|
+
}).join("\n");
|
|
3247
|
+
let finishReason = null;
|
|
3248
|
+
if (stopReason === "end_turn") {
|
|
3249
|
+
finishReason = "stop";
|
|
3250
|
+
} else if (stopReason === "tool_use") {
|
|
3251
|
+
finishReason = "tool_calls";
|
|
3252
|
+
} else if (stopReason !== null) {
|
|
3253
|
+
finishReason = stopReason;
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
finish_reason: finishReason,
|
|
3257
|
+
content: textContent.length > 0 ? textContent : null,
|
|
3258
|
+
tool_calls_present: toolUseBlocks.length > 0,
|
|
3259
|
+
has_content: textContent.length > 0
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
function evaluateResponseFormatInvariants(response, contracts, requestToolNames, provider) {
|
|
3263
|
+
const requestToolSet = new Set(requestToolNames);
|
|
3264
|
+
const metadata = extractResponseMetadata(response, provider);
|
|
3265
|
+
const failures = [];
|
|
3266
|
+
for (const contract of contracts) {
|
|
3267
|
+
if (!contract.response_format_invariants) continue;
|
|
3268
|
+
if (!requestToolSet.has(contract.tool)) continue;
|
|
3269
|
+
const rfi = contract.response_format_invariants;
|
|
3270
|
+
const contractFile = contract.contract_file ?? contract.tool;
|
|
3271
|
+
failures.push(...checkFinishReason(rfi, metadata, contractFile));
|
|
3272
|
+
failures.push(...checkContentWhenToolCalls(rfi, metadata, contractFile));
|
|
3273
|
+
failures.push(...checkToolCallsPresent(rfi, metadata, contractFile));
|
|
3274
|
+
}
|
|
3275
|
+
return { failures };
|
|
3276
|
+
}
|
|
3277
|
+
function checkFinishReason(rfi, metadata, contractFile) {
|
|
3278
|
+
if (rfi.finish_reason === void 0) return [];
|
|
3279
|
+
if (metadata.finish_reason === rfi.finish_reason) return [];
|
|
3280
|
+
return [{
|
|
3281
|
+
path: "$.finish_reason",
|
|
3282
|
+
operator: "response_format",
|
|
3283
|
+
expected: rfi.finish_reason,
|
|
3284
|
+
found: metadata.finish_reason,
|
|
3285
|
+
message: `Expected finish_reason "${rfi.finish_reason}", got "${metadata.finish_reason}"`,
|
|
3286
|
+
contract_file: contractFile
|
|
3287
|
+
}];
|
|
3288
|
+
}
|
|
3289
|
+
function checkContentWhenToolCalls(rfi, metadata, contractFile) {
|
|
3290
|
+
if (rfi.content_when_tool_calls === void 0) return [];
|
|
3291
|
+
if (rfi.content_when_tool_calls !== "empty") return [];
|
|
3292
|
+
if (!metadata.tool_calls_present) return [];
|
|
3293
|
+
if (!metadata.has_content) return [];
|
|
3294
|
+
return [{
|
|
3295
|
+
path: "$.content",
|
|
3296
|
+
operator: "response_format",
|
|
3297
|
+
expected: "empty when tool_calls present",
|
|
3298
|
+
found: "content present",
|
|
3299
|
+
message: 'content_when_tool_calls is "empty" but response has both content and tool_calls',
|
|
3300
|
+
contract_file: contractFile
|
|
3301
|
+
}];
|
|
3302
|
+
}
|
|
3303
|
+
function checkToolCallsPresent(rfi, metadata, contractFile) {
|
|
3304
|
+
if (rfi.tool_calls_present === void 0) return [];
|
|
3305
|
+
if (rfi.tool_calls_present === metadata.tool_calls_present) return [];
|
|
3306
|
+
return [{
|
|
3307
|
+
path: "$.tool_calls",
|
|
3308
|
+
operator: "response_format",
|
|
3309
|
+
expected: rfi.tool_calls_present,
|
|
3310
|
+
found: metadata.tool_calls_present,
|
|
3311
|
+
message: rfi.tool_calls_present ? "tool_calls_present: true but no tool_calls in response" : "tool_calls_present: false but tool_calls found in response",
|
|
3312
|
+
contract_file: contractFile
|
|
3313
|
+
}];
|
|
3314
|
+
}
|
|
3315
|
+
function toRecord8(value) {
|
|
3316
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
// src/sessionState.ts
|
|
3320
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
3321
|
+
|
|
3322
|
+
// src/phases.ts
|
|
3323
|
+
function validatePhaseTransition(toolCalls, sessionState, compiledSession) {
|
|
3324
|
+
if (!compiledSession.phases) {
|
|
3325
|
+
return { legal: true, newPhase: sessionState.currentPhase };
|
|
3326
|
+
}
|
|
3327
|
+
const attemptedTransitions = [];
|
|
3328
|
+
for (const toolCall of toolCalls) {
|
|
3329
|
+
const contract = compiledSession.perToolContracts.get(toolCall.name);
|
|
3330
|
+
if (!contract?.transitions?.advances_to) continue;
|
|
3331
|
+
const allowedTransitions = compiledSession.transitions.get(
|
|
3332
|
+
sessionState.currentPhase ?? ""
|
|
3333
|
+
);
|
|
3334
|
+
if (!allowedTransitions?.includes(contract.transitions.advances_to)) {
|
|
3335
|
+
return {
|
|
3336
|
+
legal: false,
|
|
3337
|
+
newPhase: sessionState.currentPhase,
|
|
3338
|
+
blockedTool: toolCall.name,
|
|
3339
|
+
attemptedTransition: `${sessionState.currentPhase} \u2192 ${contract.transitions.advances_to}`,
|
|
3340
|
+
reason: "illegal_phase_transition"
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
attemptedTransitions.push({
|
|
3344
|
+
tool: toolCall.name,
|
|
3345
|
+
target: contract.transitions.advances_to
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (attemptedTransitions.length > 1) {
|
|
3349
|
+
const distinctTargets = new Set(attemptedTransitions.map((t) => t.target));
|
|
3350
|
+
if (distinctTargets.size > 1) {
|
|
3351
|
+
return {
|
|
3352
|
+
legal: false,
|
|
3353
|
+
newPhase: sessionState.currentPhase,
|
|
3354
|
+
blockedTool: attemptedTransitions.map((t) => t.tool).join(", "),
|
|
3355
|
+
attemptedTransition: attemptedTransitions.map((t) => `${t.tool} \u2192 ${t.target}`).join("; "),
|
|
3356
|
+
reason: "ambiguous_phase_transition"
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
if (attemptedTransitions.length > 0) {
|
|
3361
|
+
return { legal: true, newPhase: attemptedTransitions[0].target };
|
|
3362
|
+
}
|
|
3363
|
+
return { legal: true, newPhase: sessionState.currentPhase };
|
|
3364
|
+
}
|
|
3365
|
+
function recomputePhaseFromCommitted(committedCalls, sessionState, compiledSession) {
|
|
3366
|
+
if (!compiledSession.phases) return sessionState.currentPhase;
|
|
3367
|
+
const transitions = [];
|
|
3368
|
+
for (const tc of committedCalls) {
|
|
3369
|
+
const contract = compiledSession.perToolContracts.get(tc.toolName);
|
|
3370
|
+
if (!contract?.transitions?.advances_to) continue;
|
|
3371
|
+
transitions.push(contract.transitions.advances_to);
|
|
3372
|
+
}
|
|
3373
|
+
if (transitions.length === 0) return sessionState.currentPhase;
|
|
3374
|
+
const distinct = new Set(transitions);
|
|
3375
|
+
if (distinct.size > 1) {
|
|
3376
|
+
return sessionState.currentPhase;
|
|
3377
|
+
}
|
|
3378
|
+
return transitions[0];
|
|
3379
|
+
}
|
|
3380
|
+
function getLegalNextPhases(sessionState, compiledSession) {
|
|
3381
|
+
if (!compiledSession.phases) return [];
|
|
3382
|
+
return compiledSession.transitions.get(sessionState.currentPhase ?? "") ?? [];
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/sessionState.ts
|
|
3386
|
+
var MAX_STEPS_HARD_CAP = 1e4;
|
|
3387
|
+
function createInitialState(sessionId, options) {
|
|
3388
|
+
return {
|
|
3389
|
+
sessionId,
|
|
3390
|
+
agent: options?.agent ?? null,
|
|
3391
|
+
principal: options?.principal ?? null,
|
|
3392
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3393
|
+
tier: options?.tier ?? "compat",
|
|
3394
|
+
stateVersion: 0,
|
|
3395
|
+
controlRevision: 0,
|
|
3396
|
+
currentPhase: null,
|
|
3397
|
+
totalStepCount: 0,
|
|
3398
|
+
totalToolCalls: 0,
|
|
3399
|
+
actualCost: 0,
|
|
3400
|
+
totalCost: 0,
|
|
3401
|
+
toolCallCounts: /* @__PURE__ */ new Map(),
|
|
3402
|
+
forbiddenTools: /* @__PURE__ */ new Set(),
|
|
3403
|
+
satisfiedPreconditions: /* @__PURE__ */ new Map(),
|
|
3404
|
+
steps: [],
|
|
3405
|
+
pendingEntries: [],
|
|
3406
|
+
lastStep: null,
|
|
3407
|
+
consecutiveBlockCount: 0,
|
|
3408
|
+
consecutiveErrorCount: 0,
|
|
3409
|
+
totalBlockCount: 0,
|
|
3410
|
+
totalUnguardedCalls: 0,
|
|
3411
|
+
killed: false,
|
|
3412
|
+
contractHash: null
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
function finalizeExecutedStep(state, step, contracts, compiledSession) {
|
|
3416
|
+
const newSteps = [...state.steps, step];
|
|
3417
|
+
const newToolCallCounts = updateToolCallCounts(state.toolCallCounts, step);
|
|
3418
|
+
const resolvedContracts = compiledSession ? Array.from(compiledSession.perToolContracts.values()) : contracts;
|
|
3419
|
+
const newForbiddenTools = updateForbidden(state.forbiddenTools, step, resolvedContracts);
|
|
3420
|
+
const newSatisfiedPreconditions = updatePreconditionCache(
|
|
3421
|
+
state.satisfiedPreconditions,
|
|
3422
|
+
step
|
|
3423
|
+
);
|
|
3424
|
+
const costDelta = computeStepCost(step);
|
|
3425
|
+
const newPhase = compiledSession ? recomputePhaseFromCommitted(step.toolCalls, state, compiledSession) : state.currentPhase;
|
|
3426
|
+
return {
|
|
3427
|
+
...state,
|
|
3428
|
+
steps: newSteps,
|
|
3429
|
+
currentPhase: newPhase,
|
|
3430
|
+
totalStepCount: state.totalStepCount + 1,
|
|
3431
|
+
totalToolCalls: state.totalToolCalls + step.toolCalls.length,
|
|
3432
|
+
totalCost: state.totalCost + costDelta,
|
|
3433
|
+
toolCallCounts: newToolCallCounts,
|
|
3434
|
+
forbiddenTools: newForbiddenTools,
|
|
3435
|
+
satisfiedPreconditions: newSatisfiedPreconditions,
|
|
3436
|
+
lastStep: step,
|
|
3437
|
+
stateVersion: state.stateVersion + 1
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
function updateActualCost(state, costDelta) {
|
|
3441
|
+
return {
|
|
3442
|
+
...state,
|
|
3443
|
+
actualCost: state.actualCost + costDelta
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
function recordDecisionOutcome(state, outcome) {
|
|
3447
|
+
switch (outcome) {
|
|
3448
|
+
case "allowed":
|
|
3449
|
+
return {
|
|
3450
|
+
...state,
|
|
3451
|
+
consecutiveBlockCount: 0,
|
|
3452
|
+
consecutiveErrorCount: 0
|
|
3453
|
+
};
|
|
3454
|
+
case "blocked":
|
|
3455
|
+
return {
|
|
3456
|
+
...state,
|
|
3457
|
+
consecutiveBlockCount: state.consecutiveBlockCount + 1,
|
|
3458
|
+
consecutiveErrorCount: 0,
|
|
3459
|
+
totalBlockCount: state.totalBlockCount + 1
|
|
3460
|
+
};
|
|
3461
|
+
case "error":
|
|
3462
|
+
return {
|
|
3463
|
+
...state,
|
|
3464
|
+
consecutiveErrorCount: state.consecutiveErrorCount + 1,
|
|
3465
|
+
consecutiveBlockCount: 0
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
function killSession(state) {
|
|
3470
|
+
return {
|
|
3471
|
+
...state,
|
|
3472
|
+
killed: true
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
function isAtHardStepCap(state) {
|
|
3476
|
+
return state.totalStepCount >= MAX_STEPS_HARD_CAP;
|
|
3477
|
+
}
|
|
3478
|
+
function computeArgumentsHash(args) {
|
|
3479
|
+
return import_node_crypto3.default.createHash("sha256").update(args).digest("hex").slice(0, 16);
|
|
3480
|
+
}
|
|
3481
|
+
var RESOURCE_SEPARATOR = "\0";
|
|
3482
|
+
function makeForbiddenKey(toolName, resourceValue) {
|
|
3483
|
+
if (resourceValue === void 0) return toolName;
|
|
3484
|
+
return `${toolName}${RESOURCE_SEPARATOR}${JSON.stringify(resourceValue)}`;
|
|
3485
|
+
}
|
|
3486
|
+
function isForbidden(forbiddenTools, toolName, resourceValue) {
|
|
3487
|
+
if (forbiddenTools.has(toolName)) return true;
|
|
3488
|
+
if (resourceValue !== void 0) {
|
|
3489
|
+
return forbiddenTools.has(makeForbiddenKey(toolName, resourceValue));
|
|
3490
|
+
}
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
function updateToolCallCounts(counts, step) {
|
|
3494
|
+
const updated = new Map(counts);
|
|
3495
|
+
for (const tc of step.toolCalls) {
|
|
3496
|
+
updated.set(tc.toolName, (updated.get(tc.toolName) ?? 0) + 1);
|
|
3497
|
+
}
|
|
3498
|
+
return updated;
|
|
3499
|
+
}
|
|
3500
|
+
function updateForbidden(forbidden, step, contracts) {
|
|
3501
|
+
const updated = new Set(forbidden);
|
|
3502
|
+
const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
|
|
3503
|
+
for (const tc of step.toolCalls) {
|
|
3504
|
+
const contract = contractByTool.get(tc.toolName);
|
|
3505
|
+
if (contract?.forbids_after) {
|
|
3506
|
+
for (const entry of contract.forbids_after) {
|
|
3507
|
+
if (typeof entry === "string") {
|
|
3508
|
+
updated.add(entry);
|
|
3509
|
+
} else {
|
|
3510
|
+
const resourcePath = entry.resource;
|
|
3511
|
+
if (resourcePath && tc.resourceValues) {
|
|
3512
|
+
const resourceValue = tc.resourceValues[resourcePath];
|
|
3513
|
+
if (resourceValue !== void 0) {
|
|
3514
|
+
updated.add(makeForbiddenKey(entry.tool, resourceValue));
|
|
3515
|
+
} else {
|
|
3516
|
+
updated.add(entry.tool);
|
|
3517
|
+
}
|
|
3518
|
+
} else {
|
|
3519
|
+
updated.add(entry.tool);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return updated;
|
|
3526
|
+
}
|
|
3527
|
+
function updatePreconditionCache(cache, step) {
|
|
3528
|
+
const updated = new Map(cache);
|
|
3529
|
+
for (const tc of step.toolCalls) {
|
|
3530
|
+
updated.set(tc.toolName, step.outputExtract);
|
|
3531
|
+
if (tc.resourceValues) {
|
|
3532
|
+
for (const [_path, value] of Object.entries(tc.resourceValues)) {
|
|
3533
|
+
const resourceKey = `${tc.toolName}:${JSON.stringify(value)}`;
|
|
3534
|
+
updated.set(resourceKey, step.outputExtract);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
return updated;
|
|
3539
|
+
}
|
|
3540
|
+
function computeStepCost(step) {
|
|
3541
|
+
if (!step.usage) return 0;
|
|
3542
|
+
return (step.usage.prompt_tokens + step.usage.completion_tokens) * 1e-5;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// src/sessionLimits.ts
|
|
3546
|
+
function checkSessionLimits(state, limits) {
|
|
3547
|
+
if (typeof limits.max_steps === "number" && state.totalStepCount >= limits.max_steps) {
|
|
3548
|
+
return {
|
|
3549
|
+
exceeded: true,
|
|
3550
|
+
reason: `max_steps exceeded: ${state.totalStepCount} >= ${limits.max_steps}`
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
if (typeof limits.max_tool_calls === "number" && state.totalToolCalls >= limits.max_tool_calls) {
|
|
3554
|
+
return {
|
|
3555
|
+
exceeded: true,
|
|
3556
|
+
reason: `max_tool_calls exceeded: ${state.totalToolCalls} >= ${limits.max_tool_calls}`
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
if (typeof limits.max_cost_per_session === "number" && state.actualCost >= limits.max_cost_per_session) {
|
|
3560
|
+
return {
|
|
3561
|
+
exceeded: true,
|
|
3562
|
+
reason: `max_cost_per_session exceeded: ${state.actualCost} >= ${limits.max_cost_per_session}`
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
return { exceeded: false, reason: null };
|
|
3566
|
+
}
|
|
3567
|
+
function checkPerToolLimits(state, toolName, limits) {
|
|
3568
|
+
if (!limits.max_calls_per_tool) return { exceeded: false, reason: null };
|
|
3569
|
+
const max = limits.max_calls_per_tool[toolName];
|
|
3570
|
+
if (typeof max !== "number") return { exceeded: false, reason: null };
|
|
3571
|
+
const current = state.toolCallCounts.get(toolName) ?? 0;
|
|
3572
|
+
if (current >= max) {
|
|
3573
|
+
return {
|
|
3574
|
+
exceeded: true,
|
|
3575
|
+
reason: `max_calls_per_tool.${toolName} exceeded: ${current} >= ${max}`
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
return { exceeded: false, reason: null };
|
|
3579
|
+
}
|
|
3580
|
+
function checkLoopDetection(toolName, argsString, state, config) {
|
|
3581
|
+
const { window, threshold } = config;
|
|
3582
|
+
const windowSteps = state.steps.slice(-window);
|
|
3583
|
+
const targetHash = computeArgumentsHash(argsString);
|
|
3584
|
+
const targetTuple = `${toolName}:${targetHash}`;
|
|
3585
|
+
let matchCount = 0;
|
|
3586
|
+
for (const step of windowSteps) {
|
|
3587
|
+
for (const tc of step.toolCalls) {
|
|
3588
|
+
if (`${tc.toolName}:${tc.arguments_hash}` === targetTuple) {
|
|
3589
|
+
matchCount++;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
return {
|
|
3594
|
+
triggered: matchCount >= threshold,
|
|
3595
|
+
matchCount,
|
|
3596
|
+
threshold,
|
|
3597
|
+
window
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
function checkCircuitBreaker(state, config) {
|
|
3601
|
+
if (state.consecutiveBlockCount >= config.consecutive_blocks) {
|
|
3602
|
+
return { triggered: true, reason: "consecutive_blocks" };
|
|
3603
|
+
}
|
|
3604
|
+
if (state.consecutiveErrorCount >= config.consecutive_errors) {
|
|
3605
|
+
return { triggered: true, reason: "consecutive_errors" };
|
|
3606
|
+
}
|
|
3607
|
+
return { triggered: false, reason: null };
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/preconditions.ts
|
|
3611
|
+
function evaluatePreconditions(preconditions, sessionState, currentArguments) {
|
|
3612
|
+
return preconditions.map(
|
|
3613
|
+
(p) => evaluatePrecondition(p, sessionState, currentArguments)
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
function evaluatePrecondition(precondition, sessionState, currentArguments) {
|
|
3617
|
+
if (precondition.requires_step_count) {
|
|
3618
|
+
const required = precondition.requires_step_count.gte;
|
|
3619
|
+
if (sessionState.totalStepCount < required) {
|
|
3620
|
+
return {
|
|
3621
|
+
satisfied: false,
|
|
3622
|
+
detail: `Need ${required} prior steps, have ${sessionState.totalStepCount}`
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
if (precondition.requires_prior_tool) {
|
|
3627
|
+
const toolName = precondition.requires_prior_tool;
|
|
3628
|
+
const resourcePath = precondition.resource ? typeof precondition.resource === "string" ? precondition.resource : precondition.resource.path : void 0;
|
|
3629
|
+
const resourceValue = resourcePath ? extractPath(currentArguments ?? {}, resourcePath) : void 0;
|
|
3630
|
+
const cacheKey = resourceValue !== void 0 ? `${toolName}:${JSON.stringify(resourceValue)}` : toolName;
|
|
3631
|
+
let priorStep;
|
|
3632
|
+
for (let i = sessionState.steps.length - 1; i >= 0; i--) {
|
|
3633
|
+
const s = sessionState.steps[i];
|
|
3634
|
+
if (s.toolCalls.some((tc) => {
|
|
3635
|
+
if (tc.toolName !== toolName) return false;
|
|
3636
|
+
if (tc.proposal_decision !== "allowed") return false;
|
|
3637
|
+
if (resourceValue !== void 0 && tc.resourceValues?.[resourcePath] !== resourceValue) {
|
|
3638
|
+
return false;
|
|
3639
|
+
}
|
|
3640
|
+
return true;
|
|
3641
|
+
})) {
|
|
3642
|
+
priorStep = s;
|
|
3643
|
+
break;
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
const cachedExtract = sessionState.satisfiedPreconditions.get(cacheKey);
|
|
3647
|
+
if (!priorStep && cachedExtract === void 0) {
|
|
3648
|
+
const detail = resourceValue !== void 0 ? `Required prior tool ${toolName} not found for resource ${JSON.stringify(resourceValue)}` : `Required prior tool ${toolName} not found in session`;
|
|
3649
|
+
return { satisfied: false, detail };
|
|
3650
|
+
}
|
|
3651
|
+
if (precondition.with_output) {
|
|
3652
|
+
const extract = priorStep?.outputExtract ?? cachedExtract ?? {};
|
|
3653
|
+
for (const assertion of precondition.with_output) {
|
|
3654
|
+
const value = extractPath(extract, assertion.path);
|
|
3655
|
+
if (assertion.equals !== void 0 && value !== assertion.equals) {
|
|
3656
|
+
return {
|
|
3657
|
+
satisfied: false,
|
|
3658
|
+
detail: `Prior tool output assertion failed: ${assertion.path} \u2014 expected ${JSON.stringify(assertion.equals)}, got ${JSON.stringify(value)}`
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return { satisfied: true, detail: "" };
|
|
3665
|
+
}
|
|
3666
|
+
function extractPath(obj, path) {
|
|
3667
|
+
const cleanPath = path.startsWith("$.") ? path.slice(2) : path;
|
|
3668
|
+
if (cleanPath === "" || cleanPath === "$") return obj;
|
|
3669
|
+
const segments = cleanPath.split(".");
|
|
3670
|
+
let current = obj;
|
|
3671
|
+
for (const segment of segments) {
|
|
3672
|
+
if (current === null || current === void 0) return void 0;
|
|
3673
|
+
if (typeof current !== "object") return void 0;
|
|
3674
|
+
current = current[segment];
|
|
3675
|
+
}
|
|
3676
|
+
return current;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/crossStep.ts
|
|
3680
|
+
function validateCrossStep(toolCalls, sessionState, contracts) {
|
|
3681
|
+
const failures = [];
|
|
3682
|
+
const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
|
|
3683
|
+
const workingForbidden = new Set(sessionState.forbiddenTools);
|
|
3684
|
+
for (const tc of toolCalls) {
|
|
3685
|
+
const contract = contractByTool.get(tc.name);
|
|
3686
|
+
let parsedArgs;
|
|
3687
|
+
try {
|
|
3688
|
+
parsedArgs = JSON.parse(tc.arguments);
|
|
3689
|
+
} catch {
|
|
3690
|
+
parsedArgs = void 0;
|
|
3691
|
+
}
|
|
3692
|
+
let resourceValue;
|
|
3693
|
+
if (parsedArgs && contract?.preconditions) {
|
|
3694
|
+
for (const pre of contract.preconditions) {
|
|
3695
|
+
if (pre.resource) {
|
|
3696
|
+
const path = typeof pre.resource === "string" ? pre.resource : pre.resource.path;
|
|
3697
|
+
resourceValue = extractPath(parsedArgs, path);
|
|
3698
|
+
break;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
if (isForbidden(workingForbidden, tc.name, resourceValue)) {
|
|
3703
|
+
failures.push({
|
|
3704
|
+
toolName: tc.name,
|
|
3705
|
+
reason: "forbidden_tool",
|
|
3706
|
+
detail: resourceValue !== void 0 ? `Tool "${tc.name}" is forbidden in this session for resource ${JSON.stringify(resourceValue)}` : `Tool "${tc.name}" is forbidden in this session`
|
|
3707
|
+
});
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
if (contract?.preconditions && contract.preconditions.length > 0) {
|
|
3711
|
+
const results = evaluatePreconditions(
|
|
3712
|
+
contract.preconditions,
|
|
3713
|
+
sessionState,
|
|
3714
|
+
parsedArgs
|
|
3715
|
+
);
|
|
3716
|
+
for (const result of results) {
|
|
3717
|
+
if (!result.satisfied) {
|
|
3718
|
+
failures.push({
|
|
3719
|
+
toolName: tc.name,
|
|
3720
|
+
reason: "precondition_not_met",
|
|
3721
|
+
detail: result.detail
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
if (contract?.forbids_after) {
|
|
3727
|
+
for (const entry of contract.forbids_after) {
|
|
3728
|
+
if (typeof entry === "string") {
|
|
3729
|
+
workingForbidden.add(entry);
|
|
3730
|
+
} else {
|
|
3731
|
+
const resourcePath = entry.resource;
|
|
3732
|
+
if (resourcePath && parsedArgs) {
|
|
3733
|
+
const val = extractPath(parsedArgs, resourcePath);
|
|
3734
|
+
if (val !== void 0) {
|
|
3735
|
+
workingForbidden.add(makeForbiddenKey(entry.tool, val));
|
|
3736
|
+
} else {
|
|
3737
|
+
workingForbidden.add(entry.tool);
|
|
3738
|
+
}
|
|
3739
|
+
} else {
|
|
3740
|
+
workingForbidden.add(entry.tool);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
return {
|
|
3747
|
+
passed: failures.length === 0,
|
|
3748
|
+
failures
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// src/argumentValues.ts
|
|
3753
|
+
function evaluateArgumentValueInvariants(parsedArguments, invariants) {
|
|
3754
|
+
const failures = [];
|
|
3755
|
+
for (const inv of invariants) {
|
|
3756
|
+
const value = extractPath(parsedArguments, inv.path);
|
|
3757
|
+
if (inv.exact_match !== void 0) {
|
|
3758
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
3759
|
+
if (strValue !== inv.exact_match) {
|
|
3760
|
+
failures.push({
|
|
3761
|
+
path: inv.path,
|
|
3762
|
+
operator: "exact_match",
|
|
3763
|
+
expected: inv.exact_match,
|
|
3764
|
+
actual: value,
|
|
3765
|
+
detail: `Expected exact match "${inv.exact_match}", got "${strValue}"`
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
if (inv.regex !== void 0) {
|
|
3770
|
+
const strValue = typeof value === "string" ? value : String(value);
|
|
3771
|
+
try {
|
|
3772
|
+
const re = safeRegex(inv.regex);
|
|
3773
|
+
if (!re.test(strValue)) {
|
|
3774
|
+
failures.push({
|
|
3775
|
+
path: inv.path,
|
|
3776
|
+
operator: "regex",
|
|
3777
|
+
expected: inv.regex,
|
|
3778
|
+
actual: value,
|
|
3779
|
+
detail: `Value "${strValue}" does not match regex "${inv.regex}"`
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
} catch {
|
|
3783
|
+
failures.push({
|
|
3784
|
+
path: inv.path,
|
|
3785
|
+
operator: "regex",
|
|
3786
|
+
expected: inv.regex,
|
|
3787
|
+
actual: value,
|
|
3788
|
+
detail: `Invalid regex pattern: "${inv.regex}"`
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (inv.one_of !== void 0) {
|
|
3793
|
+
const match = inv.one_of.some((candidate) => {
|
|
3794
|
+
if (typeof candidate === typeof value) {
|
|
3795
|
+
return JSON.stringify(candidate) === JSON.stringify(value);
|
|
3796
|
+
}
|
|
3797
|
+
return false;
|
|
3798
|
+
});
|
|
3799
|
+
if (!match) {
|
|
3800
|
+
failures.push({
|
|
3801
|
+
path: inv.path,
|
|
3802
|
+
operator: "one_of",
|
|
3803
|
+
expected: inv.one_of,
|
|
3804
|
+
actual: value,
|
|
3805
|
+
detail: `Value ${JSON.stringify(value)} not in ${JSON.stringify(inv.one_of)}`
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
if (inv.type !== void 0) {
|
|
3810
|
+
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
3811
|
+
if (actualType !== inv.type) {
|
|
3812
|
+
failures.push({
|
|
3813
|
+
path: inv.path,
|
|
3814
|
+
operator: "type",
|
|
3815
|
+
expected: inv.type,
|
|
3816
|
+
actual: actualType,
|
|
3817
|
+
detail: `Expected type "${inv.type}", got "${actualType}"`
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (typeof inv.gte === "number") {
|
|
3822
|
+
if (typeof value !== "number" || value < inv.gte) {
|
|
3823
|
+
failures.push({
|
|
3824
|
+
path: inv.path,
|
|
3825
|
+
operator: "gte",
|
|
3826
|
+
expected: inv.gte,
|
|
3827
|
+
actual: value,
|
|
3828
|
+
detail: `Expected >= ${inv.gte}, got ${JSON.stringify(value)}`
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
if (typeof inv.lte === "number") {
|
|
3833
|
+
if (typeof value !== "number" || value > inv.lte) {
|
|
3834
|
+
failures.push({
|
|
3835
|
+
path: inv.path,
|
|
3836
|
+
operator: "lte",
|
|
3837
|
+
expected: inv.lte,
|
|
3838
|
+
actual: value,
|
|
3839
|
+
detail: `Expected <= ${inv.lte}, got ${JSON.stringify(value)}`
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
return {
|
|
3845
|
+
passed: failures.length === 0,
|
|
3846
|
+
failures
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// src/messageValidation.ts
|
|
3851
|
+
var import_contracts_core4 = require("@replayci/contracts-core");
|
|
3852
|
+
function validateToolResultMessages(messages, contracts, provider) {
|
|
3853
|
+
const failures = [];
|
|
3854
|
+
const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
|
|
3855
|
+
const toolResults = extractToolResults(messages, provider);
|
|
3856
|
+
for (const result of toolResults) {
|
|
3857
|
+
const contract = contractByTool.get(result.toolName);
|
|
3858
|
+
if (!contract) continue;
|
|
3859
|
+
const outputInvariants = contract.assertions.output_invariants;
|
|
3860
|
+
if (outputInvariants.length === 0) continue;
|
|
3861
|
+
let parsed;
|
|
3862
|
+
try {
|
|
3863
|
+
parsed = typeof result.content === "string" ? JSON.parse(result.content) : result.content;
|
|
3864
|
+
} catch {
|
|
3865
|
+
continue;
|
|
3866
|
+
}
|
|
3867
|
+
const invariantResult = (0, import_contracts_core4.evaluateInvariants)(parsed, outputInvariants, process.env);
|
|
3868
|
+
for (const failure of invariantResult) {
|
|
3869
|
+
failures.push({
|
|
3870
|
+
toolName: result.toolName,
|
|
3871
|
+
detail: `Tool result validation failed for "${result.toolName}": ${failure.detail}`
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
return {
|
|
3876
|
+
passed: failures.length === 0,
|
|
3877
|
+
failures
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
function extractToolResults(messages, provider) {
|
|
3881
|
+
const results = [];
|
|
3882
|
+
if (provider === "openai") {
|
|
3883
|
+
results.push(...extractOpenAIToolResults(messages));
|
|
3884
|
+
} else {
|
|
3885
|
+
results.push(...extractAnthropicToolResults(messages));
|
|
3886
|
+
}
|
|
3887
|
+
return results;
|
|
3888
|
+
}
|
|
3889
|
+
function extractOpenAIToolResults(messages) {
|
|
3890
|
+
const results = [];
|
|
3891
|
+
const toolCallIdToName = /* @__PURE__ */ new Map();
|
|
3892
|
+
for (const msg of messages) {
|
|
3893
|
+
const rec = toRecord9(msg);
|
|
3894
|
+
if (rec.role !== "assistant") continue;
|
|
3895
|
+
const toolCalls = rec.tool_calls;
|
|
3896
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
3897
|
+
for (const tc of toolCalls) {
|
|
3898
|
+
const tcRec = toRecord9(tc);
|
|
3899
|
+
const id = typeof tcRec.id === "string" ? tcRec.id : null;
|
|
3900
|
+
const fn = toRecord9(tcRec.function);
|
|
3901
|
+
const name = typeof fn.name === "string" ? fn.name : typeof tcRec.name === "string" ? tcRec.name : null;
|
|
3902
|
+
if (id && name) {
|
|
3903
|
+
toolCallIdToName.set(id, name);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
for (const msg of messages) {
|
|
3908
|
+
const rec = toRecord9(msg);
|
|
3909
|
+
if (rec.role !== "tool") continue;
|
|
3910
|
+
const toolCallId = typeof rec.tool_call_id === "string" ? rec.tool_call_id : null;
|
|
3911
|
+
const toolName = toolCallId ? toolCallIdToName.get(toolCallId) ?? "unknown" : "unknown";
|
|
3912
|
+
results.push({
|
|
3913
|
+
toolName,
|
|
3914
|
+
toolCallId,
|
|
3915
|
+
content: rec.content
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
return results;
|
|
3919
|
+
}
|
|
3920
|
+
function extractAnthropicToolResults(messages) {
|
|
3921
|
+
const results = [];
|
|
3922
|
+
const toolUseIdToName = /* @__PURE__ */ new Map();
|
|
3923
|
+
for (const msg of messages) {
|
|
3924
|
+
const rec = toRecord9(msg);
|
|
3925
|
+
if (rec.role !== "assistant") continue;
|
|
3926
|
+
const content = rec.content;
|
|
3927
|
+
if (!Array.isArray(content)) continue;
|
|
3928
|
+
for (const block of content) {
|
|
3929
|
+
const blockRec = toRecord9(block);
|
|
3930
|
+
if (blockRec.type === "tool_use") {
|
|
3931
|
+
const id = typeof blockRec.id === "string" ? blockRec.id : null;
|
|
3932
|
+
const name = typeof blockRec.name === "string" ? blockRec.name : null;
|
|
3933
|
+
if (id && name) {
|
|
3934
|
+
toolUseIdToName.set(id, name);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
for (const msg of messages) {
|
|
3940
|
+
const rec = toRecord9(msg);
|
|
3941
|
+
if (rec.role !== "user") continue;
|
|
3942
|
+
const content = rec.content;
|
|
3943
|
+
if (!Array.isArray(content)) continue;
|
|
3944
|
+
for (const block of content) {
|
|
3945
|
+
const blockRec = toRecord9(block);
|
|
3946
|
+
if (blockRec.type === "tool_result") {
|
|
3947
|
+
const toolUseId = typeof blockRec.tool_use_id === "string" ? blockRec.tool_use_id : null;
|
|
3948
|
+
const toolName = toolUseId ? toolUseIdToName.get(toolUseId) ?? "unknown" : "unknown";
|
|
3949
|
+
results.push({
|
|
3950
|
+
toolName,
|
|
3951
|
+
toolCallId: toolUseId,
|
|
3952
|
+
content: blockRec.content
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return results;
|
|
3958
|
+
}
|
|
3959
|
+
function toRecord9(value) {
|
|
3960
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
// src/policy.ts
|
|
3964
|
+
function evaluatePolicy(toolName, principal, _arguments, _sessionState, policyProgram) {
|
|
3965
|
+
for (const rule of policyProgram.sessionRules) {
|
|
3966
|
+
if (!rule.deny) continue;
|
|
3967
|
+
if (rule.allow) continue;
|
|
3968
|
+
const denyPredicate = rule.deny.principal;
|
|
3969
|
+
if (evaluatePredicate(denyPredicate, principal)) {
|
|
3970
|
+
return {
|
|
3971
|
+
allowed: false,
|
|
3972
|
+
reason: `Session deny rule matched: ${denyPredicate.path}`
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
const toolPolicy = policyProgram.perToolRules.get(toolName);
|
|
3977
|
+
if (toolPolicy?.deny) {
|
|
3978
|
+
for (const rule of toolPolicy.deny) {
|
|
3979
|
+
if (evaluatePredicate(rule.principal, principal)) {
|
|
3980
|
+
return {
|
|
3981
|
+
allowed: false,
|
|
3982
|
+
reason: `Per-tool deny rule matched: ${rule.principal.path}`
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
if (policyProgram.defaultDeny) {
|
|
3988
|
+
const sessionAllow = policyProgram.sessionRules.some((r) => {
|
|
3989
|
+
if (!r.allow) return false;
|
|
3990
|
+
if (r.allow.tools && !r.allow.tools.includes(toolName)) return false;
|
|
3991
|
+
return evaluatePredicate(r.allow.principal, principal);
|
|
3992
|
+
});
|
|
3993
|
+
const toolAllow = toolPolicy?.allow?.some(
|
|
3994
|
+
(rule) => evaluatePredicate(rule.principal, principal)
|
|
3995
|
+
) ?? false;
|
|
3996
|
+
if (!sessionAllow && !toolAllow) {
|
|
3997
|
+
return {
|
|
3998
|
+
allowed: false,
|
|
3999
|
+
reason: "default_deny: no matching allow rule"
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return { allowed: true, reason: null };
|
|
4004
|
+
}
|
|
4005
|
+
function evaluatePredicate(predicate, principal) {
|
|
4006
|
+
const value = extractPath2(principal, predicate.path);
|
|
4007
|
+
if (predicate.equals !== void 0) {
|
|
4008
|
+
return JSON.stringify(value) === JSON.stringify(predicate.equals);
|
|
4009
|
+
}
|
|
4010
|
+
if (predicate.one_of !== void 0) {
|
|
4011
|
+
return predicate.one_of.some(
|
|
4012
|
+
(candidate) => JSON.stringify(candidate) === JSON.stringify(value)
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
if (predicate.regex !== void 0) {
|
|
4016
|
+
const strValue = typeof value === "string" ? value : String(value);
|
|
4017
|
+
try {
|
|
4018
|
+
const re = safeRegex(predicate.regex);
|
|
4019
|
+
return re.test(strValue);
|
|
4020
|
+
} catch {
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (predicate.contains !== void 0) {
|
|
4025
|
+
const strValue = typeof value === "string" ? value : String(value);
|
|
4026
|
+
return strValue.includes(predicate.contains);
|
|
4027
|
+
}
|
|
4028
|
+
return false;
|
|
4029
|
+
}
|
|
4030
|
+
function extractPath2(obj, path) {
|
|
4031
|
+
const cleanPath = path.startsWith("$.") ? path.slice(2) : path;
|
|
4032
|
+
if (cleanPath === "" || cleanPath === "$") return obj;
|
|
4033
|
+
const segments = cleanPath.split(".");
|
|
4034
|
+
let current = obj;
|
|
4035
|
+
for (const seg of segments) {
|
|
4036
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
4037
|
+
return void 0;
|
|
4038
|
+
}
|
|
4039
|
+
current = current[seg];
|
|
4040
|
+
}
|
|
4041
|
+
return current;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
// src/narrow.ts
|
|
4045
|
+
function narrowTools(requestedTools, sessionState, compiledSession, unmatchedPolicy, manualFilter) {
|
|
4046
|
+
const allowed = [];
|
|
4047
|
+
const removed = [];
|
|
4048
|
+
for (const tool of requestedTools) {
|
|
4049
|
+
if (manualFilter && !manualFilter.includes(tool.name)) {
|
|
4050
|
+
removed.push({ tool: tool.name, reason: "manual_filter" });
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
const contract = compiledSession.perToolContracts.get(tool.name);
|
|
4054
|
+
if (!contract) {
|
|
4055
|
+
if (unmatchedPolicy === "allow") {
|
|
4056
|
+
allowed.push(tool);
|
|
4057
|
+
} else {
|
|
4058
|
+
removed.push({ tool: tool.name, reason: "no_contract" });
|
|
4059
|
+
}
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
if (sessionState.currentPhase && contract.transitions?.valid_in_phases) {
|
|
4063
|
+
if (!contract.transitions.valid_in_phases.includes(
|
|
4064
|
+
sessionState.currentPhase
|
|
4065
|
+
)) {
|
|
4066
|
+
removed.push({
|
|
4067
|
+
tool: tool.name,
|
|
4068
|
+
reason: "wrong_phase",
|
|
4069
|
+
detail: `Tool valid in [${contract.transitions.valid_in_phases.join(", ")}], current phase: ${sessionState.currentPhase}`
|
|
4070
|
+
});
|
|
4071
|
+
continue;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
if (contract.preconditions && contract.preconditions.length > 0) {
|
|
4075
|
+
const results = evaluatePreconditions(
|
|
4076
|
+
contract.preconditions,
|
|
4077
|
+
sessionState
|
|
4078
|
+
);
|
|
4079
|
+
const unsatisfied = results.find((r) => !r.satisfied);
|
|
4080
|
+
if (unsatisfied) {
|
|
4081
|
+
removed.push({
|
|
4082
|
+
tool: tool.name,
|
|
4083
|
+
reason: "precondition_not_met",
|
|
4084
|
+
detail: unsatisfied.detail
|
|
4085
|
+
});
|
|
4086
|
+
continue;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
if (sessionState.forbiddenTools.has(tool.name)) {
|
|
4090
|
+
removed.push({
|
|
4091
|
+
tool: tool.name,
|
|
4092
|
+
reason: "forbidden_in_state"
|
|
4093
|
+
});
|
|
4094
|
+
continue;
|
|
4095
|
+
}
|
|
4096
|
+
if (compiledSession.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
|
|
4097
|
+
const verdict = evaluatePolicy(
|
|
4098
|
+
tool.name,
|
|
4099
|
+
compiledSession.principal,
|
|
4100
|
+
{},
|
|
4101
|
+
sessionState,
|
|
4102
|
+
compiledSession.policyProgram
|
|
4103
|
+
);
|
|
4104
|
+
if (!verdict.allowed) {
|
|
4105
|
+
removed.push({
|
|
4106
|
+
tool: tool.name,
|
|
4107
|
+
reason: "policy_denied",
|
|
4108
|
+
detail: verdict.reason ?? "Policy deny rule matched"
|
|
4109
|
+
});
|
|
4110
|
+
continue;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
allowed.push(tool);
|
|
4114
|
+
}
|
|
4115
|
+
return { allowed, removed };
|
|
4116
|
+
}
|
|
4117
|
+
function extractToolDefinitions(tools) {
|
|
4118
|
+
const result = [];
|
|
4119
|
+
for (const tool of tools) {
|
|
4120
|
+
if (!tool || typeof tool !== "object") continue;
|
|
4121
|
+
const record = tool;
|
|
4122
|
+
const name = getToolName(record);
|
|
4123
|
+
if (name) {
|
|
4124
|
+
result.push({ ...record, name });
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
return result;
|
|
4128
|
+
}
|
|
4129
|
+
function getToolName(tool) {
|
|
4130
|
+
if (typeof tool.name === "string" && tool.name.length > 0) return tool.name;
|
|
4131
|
+
const fn = tool.function;
|
|
4132
|
+
if (fn && typeof fn === "object" && typeof fn.name === "string") {
|
|
4133
|
+
return fn.name;
|
|
4134
|
+
}
|
|
4135
|
+
return void 0;
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
// src/executionConstraints.ts
|
|
4139
|
+
var import_contracts_core5 = require("@replayci/contracts-core");
|
|
4140
|
+
function enforceExecutionConstraints(toolName, args, constraints) {
|
|
4141
|
+
if (constraints.length === 0) {
|
|
4142
|
+
return { passed: true, failures: [] };
|
|
4143
|
+
}
|
|
4144
|
+
const failures = (0, import_contracts_core5.evaluateInvariants)(args, constraints, process.env);
|
|
4145
|
+
const constraintFailures = failures.map((f) => ({
|
|
4146
|
+
path: f.path,
|
|
4147
|
+
operator: f.rule,
|
|
4148
|
+
expected: f.detail,
|
|
4149
|
+
actual: f.detail
|
|
4150
|
+
}));
|
|
4151
|
+
return {
|
|
4152
|
+
passed: constraintFailures.length === 0,
|
|
4153
|
+
failures: constraintFailures
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
function createWrappedToolExecutor(toolName, executor, compiledSession) {
|
|
4157
|
+
const constraints = compiledSession.executionConstraints.get(toolName);
|
|
4158
|
+
return async (args) => {
|
|
4159
|
+
if (constraints && constraints.length > 0) {
|
|
4160
|
+
const verdict = enforceExecutionConstraints(toolName, args, constraints);
|
|
4161
|
+
if (!verdict.passed) {
|
|
4162
|
+
return { result: void 0, constraint_verdict: verdict };
|
|
4163
|
+
}
|
|
4164
|
+
const result2 = await executor(args);
|
|
4165
|
+
return { result: result2, constraint_verdict: verdict };
|
|
4166
|
+
}
|
|
4167
|
+
const result = await executor(args);
|
|
4168
|
+
return {
|
|
4169
|
+
result,
|
|
4170
|
+
constraint_verdict: { passed: true, failures: [] }
|
|
4171
|
+
};
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
function buildWrappedToolsMap(tools, compiledSession) {
|
|
4175
|
+
if (!tools) return {};
|
|
4176
|
+
if (!compiledSession) {
|
|
4177
|
+
return Object.fromEntries(
|
|
4178
|
+
Object.entries(tools).map(([name, executor]) => [
|
|
4179
|
+
name,
|
|
4180
|
+
async (args) => {
|
|
4181
|
+
const result = await executor(args);
|
|
4182
|
+
return { result, constraint_verdict: { passed: true, failures: [] } };
|
|
4183
|
+
}
|
|
4184
|
+
])
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
return Object.fromEntries(
|
|
4188
|
+
Object.entries(tools).map(([name, executor]) => [
|
|
4189
|
+
name,
|
|
4190
|
+
createWrappedToolExecutor(name, executor, compiledSession)
|
|
4191
|
+
])
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
// src/runtimeClient.ts
|
|
4196
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
4197
|
+
var CIRCUIT_BREAKER_FAILURE_LIMIT2 = 5;
|
|
4198
|
+
var CIRCUIT_BREAKER_MS2 = 10 * 6e4;
|
|
4199
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
4200
|
+
var DEFAULT_RUNTIME_URL = "https://app.replayci.com";
|
|
4201
|
+
var RuntimeClientError = class extends Error {
|
|
4202
|
+
code;
|
|
4203
|
+
httpStatus;
|
|
4204
|
+
constructor(code, message, httpStatus) {
|
|
4205
|
+
super(message);
|
|
4206
|
+
this.name = "RuntimeClientError";
|
|
4207
|
+
this.code = code;
|
|
4208
|
+
this.httpStatus = httpStatus;
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
function createRuntimeClient(opts) {
|
|
4212
|
+
return new RuntimeClient(opts);
|
|
4213
|
+
}
|
|
4214
|
+
var RuntimeClient = class {
|
|
4215
|
+
apiKey;
|
|
4216
|
+
baseUrl;
|
|
4217
|
+
timeoutMs;
|
|
4218
|
+
fetchImpl;
|
|
4219
|
+
now;
|
|
4220
|
+
failureCount = 0;
|
|
4221
|
+
circuitOpenUntil = 0;
|
|
4222
|
+
constructor(opts) {
|
|
4223
|
+
this.apiKey = opts.apiKey;
|
|
4224
|
+
this.baseUrl = normalizeUrl(opts.apiUrl ?? DEFAULT_RUNTIME_URL);
|
|
4225
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4226
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4227
|
+
this.now = opts.now ?? (() => Date.now());
|
|
4228
|
+
}
|
|
4229
|
+
// -------------------------------------------------------------------------
|
|
4230
|
+
// Public API
|
|
4231
|
+
// -------------------------------------------------------------------------
|
|
4232
|
+
async createSession(input) {
|
|
4233
|
+
const body = {
|
|
4234
|
+
agent: input.agent,
|
|
4235
|
+
requested_mode: input.requestedMode,
|
|
4236
|
+
requested_tier: input.requestedTier,
|
|
4237
|
+
adapter_capability: input.adapterCapability,
|
|
4238
|
+
contract_hash: input.contractHash
|
|
4239
|
+
};
|
|
4240
|
+
if (input.sessionId) body.session_id = input.sessionId;
|
|
4241
|
+
if (input.allowAdvisoryDowngrade) body.allow_advisory_downgrade = true;
|
|
4242
|
+
if (input.provider) body.provider = input.provider;
|
|
4243
|
+
if (input.modelId !== void 0) body.model_id = input.modelId;
|
|
4244
|
+
if (input.principal !== void 0) body.principal = input.principal;
|
|
4245
|
+
if (input.compiledSession) body.compiled_session = input.compiledSession;
|
|
4246
|
+
if (input.sessionContractHash !== void 0) body.session_contract_hash = input.sessionContractHash;
|
|
4247
|
+
if (input.workflow) {
|
|
4248
|
+
const wf = {
|
|
4249
|
+
workflow_id: input.workflow.workflowId,
|
|
4250
|
+
role: input.workflow.role
|
|
4251
|
+
};
|
|
4252
|
+
if (input.workflow.compiledWorkflow) {
|
|
4253
|
+
wf.compiled_workflow = {
|
|
4254
|
+
hash: input.workflow.compiledWorkflow.hash,
|
|
4255
|
+
body: input.workflow.compiledWorkflow.body
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
if (input.workflow.parentSessionId) wf.parent_session_id = input.workflow.parentSessionId;
|
|
4259
|
+
if (input.workflow.handoffId) wf.handoff_id = input.workflow.handoffId;
|
|
4260
|
+
body.workflow = wf;
|
|
4261
|
+
}
|
|
4262
|
+
const data = await this.post("/api/v1/replay/sessions", body);
|
|
4263
|
+
const s = data.session;
|
|
4264
|
+
const result = {
|
|
4265
|
+
sessionId: s.session_id,
|
|
4266
|
+
mode: s.mode,
|
|
4267
|
+
tier: s.tier,
|
|
4268
|
+
status: s.status,
|
|
4269
|
+
stateVersion: s.state_version,
|
|
4270
|
+
controlRevision: s.control_revision,
|
|
4271
|
+
leaseFence: s.lease_fence ?? null
|
|
4272
|
+
};
|
|
4273
|
+
const wfResp = data.workflow;
|
|
4274
|
+
if (wfResp) {
|
|
4275
|
+
result.workflow = {
|
|
4276
|
+
workflowId: wfResp.workflow_id,
|
|
4277
|
+
role: wfResp.role,
|
|
4278
|
+
stateVersion: wfResp.state_version,
|
|
4279
|
+
controlRevision: wfResp.control_revision,
|
|
4280
|
+
linkId: wfResp.link_id,
|
|
4281
|
+
generation: wfResp.generation,
|
|
4282
|
+
depth: wfResp.depth,
|
|
4283
|
+
status: wfResp.status
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4286
|
+
return result;
|
|
4287
|
+
}
|
|
4288
|
+
async preflight(input) {
|
|
4289
|
+
const body = {
|
|
4290
|
+
lease_fence: input.leaseFence,
|
|
4291
|
+
provider: input.provider,
|
|
4292
|
+
model_id: input.modelId,
|
|
4293
|
+
request_envelope: input.requestEnvelope
|
|
4294
|
+
};
|
|
4295
|
+
const data = await this.post(
|
|
4296
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/preflight`,
|
|
4297
|
+
body
|
|
4298
|
+
);
|
|
4299
|
+
const pr = data.prepared_request;
|
|
4300
|
+
return {
|
|
4301
|
+
preparedRequestId: pr.prepared_request_id,
|
|
4302
|
+
stateVersion: pr.state_version,
|
|
4303
|
+
controlRevision: pr.control_revision,
|
|
4304
|
+
leaseFence: pr.lease_fence,
|
|
4305
|
+
requestEnvelope: pr.request_envelope,
|
|
4306
|
+
removedTools: pr.removed_tools ?? []
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
async submitProposal(input) {
|
|
4310
|
+
const body = {
|
|
4311
|
+
lease_fence: input.leaseFence,
|
|
4312
|
+
prepared_request_id: input.preparedRequestId,
|
|
4313
|
+
response_envelope: input.responseEnvelope
|
|
4314
|
+
};
|
|
4315
|
+
const data = await this.post(
|
|
4316
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/proposals`,
|
|
4317
|
+
body
|
|
4318
|
+
);
|
|
4319
|
+
const d = data.decision;
|
|
4320
|
+
const pending = d.pending_calls ?? [];
|
|
4321
|
+
const blocked = d.blocked_calls ?? [];
|
|
4322
|
+
return {
|
|
4323
|
+
decision: d.result,
|
|
4324
|
+
advisory: d.mode === "advisory",
|
|
4325
|
+
stateVersion: d.state_version,
|
|
4326
|
+
pendingCalls: pending.map((pc) => ({
|
|
4327
|
+
pendingCallId: pc.pending_call_id,
|
|
4328
|
+
toolCallId: pc.tool_call_id,
|
|
4329
|
+
toolName: pc.tool_name,
|
|
4330
|
+
argumentsHash: pc.arguments_hash
|
|
4331
|
+
})),
|
|
4332
|
+
blockedCalls: blocked.map((bc) => ({
|
|
4333
|
+
toolName: bc.tool_name,
|
|
4334
|
+
reason: bc.reason
|
|
4335
|
+
}))
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
async submitReceipt(input) {
|
|
4339
|
+
const body = {
|
|
4340
|
+
lease_fence: input.leaseFence,
|
|
4341
|
+
pending_call_id: input.pendingCallId,
|
|
4342
|
+
executor_kind: input.executorKind,
|
|
4343
|
+
tool_name: input.toolName,
|
|
4344
|
+
arguments_hash: input.argumentsHash,
|
|
4345
|
+
status: input.status,
|
|
4346
|
+
started_at: input.startedAt,
|
|
4347
|
+
completed_at: input.completedAt
|
|
4348
|
+
};
|
|
4349
|
+
if (input.executorId) body.executor_id = input.executorId;
|
|
4350
|
+
if (input.outputHash) body.output_hash = input.outputHash;
|
|
4351
|
+
if (input.outputExtract) body.output_extract = input.outputExtract;
|
|
4352
|
+
if (input.resourceValues) body.resource_values = input.resourceValues;
|
|
4353
|
+
if (input.evidenceArtifactHash) body.evidence_artifact_hash = input.evidenceArtifactHash;
|
|
4354
|
+
const data = await this.post(
|
|
4355
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/receipts`,
|
|
4356
|
+
body
|
|
4357
|
+
);
|
|
4358
|
+
const r = data.resolution;
|
|
4359
|
+
return {
|
|
4360
|
+
accepted: r.accepted,
|
|
4361
|
+
commitState: r.commit_state,
|
|
4362
|
+
stateAdvanced: r.state_advanced,
|
|
4363
|
+
stateVersion: r.state_version
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
async setToolFilter(input) {
|
|
4367
|
+
const body = {
|
|
4368
|
+
lease_fence: input.leaseFence,
|
|
4369
|
+
allowed_tools: input.allowedTools
|
|
4370
|
+
};
|
|
4371
|
+
const data = await this.post(
|
|
4372
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/tool-filter`,
|
|
4373
|
+
body
|
|
4374
|
+
);
|
|
4375
|
+
const s = data.session;
|
|
4376
|
+
return {
|
|
4377
|
+
controlRevision: s.control_revision
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
async reportBypass(input) {
|
|
4381
|
+
const body = {
|
|
4382
|
+
source: input.source
|
|
4383
|
+
};
|
|
4384
|
+
if (input.detail) body.detail = input.detail;
|
|
4385
|
+
const data = await this.post(
|
|
4386
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/report-bypass`,
|
|
4387
|
+
body
|
|
4388
|
+
);
|
|
4389
|
+
const s = data.session;
|
|
4390
|
+
return {
|
|
4391
|
+
status: s.status
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
4394
|
+
async killSession(input) {
|
|
4395
|
+
const body = {
|
|
4396
|
+
lease_fence: input.leaseFence,
|
|
4397
|
+
reason: input.reason
|
|
4398
|
+
};
|
|
4399
|
+
const data = await this.post(
|
|
4400
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/kill`,
|
|
4401
|
+
body
|
|
4402
|
+
);
|
|
4403
|
+
const s = data.session;
|
|
4404
|
+
return {
|
|
4405
|
+
status: s.status
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4408
|
+
/** v4: Get workflow state from the runtime. */
|
|
4409
|
+
async getWorkflowState(workflowId) {
|
|
4410
|
+
const data = await this.get(
|
|
4411
|
+
`/api/v1/replay/workflows/${encodeURIComponent(workflowId)}`
|
|
4412
|
+
);
|
|
4413
|
+
const w = data.workflow;
|
|
4414
|
+
return {
|
|
4415
|
+
workflowId: w.workflow_id,
|
|
4416
|
+
rootSessionId: w.root_session_id,
|
|
4417
|
+
status: w.status,
|
|
4418
|
+
stateVersion: w.state_version,
|
|
4419
|
+
controlRevision: w.control_revision,
|
|
4420
|
+
totalSessionCount: w.total_session_count,
|
|
4421
|
+
activeSessionCount: w.active_session_count,
|
|
4422
|
+
totalStepCount: w.total_step_count,
|
|
4423
|
+
totalCost: w.total_cost,
|
|
4424
|
+
totalHandoffCount: w.total_handoff_count,
|
|
4425
|
+
unresolvedHandoffCount: w.unresolved_handoff_count,
|
|
4426
|
+
lastEventSeq: w.last_event_seq,
|
|
4427
|
+
killScope: w.kill_scope,
|
|
4428
|
+
createdAt: w.created_at,
|
|
4429
|
+
updatedAt: w.updated_at
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
/** v4: Offer a handoff from a session. */
|
|
4433
|
+
async offerHandoff(input) {
|
|
4434
|
+
const body = {
|
|
4435
|
+
workflow_id: input.workflowId,
|
|
4436
|
+
from_role: input.fromRole,
|
|
4437
|
+
to_role: input.toRole,
|
|
4438
|
+
handoff_id: input.handoffId
|
|
4439
|
+
};
|
|
4440
|
+
if (input.artifactRefs !== void 0) body.artifact_refs = input.artifactRefs;
|
|
4441
|
+
if (input.summary !== void 0) body.summary = input.summary;
|
|
4442
|
+
const data = await this.post(
|
|
4443
|
+
`/api/v1/replay/sessions/${encodeURIComponent(input.sessionId)}/handoffs`,
|
|
4444
|
+
body
|
|
4445
|
+
);
|
|
4446
|
+
const h = data.handoff;
|
|
4447
|
+
return {
|
|
4448
|
+
handoffId: h.handoff_id,
|
|
4449
|
+
eventSeq: h.event_seq,
|
|
4450
|
+
stateVersion: h.state_version
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
getHealth() {
|
|
4454
|
+
return {
|
|
4455
|
+
circuitOpen: this.now() < this.circuitOpenUntil,
|
|
4456
|
+
failureCount: this.failureCount,
|
|
4457
|
+
circuitOpenUntil: this.circuitOpenUntil
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
isCircuitOpen() {
|
|
4461
|
+
return this.now() < this.circuitOpenUntil;
|
|
4462
|
+
}
|
|
4463
|
+
// -------------------------------------------------------------------------
|
|
4464
|
+
// Internal
|
|
4465
|
+
// -------------------------------------------------------------------------
|
|
4466
|
+
async get(path) {
|
|
4467
|
+
if (this.isCircuitOpen()) {
|
|
4468
|
+
throw new RuntimeClientError(
|
|
4469
|
+
"CIRCUIT_OPEN",
|
|
4470
|
+
"Runtime client circuit breaker is open",
|
|
4471
|
+
503
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
const url = `${this.baseUrl}${path}`;
|
|
4475
|
+
const controller = new AbortController();
|
|
4476
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
4477
|
+
try {
|
|
4478
|
+
const response = await this.fetchImpl(url, {
|
|
4479
|
+
method: "GET",
|
|
4480
|
+
headers: {
|
|
4481
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
4482
|
+
},
|
|
4483
|
+
signal: controller.signal
|
|
4484
|
+
});
|
|
4485
|
+
clearTimeout(timeoutId);
|
|
4486
|
+
if (!response.ok) {
|
|
4487
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
4488
|
+
const errorCode = errorBody.error ?? "UNKNOWN";
|
|
4489
|
+
const errorMessage = errorBody.message ?? `HTTP ${response.status}`;
|
|
4490
|
+
if (response.status >= 500 || response.status === 429) {
|
|
4491
|
+
this.recordFailure();
|
|
4492
|
+
}
|
|
4493
|
+
throw new RuntimeClientError(errorCode, errorMessage, response.status);
|
|
4494
|
+
}
|
|
4495
|
+
this.failureCount = 0;
|
|
4496
|
+
const data = await response.json();
|
|
4497
|
+
if (!data.ok) {
|
|
4498
|
+
throw new RuntimeClientError(
|
|
4499
|
+
data.error ?? "UNKNOWN",
|
|
4500
|
+
data.message ?? "Request failed",
|
|
4501
|
+
400
|
|
4502
|
+
);
|
|
4503
|
+
}
|
|
4504
|
+
return data;
|
|
4505
|
+
} catch (err) {
|
|
4506
|
+
clearTimeout(timeoutId);
|
|
4507
|
+
if (err instanceof RuntimeClientError) throw err;
|
|
4508
|
+
this.recordFailure();
|
|
4509
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
4510
|
+
throw new RuntimeClientError("TIMEOUT", "Runtime request timed out", 408);
|
|
4511
|
+
}
|
|
4512
|
+
throw new RuntimeClientError(
|
|
4513
|
+
"NETWORK_ERROR",
|
|
4514
|
+
err instanceof Error ? err.message : "Network error",
|
|
4515
|
+
0
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
async post(path, body) {
|
|
4520
|
+
if (this.isCircuitOpen()) {
|
|
4521
|
+
throw new RuntimeClientError(
|
|
4522
|
+
"CIRCUIT_OPEN",
|
|
4523
|
+
"Runtime client circuit breaker is open",
|
|
4524
|
+
503
|
|
4525
|
+
);
|
|
4526
|
+
}
|
|
4527
|
+
const url = `${this.baseUrl}${path}`;
|
|
4528
|
+
const idempotencyKey = generateIdempotencyKey();
|
|
4529
|
+
const controller = new AbortController();
|
|
4530
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
4531
|
+
try {
|
|
4532
|
+
const response = await this.fetchImpl(url, {
|
|
4533
|
+
method: "POST",
|
|
4534
|
+
headers: {
|
|
4535
|
+
"Content-Type": "application/json",
|
|
4536
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
4537
|
+
"Idempotency-Key": idempotencyKey
|
|
4538
|
+
},
|
|
4539
|
+
body: JSON.stringify(body),
|
|
4540
|
+
signal: controller.signal
|
|
4541
|
+
});
|
|
4542
|
+
clearTimeout(timeoutId);
|
|
4543
|
+
if (!response.ok) {
|
|
4544
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
4545
|
+
const errorCode = errorBody.error ?? "UNKNOWN";
|
|
4546
|
+
const errorMessage = errorBody.message ?? `HTTP ${response.status}`;
|
|
4547
|
+
if (response.status >= 500 || response.status === 429) {
|
|
4548
|
+
this.recordFailure();
|
|
4549
|
+
}
|
|
4550
|
+
throw new RuntimeClientError(errorCode, errorMessage, response.status);
|
|
4551
|
+
}
|
|
4552
|
+
this.failureCount = 0;
|
|
4553
|
+
const data = await response.json();
|
|
4554
|
+
if (!data.ok) {
|
|
4555
|
+
throw new RuntimeClientError(
|
|
4556
|
+
data.error ?? "UNKNOWN",
|
|
4557
|
+
data.message ?? "Request failed",
|
|
4558
|
+
400
|
|
4559
|
+
);
|
|
4560
|
+
}
|
|
4561
|
+
return data;
|
|
4562
|
+
} catch (err) {
|
|
4563
|
+
clearTimeout(timeoutId);
|
|
4564
|
+
if (err instanceof RuntimeClientError) throw err;
|
|
4565
|
+
this.recordFailure();
|
|
4566
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
4567
|
+
throw new RuntimeClientError("TIMEOUT", "Runtime request timed out", 408);
|
|
4568
|
+
}
|
|
4569
|
+
throw new RuntimeClientError(
|
|
4570
|
+
"NETWORK_ERROR",
|
|
4571
|
+
err instanceof Error ? err.message : "Network error",
|
|
4572
|
+
0
|
|
4573
|
+
);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
recordFailure() {
|
|
4577
|
+
this.failureCount++;
|
|
4578
|
+
if (this.failureCount >= CIRCUIT_BREAKER_FAILURE_LIMIT2) {
|
|
4579
|
+
this.circuitOpenUntil = this.now() + CIRCUIT_BREAKER_MS2;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
};
|
|
4583
|
+
function normalizeUrl(url) {
|
|
4584
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
4585
|
+
}
|
|
4586
|
+
function generateIdempotencyKey() {
|
|
4587
|
+
return `sdk_${import_node_crypto4.default.randomUUID().replace(/-/g, "")}`;
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
// src/replay.ts
|
|
4591
|
+
var REPLAY_ATTACHED2 = /* @__PURE__ */ Symbol.for("replayci.replay_attached");
|
|
4592
|
+
var OBSERVE_WRAPPED = /* @__PURE__ */ Symbol.for("replayci.wrapped");
|
|
4593
|
+
var MAX_RETRIES = 5;
|
|
4594
|
+
var DEFAULT_AGENT2 = "default";
|
|
4595
|
+
var DEFAULT_MAX_UNGUARDED_CALLS = 3;
|
|
4596
|
+
function replay(client, opts = {}) {
|
|
4597
|
+
assertSupportedNodeRuntime();
|
|
4598
|
+
const sessionId = opts.sessionId ?? generateSessionId2();
|
|
4599
|
+
const agent = typeof opts.agent === "string" && opts.agent.length > 0 ? opts.agent : DEFAULT_AGENT2;
|
|
4600
|
+
const mode = opts.mode ?? "enforce";
|
|
4601
|
+
const gateMode = opts.gate ?? "reject_all";
|
|
4602
|
+
const onError = opts.onError ?? "block";
|
|
4603
|
+
const unmatchedPolicy = opts.unmatchedPolicy ?? "block";
|
|
4604
|
+
const maxRetries = Math.min(Math.max(0, opts.maxRetries ?? 0), MAX_RETRIES);
|
|
4605
|
+
const compatEnforcement = opts.compatEnforcement ?? "protective";
|
|
4606
|
+
const diagnostics = opts.diagnostics;
|
|
4607
|
+
let provider;
|
|
4608
|
+
try {
|
|
4609
|
+
provider = detectProvider(client);
|
|
4610
|
+
} catch {
|
|
4611
|
+
emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "unsupported_client" });
|
|
4612
|
+
return createInactiveSession(client, sessionId, "Unsupported client");
|
|
4613
|
+
}
|
|
4614
|
+
if (isObserveWrapped(client) || isReplayAttached2(client)) {
|
|
4615
|
+
emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "already_attached" });
|
|
4616
|
+
return createInactiveSession(client, sessionId, "Client already has an active observe() or replay() attachment");
|
|
4617
|
+
}
|
|
4618
|
+
let contracts;
|
|
4619
|
+
try {
|
|
4620
|
+
contracts = resolveContracts(opts);
|
|
4621
|
+
} catch (err) {
|
|
4622
|
+
const detail = err instanceof Error ? err.message : "Failed to load contracts";
|
|
4623
|
+
emitDiagnostic2(diagnostics, { type: "replay_contract_error", details: detail });
|
|
4624
|
+
return createBlockingInactiveSession(client, sessionId, detail);
|
|
4625
|
+
}
|
|
4626
|
+
const configError = validateConfig(contracts, opts);
|
|
4627
|
+
if (configError) {
|
|
4628
|
+
emitDiagnostic2(diagnostics, { type: "replay_contract_error", details: configError.message });
|
|
4629
|
+
return createBlockingInactiveSession(client, sessionId, configError.message, configError);
|
|
4630
|
+
}
|
|
4631
|
+
let discoveredSessionYaml = null;
|
|
4632
|
+
try {
|
|
4633
|
+
discoveredSessionYaml = discoverSessionYaml(opts);
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
const detail = `session.yaml: ${err instanceof Error ? err.message : String(err)}`;
|
|
4636
|
+
emitDiagnostic2(diagnostics, { type: "replay_contract_error", details: detail });
|
|
4637
|
+
return createBlockingInactiveSession(client, sessionId, detail);
|
|
4638
|
+
}
|
|
4639
|
+
let sessionYaml = discoveredSessionYaml;
|
|
4640
|
+
if (!sessionYaml && opts.providerConstraints) {
|
|
4641
|
+
sessionYaml = { schema_version: "1.0", agent, provider_constraints: opts.providerConstraints };
|
|
4642
|
+
} else if (sessionYaml && opts.providerConstraints && !sessionYaml.provider_constraints) {
|
|
4643
|
+
sessionYaml = { ...sessionYaml, provider_constraints: opts.providerConstraints };
|
|
4644
|
+
}
|
|
4645
|
+
let compiledSession = null;
|
|
4646
|
+
try {
|
|
4647
|
+
compiledSession = (0, import_contracts_core6.compileSession)(contracts, sessionYaml, {
|
|
4648
|
+
principal: opts.principal,
|
|
4649
|
+
tools: opts.tools ? new Map(Object.entries(opts.tools)) : void 0
|
|
4650
|
+
});
|
|
4651
|
+
} catch (err) {
|
|
4652
|
+
emitDiagnostic2(diagnostics, {
|
|
4653
|
+
type: "replay_contract_error",
|
|
4654
|
+
details: `Session compilation: ${err instanceof Error ? err.message : String(err)}`
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
if (compiledSession?.warnings && compiledSession.warnings.length > 0) {
|
|
4658
|
+
for (const warning of compiledSession.warnings) {
|
|
4659
|
+
emitDiagnostic2(diagnostics, {
|
|
4660
|
+
type: "replay_contract_error",
|
|
4661
|
+
details: `Compile warning: ${warning}`
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
const providerConstraints = compiledSession?.providerConstraints ?? opts.providerConstraints ?? null;
|
|
4666
|
+
if (providerConstraints) {
|
|
4667
|
+
const spec = providerConstraints[provider];
|
|
4668
|
+
if (spec) {
|
|
4669
|
+
if (spec.block_incompatible && spec.block_incompatible.length > 0) {
|
|
4670
|
+
const detail = `Provider '${provider}' is blocked by provider_constraints: ${spec.block_incompatible.join("; ")}`;
|
|
4671
|
+
const err = new ReplayConfigError("provider_incompatible", detail);
|
|
4672
|
+
emitDiagnostic2(diagnostics, { type: "replay_contract_error", details: detail });
|
|
4673
|
+
return createBlockingInactiveSession(client, sessionId, detail, err);
|
|
4674
|
+
}
|
|
4675
|
+
if (spec.warn_incompatible && spec.warn_incompatible.length > 0) {
|
|
4676
|
+
emitDiagnostic2(diagnostics, {
|
|
4677
|
+
type: "replay_provider_warning",
|
|
4678
|
+
provider,
|
|
4679
|
+
warnings: spec.warn_incompatible
|
|
4680
|
+
});
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
let compiledWorkflow = null;
|
|
4685
|
+
const workflowOpts = opts.workflow;
|
|
4686
|
+
let workflowId = null;
|
|
4687
|
+
if (workflowOpts) {
|
|
4688
|
+
if (workflowOpts.type === "root") {
|
|
4689
|
+
workflowId = workflowOpts.workflowId ?? generateWorkflowId();
|
|
4690
|
+
try {
|
|
4691
|
+
compiledWorkflow = discoverWorkflowYaml(opts, workflowOpts);
|
|
4692
|
+
} catch (err) {
|
|
4693
|
+
const detail = `workflow.yaml: ${err instanceof Error ? err.message : String(err)}`;
|
|
4694
|
+
emitDiagnostic2(diagnostics, { type: "replay_workflow_error", session_id: sessionId, details: detail });
|
|
4695
|
+
return createBlockingInactiveSession(client, sessionId, detail);
|
|
4696
|
+
}
|
|
4697
|
+
} else {
|
|
4698
|
+
workflowId = workflowOpts.workflowId;
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
const terminalInfo = resolveTerminal(client, provider);
|
|
4702
|
+
if (!terminalInfo) {
|
|
4703
|
+
emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "unsupported_client" });
|
|
4704
|
+
return createInactiveSession(client, sessionId, "Could not resolve terminal resource");
|
|
4705
|
+
}
|
|
4706
|
+
const protectionLevel = determineProtectionLevel(mode, opts.tools, contracts);
|
|
4707
|
+
const maxUnguardedCalls = opts.maxUnguardedCalls ?? DEFAULT_MAX_UNGUARDED_CALLS;
|
|
4708
|
+
const narrowingFeedback = opts.narrowingFeedback ?? "silent";
|
|
4709
|
+
const apiKey = resolveApiKey2(opts);
|
|
4710
|
+
let runtimeClient = null;
|
|
4711
|
+
let runtimeSession = null;
|
|
4712
|
+
let runtimeInitPromise = null;
|
|
4713
|
+
let leaseFence = null;
|
|
4714
|
+
let runtimeDegraded = false;
|
|
4715
|
+
let runtimeInitDone = false;
|
|
4716
|
+
if (protectionLevel === "govern" && apiKey) {
|
|
4717
|
+
const runtimeUrl = opts.runtimeUrl;
|
|
4718
|
+
runtimeClient = new RuntimeClient({
|
|
4719
|
+
apiKey,
|
|
4720
|
+
apiUrl: runtimeUrl
|
|
4721
|
+
});
|
|
4722
|
+
const runtimeRequest = deriveRuntimeRequest(protectionLevel, mode);
|
|
4723
|
+
const sessionInitPayload = {
|
|
4724
|
+
agent,
|
|
4725
|
+
sessionId,
|
|
4726
|
+
requestedMode: runtimeRequest.requestedMode,
|
|
4727
|
+
requestedTier: runtimeRequest.requestedTier,
|
|
4728
|
+
adapterCapability: "full",
|
|
4729
|
+
contractHash: compiledSession?.compiledHash ?? "",
|
|
4730
|
+
compiledSession: compiledSession ? {
|
|
4731
|
+
schemaVersion: "1",
|
|
4732
|
+
hash: compiledSession.compiledHash,
|
|
4733
|
+
body: (0, import_contracts_core6.serializeCompiledSession)(compiledSession)
|
|
4734
|
+
} : void 0,
|
|
4735
|
+
principal: opts.principal ?? null
|
|
4736
|
+
};
|
|
4737
|
+
if (workflowOpts && workflowId) {
|
|
4738
|
+
if (workflowOpts.type === "root" && compiledWorkflow) {
|
|
4739
|
+
const serialized = (0, import_contracts_core6.serializeCompiledWorkflow)(compiledWorkflow);
|
|
4740
|
+
sessionInitPayload.workflow = {
|
|
4741
|
+
workflowId,
|
|
4742
|
+
role: workflowOpts.role,
|
|
4743
|
+
compiledWorkflow: {
|
|
4744
|
+
hash: compiledWorkflow.compiledHash,
|
|
4745
|
+
body: serialized
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
} else if (workflowOpts.type === "child") {
|
|
4749
|
+
sessionInitPayload.workflow = {
|
|
4750
|
+
workflowId,
|
|
4751
|
+
role: workflowOpts.role,
|
|
4752
|
+
parentSessionId: workflowOpts.parentSessionId,
|
|
4753
|
+
handoffId: workflowOpts.handoffId
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
runtimeInitPromise = runtimeClient.createSession(sessionInitPayload).then((result) => {
|
|
4758
|
+
runtimeSession = result;
|
|
4759
|
+
leaseFence = result.leaseFence;
|
|
4760
|
+
runtimeInitDone = true;
|
|
4761
|
+
if (result.workflow && workflowOpts) {
|
|
4762
|
+
emitDiagnostic2(diagnostics, {
|
|
4763
|
+
type: "replay_workflow_attached",
|
|
4764
|
+
session_id: sessionId,
|
|
4765
|
+
workflow_id: result.workflow.workflowId,
|
|
4766
|
+
role: result.workflow.role,
|
|
4767
|
+
attach_type: workflowOpts.type
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
}).catch(() => {
|
|
4771
|
+
runtimeInitDone = true;
|
|
4772
|
+
runtimeDegraded = true;
|
|
4773
|
+
emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "runtime_degraded_to_protect" });
|
|
4774
|
+
});
|
|
4775
|
+
} else {
|
|
4776
|
+
runtimeInitDone = true;
|
|
4777
|
+
}
|
|
4778
|
+
const initialTier = protectionLevel === "govern" && apiKey ? "strong" : "compat";
|
|
4779
|
+
const principalValue = opts.principal != null && typeof opts.principal === "object" && !Array.isArray(opts.principal) ? opts.principal : null;
|
|
4780
|
+
let sessionState = createInitialState(sessionId, { tier: initialTier, agent, principal: principalValue });
|
|
4781
|
+
if (compiledSession?.phases) {
|
|
4782
|
+
const initial = compiledSession.phases.find((p) => p.initial);
|
|
4783
|
+
if (initial) {
|
|
4784
|
+
sessionState = { ...sessionState, currentPhase: initial.name };
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
sessionState = { ...sessionState, contractHash: compiledSession?.compiledHash ?? null };
|
|
4788
|
+
let killed = false;
|
|
4789
|
+
let killedAt = null;
|
|
4790
|
+
let restored = false;
|
|
4791
|
+
let bypassDetected = false;
|
|
4792
|
+
let lastShadowDeltaValue = null;
|
|
4793
|
+
let lastNarrowResult = null;
|
|
4794
|
+
let shadowEvaluationCount = 0;
|
|
4795
|
+
let manualFilter = null;
|
|
4796
|
+
const deferredReceipts = /* @__PURE__ */ new Map();
|
|
4797
|
+
const contractLimits = resolveSessionLimits(contracts);
|
|
4798
|
+
const compiledLimits = compiledSession?.sessionLimits;
|
|
4799
|
+
const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
|
|
4800
|
+
const resolvedSessionLimits = Object.keys(mergedLimits).length > 0 ? mergedLimits : null;
|
|
4801
|
+
const store = opts.store ?? null;
|
|
4802
|
+
let storeLoadPromise = null;
|
|
4803
|
+
let storeLoadDone = false;
|
|
4804
|
+
if (store) {
|
|
4805
|
+
storeLoadPromise = Promise.resolve().then(() => store.load()).then((loaded) => {
|
|
4806
|
+
if (loaded && loaded.sessionId === sessionId) {
|
|
4807
|
+
const contractDrift = loaded.contractHash !== null && loaded.contractHash !== (compiledSession?.compiledHash ?? null);
|
|
4808
|
+
sessionState = loaded;
|
|
4809
|
+
if (loaded.killed) {
|
|
4810
|
+
killed = true;
|
|
4811
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4812
|
+
}
|
|
4813
|
+
emitDiagnostic2(diagnostics, {
|
|
4814
|
+
type: "replay_resumed",
|
|
4815
|
+
session_id: sessionId,
|
|
4816
|
+
state_version: loaded.stateVersion,
|
|
4817
|
+
contract_drift: contractDrift
|
|
4818
|
+
});
|
|
4819
|
+
} else {
|
|
4820
|
+
void Promise.resolve(store.compareAndSet(loaded?.stateVersion ?? 0, sessionState));
|
|
4821
|
+
}
|
|
4822
|
+
storeLoadDone = true;
|
|
4823
|
+
}).catch(() => {
|
|
4824
|
+
try {
|
|
4825
|
+
void Promise.resolve(store.compareAndSet(0, sessionState));
|
|
4826
|
+
} catch {
|
|
4827
|
+
}
|
|
4828
|
+
storeLoadDone = true;
|
|
4829
|
+
});
|
|
4830
|
+
} else {
|
|
4831
|
+
storeLoadDone = true;
|
|
4832
|
+
}
|
|
4833
|
+
const buffer = apiKey ? new CaptureBuffer({
|
|
4834
|
+
apiKey,
|
|
4835
|
+
endpoint: void 0,
|
|
4836
|
+
diagnostics
|
|
4837
|
+
}) : null;
|
|
4838
|
+
if (buffer) {
|
|
4839
|
+
registerBeforeExit(buffer);
|
|
4840
|
+
}
|
|
4841
|
+
function syncStateToStore(prevVersion, newState) {
|
|
4842
|
+
if (!store) return;
|
|
4843
|
+
try {
|
|
4844
|
+
const result = store.compareAndSet(prevVersion, newState);
|
|
4845
|
+
if (result && typeof result.then === "function") {
|
|
4846
|
+
void result.catch(() => {
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
} catch {
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
function appendCaptureToStore(capture) {
|
|
4853
|
+
if (!store) return;
|
|
4854
|
+
try {
|
|
4855
|
+
const result = store.appendCapture(capture);
|
|
4856
|
+
if (result && typeof result.then === "function") {
|
|
4857
|
+
void result.catch(() => {
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
} catch {
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
const enforcementCreate = async function replayEnforcementCreate(...args) {
|
|
4864
|
+
if (killed) {
|
|
4865
|
+
throw new ReplayKillError(sessionId, killedAt);
|
|
4866
|
+
}
|
|
4867
|
+
if (restored) {
|
|
4868
|
+
throw new ReplayContractError(
|
|
4869
|
+
"Session has been restored \u2014 wrapper is inert",
|
|
4870
|
+
{ action: "block", tool_calls: [], blocked: [], response_modification: "reject_all" },
|
|
4871
|
+
"",
|
|
4872
|
+
[]
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4875
|
+
if (runtimeInitPromise && !runtimeInitDone) {
|
|
4876
|
+
await runtimeInitPromise;
|
|
4877
|
+
}
|
|
4878
|
+
if (storeLoadPromise && !storeLoadDone) {
|
|
4879
|
+
await storeLoadPromise;
|
|
4880
|
+
}
|
|
4881
|
+
if (protectionLevel === "govern" && runtimeDegraded && sessionState.tier === "strong") {
|
|
4882
|
+
sessionState = { ...sessionState, tier: "compat" };
|
|
4883
|
+
}
|
|
4884
|
+
const effectiveTier = sessionState.tier;
|
|
4885
|
+
const isCompatAdvisory = effectiveTier === "compat" && compatEnforcement === "advisory";
|
|
4886
|
+
if (protectionLevel === "govern" && runtimeDegraded && onError === "block" && !isCompatAdvisory) {
|
|
4887
|
+
throw new ReplayInternalError("Govern mode requires runtime \u2014 runtime unavailable", { sessionId });
|
|
4888
|
+
}
|
|
4889
|
+
const guardStart = Date.now();
|
|
4890
|
+
const timing = {
|
|
4891
|
+
narrow_ms: 0,
|
|
4892
|
+
pre_check_ms: 0,
|
|
4893
|
+
llm_call_ms: 0,
|
|
4894
|
+
validate_ms: 0,
|
|
4895
|
+
cross_step_ms: 0,
|
|
4896
|
+
phase_ms: 0,
|
|
4897
|
+
argument_values_ms: 0,
|
|
4898
|
+
policy_ms: 0,
|
|
4899
|
+
gate_ms: 0,
|
|
4900
|
+
finalize_ms: 0,
|
|
4901
|
+
runtime_ms: 0,
|
|
4902
|
+
total_ms: 0,
|
|
4903
|
+
enforcement_ms: 0
|
|
4904
|
+
};
|
|
4905
|
+
const request = toRecord10(args[0]);
|
|
4906
|
+
const requestToolNames = extractRequestToolNames(request);
|
|
4907
|
+
let narrowResult = null;
|
|
4908
|
+
let activeArgs = args;
|
|
4909
|
+
if (compiledSession && Array.isArray(request.tools) && request.tools.length > 0) {
|
|
4910
|
+
const toolDefs = extractToolDefinitions(request.tools);
|
|
4911
|
+
if (toolDefs.length > 0) {
|
|
4912
|
+
narrowResult = narrowTools(
|
|
4913
|
+
toolDefs,
|
|
4914
|
+
sessionState,
|
|
4915
|
+
compiledSession,
|
|
4916
|
+
unmatchedPolicy,
|
|
4917
|
+
manualFilter
|
|
4918
|
+
);
|
|
4919
|
+
lastNarrowResult = narrowResult;
|
|
4920
|
+
if (narrowResult.removed.length > 0) {
|
|
4921
|
+
if (mode === "enforce") {
|
|
4922
|
+
const modifiedRequest = { ...request };
|
|
4923
|
+
if (narrowResult.allowed.length === 0) {
|
|
4924
|
+
modifiedRequest.tools = [];
|
|
4925
|
+
delete modifiedRequest.tool_choice;
|
|
4926
|
+
} else {
|
|
4927
|
+
modifiedRequest.tools = narrowResult.allowed;
|
|
4928
|
+
}
|
|
4929
|
+
if (narrowingFeedback === "inject") {
|
|
4930
|
+
const injectionMsg = buildNarrowingInjectionMessage(narrowResult);
|
|
4931
|
+
injectNarrowingSystemMessage(modifiedRequest, injectionMsg, provider);
|
|
4932
|
+
emitDiagnostic2(diagnostics, {
|
|
4933
|
+
type: "replay_narrow_injected",
|
|
4934
|
+
session_id: sessionId,
|
|
4935
|
+
message: injectionMsg
|
|
4936
|
+
});
|
|
4937
|
+
}
|
|
4938
|
+
activeArgs = [modifiedRequest, ...Array.prototype.slice.call(args, 1)];
|
|
4939
|
+
}
|
|
4940
|
+
emitDiagnostic2(diagnostics, {
|
|
4941
|
+
type: "replay_narrow",
|
|
4942
|
+
session_id: sessionId,
|
|
4943
|
+
removed: narrowResult.removed
|
|
4944
|
+
});
|
|
4945
|
+
try {
|
|
4946
|
+
opts.onNarrow?.(narrowResult);
|
|
4947
|
+
} catch {
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
timing.narrow_ms = Date.now() - guardStart;
|
|
4953
|
+
const preCheckStart = Date.now();
|
|
4954
|
+
try {
|
|
4955
|
+
if (mode === "enforce" && resolvedSessionLimits) {
|
|
4956
|
+
const limitResult = checkSessionLimits(sessionState, resolvedSessionLimits);
|
|
4957
|
+
if (limitResult.exceeded) {
|
|
4958
|
+
const decision = {
|
|
4959
|
+
action: "block",
|
|
4960
|
+
tool_calls: [],
|
|
4961
|
+
blocked: [{
|
|
4962
|
+
tool_name: "_session",
|
|
4963
|
+
arguments: "",
|
|
4964
|
+
reason: "session_limit_exceeded",
|
|
4965
|
+
contract_file: "",
|
|
4966
|
+
failures: [{ path: "$", operator: "session_limit", expected: "", found: "", message: limitResult.reason ?? "session limit exceeded" }]
|
|
4967
|
+
}],
|
|
4968
|
+
response_modification: gateMode
|
|
4969
|
+
};
|
|
4970
|
+
sessionState = recordDecisionOutcome(sessionState, "blocked");
|
|
4971
|
+
if (resolvedSessionLimits.circuit_breaker) {
|
|
4972
|
+
const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
|
|
4973
|
+
if (cbResult.triggered) {
|
|
4974
|
+
killed = true;
|
|
4975
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4976
|
+
sessionState = killSession(sessionState);
|
|
4977
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
timing.pre_check_ms = Date.now() - preCheckStart;
|
|
4981
|
+
captureDecision(
|
|
4982
|
+
decision,
|
|
4983
|
+
null,
|
|
4984
|
+
request,
|
|
4985
|
+
guardStart,
|
|
4986
|
+
requestToolNames,
|
|
4987
|
+
null,
|
|
4988
|
+
narrowResult,
|
|
4989
|
+
null,
|
|
4990
|
+
null,
|
|
4991
|
+
null,
|
|
4992
|
+
void 0,
|
|
4993
|
+
timing
|
|
4994
|
+
);
|
|
4995
|
+
if (isCompatAdvisory) {
|
|
4996
|
+
emitDiagnostic2(diagnostics, {
|
|
4997
|
+
type: "replay_compat_advisory",
|
|
4998
|
+
session_id: sessionId,
|
|
4999
|
+
would_block: decision.blocked,
|
|
5000
|
+
details: limitResult.reason ?? "session limit exceeded"
|
|
5001
|
+
});
|
|
5002
|
+
} else {
|
|
5003
|
+
throw buildContractError2(decision);
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
if (isAtHardStepCap(sessionState)) {
|
|
5007
|
+
const decision = {
|
|
5008
|
+
action: "block",
|
|
5009
|
+
tool_calls: [],
|
|
5010
|
+
blocked: [{
|
|
5011
|
+
tool_name: "_session",
|
|
5012
|
+
arguments: "",
|
|
5013
|
+
reason: "session_limit_exceeded",
|
|
5014
|
+
contract_file: "",
|
|
5015
|
+
failures: [{ path: "$", operator: "session_limit", expected: "", found: "", message: "hard step cap (10,000) reached" }]
|
|
5016
|
+
}],
|
|
5017
|
+
response_modification: gateMode
|
|
5018
|
+
};
|
|
5019
|
+
timing.pre_check_ms = Date.now() - preCheckStart;
|
|
5020
|
+
captureDecision(
|
|
5021
|
+
decision,
|
|
5022
|
+
null,
|
|
5023
|
+
request,
|
|
5024
|
+
guardStart,
|
|
5025
|
+
requestToolNames,
|
|
5026
|
+
null,
|
|
5027
|
+
narrowResult,
|
|
5028
|
+
null,
|
|
5029
|
+
null,
|
|
5030
|
+
null,
|
|
5031
|
+
void 0,
|
|
5032
|
+
timing
|
|
5033
|
+
);
|
|
5034
|
+
throw buildContractError2(decision);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
5038
|
+
if (messages.length > 0) {
|
|
5039
|
+
const msgResult = validateToolResultMessages(messages, contracts, provider);
|
|
5040
|
+
if (!msgResult.passed) {
|
|
5041
|
+
emitDiagnostic2(diagnostics, {
|
|
5042
|
+
type: "replay_contract_error",
|
|
5043
|
+
details: `Message validation: ${msgResult.failures.map((f) => f.detail).join("; ")}`
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
if (messages.length > 0) {
|
|
5048
|
+
const toolResults = extractToolResults(messages, provider);
|
|
5049
|
+
if (toolResults.length > 0) {
|
|
5050
|
+
const outputUpdates = extractOutputFromToolResults(toolResults, sessionState, contracts);
|
|
5051
|
+
sessionState = applyOutputExtracts(sessionState, outputUpdates);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
const inputFailures = evaluateInputInvariants(request, contracts);
|
|
5055
|
+
if (mode === "enforce" && inputFailures.length > 0) {
|
|
5056
|
+
if (onError === "block") {
|
|
5057
|
+
const decision = {
|
|
5058
|
+
action: "block",
|
|
5059
|
+
tool_calls: [],
|
|
5060
|
+
blocked: [{
|
|
5061
|
+
tool_name: "_request",
|
|
5062
|
+
arguments: "",
|
|
5063
|
+
reason: "input_invariant_failed",
|
|
5064
|
+
contract_file: inputFailures[0]?.contract_file ?? "",
|
|
5065
|
+
failures: inputFailures
|
|
5066
|
+
}],
|
|
5067
|
+
response_modification: gateMode
|
|
5068
|
+
};
|
|
5069
|
+
timing.pre_check_ms = Date.now() - preCheckStart;
|
|
5070
|
+
captureDecision(
|
|
5071
|
+
decision,
|
|
5072
|
+
null,
|
|
5073
|
+
request,
|
|
5074
|
+
guardStart,
|
|
5075
|
+
requestToolNames,
|
|
5076
|
+
null,
|
|
5077
|
+
narrowResult,
|
|
5078
|
+
null,
|
|
5079
|
+
null,
|
|
5080
|
+
null,
|
|
5081
|
+
void 0,
|
|
5082
|
+
timing
|
|
5083
|
+
);
|
|
5084
|
+
throw buildContractError2(decision);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
timing.pre_check_ms = Date.now() - preCheckStart;
|
|
5088
|
+
let lastError = null;
|
|
5089
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
5090
|
+
if (killed) throw new ReplayKillError(sessionId, killedAt);
|
|
5091
|
+
let attemptPreparedRequestId = null;
|
|
5092
|
+
let attemptDegraded = false;
|
|
5093
|
+
let attemptPendingCalls = null;
|
|
5094
|
+
const isActiveGovern = protectionLevel === "govern" && !runtimeDegraded && runtimeClient != null && runtimeSession != null && leaseFence != null;
|
|
5095
|
+
if (isActiveGovern) {
|
|
5096
|
+
const rtPreflightStart = Date.now();
|
|
5097
|
+
try {
|
|
5098
|
+
const pf = await runtimeClient.preflight({
|
|
5099
|
+
sessionId,
|
|
5100
|
+
leaseFence,
|
|
5101
|
+
requestEnvelope: activeArgs[0],
|
|
5102
|
+
provider,
|
|
5103
|
+
modelId: typeof request.model === "string" ? request.model : null
|
|
5104
|
+
});
|
|
5105
|
+
attemptPreparedRequestId = pf.preparedRequestId;
|
|
5106
|
+
leaseFence = pf.leaseFence;
|
|
5107
|
+
} catch (err) {
|
|
5108
|
+
attemptDegraded = true;
|
|
5109
|
+
if (runtimeClient.isCircuitOpen()) runtimeDegraded = true;
|
|
5110
|
+
emitDiagnostic2(diagnostics, {
|
|
5111
|
+
type: "replay_inactive",
|
|
5112
|
+
reason: "runtime_preflight_failed",
|
|
5113
|
+
error_message: err instanceof Error ? err.message : String(err)
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
timing.runtime_ms += Date.now() - rtPreflightStart;
|
|
5117
|
+
}
|
|
5118
|
+
const llmCallStart = Date.now();
|
|
5119
|
+
const response = await terminalInfo.originalCreate.apply(this, activeArgs);
|
|
5120
|
+
timing.llm_call_ms += Date.now() - llmCallStart;
|
|
5121
|
+
if (killed) throw new ReplayKillError(sessionId, killedAt);
|
|
5122
|
+
const responseUsage = extractUsage(response, provider);
|
|
5123
|
+
if (responseUsage) {
|
|
5124
|
+
const costDelta = (responseUsage.prompt_tokens + responseUsage.completion_tokens) * 1e-5;
|
|
5125
|
+
sessionState = updateActualCost(sessionState, costDelta);
|
|
5126
|
+
}
|
|
5127
|
+
if (mode === "log-only") {
|
|
5128
|
+
captureDecision(
|
|
5129
|
+
{ action: "allow", tool_calls: extractToolCalls(response, provider) },
|
|
5130
|
+
response,
|
|
5131
|
+
request,
|
|
5132
|
+
guardStart,
|
|
5133
|
+
requestToolNames,
|
|
5134
|
+
null,
|
|
5135
|
+
narrowResult,
|
|
5136
|
+
null,
|
|
5137
|
+
null,
|
|
5138
|
+
null,
|
|
5139
|
+
void 0,
|
|
5140
|
+
timing
|
|
5141
|
+
);
|
|
5142
|
+
return response;
|
|
5143
|
+
}
|
|
5144
|
+
const toolCalls = extractToolCalls(response, provider);
|
|
5145
|
+
const validateStart = Date.now();
|
|
5146
|
+
const validation = validateResponse2(response, toolCalls, contracts, requestToolNames, unmatchedPolicy, provider);
|
|
5147
|
+
timing.validate_ms += Date.now() - validateStart;
|
|
5148
|
+
if (isActiveGovern && !attemptDegraded && attemptPreparedRequestId) {
|
|
5149
|
+
const rtProposalStart = Date.now();
|
|
5150
|
+
try {
|
|
5151
|
+
const pr = await runtimeClient.submitProposal({
|
|
5152
|
+
sessionId,
|
|
5153
|
+
leaseFence,
|
|
5154
|
+
preparedRequestId: attemptPreparedRequestId,
|
|
5155
|
+
responseEnvelope: response
|
|
5156
|
+
});
|
|
5157
|
+
attemptPendingCalls = new Map(
|
|
5158
|
+
pr.pendingCalls.map((pc) => [pc.toolCallId, pc])
|
|
5159
|
+
);
|
|
5160
|
+
for (const bc of pr.blockedCalls) {
|
|
5161
|
+
validation.failures.push({
|
|
5162
|
+
path: `$.tool_calls.${bc.toolName}`,
|
|
5163
|
+
operator: "server_blocked",
|
|
5164
|
+
expected: "allowed",
|
|
5165
|
+
found: bc.reason,
|
|
5166
|
+
message: `Server blocked: ${bc.toolName} \u2014 ${bc.reason}`,
|
|
5167
|
+
contract_file: ""
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
} catch (err) {
|
|
5171
|
+
attemptDegraded = true;
|
|
5172
|
+
if (runtimeClient.isCircuitOpen()) runtimeDegraded = true;
|
|
5173
|
+
emitDiagnostic2(diagnostics, {
|
|
5174
|
+
type: "replay_inactive",
|
|
5175
|
+
reason: "runtime_proposal_failed",
|
|
5176
|
+
error_message: err instanceof Error ? err.message : String(err)
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
timing.runtime_ms += Date.now() - rtProposalStart;
|
|
5180
|
+
}
|
|
5181
|
+
const crossStepStart = Date.now();
|
|
5182
|
+
const crossStepContracts = compiledSession ? Array.from(compiledSession.perToolContracts.values()) : contracts;
|
|
5183
|
+
const crossStepResult = validateCrossStep(toolCalls, sessionState, crossStepContracts);
|
|
5184
|
+
if (!crossStepResult.passed) {
|
|
5185
|
+
for (const f of crossStepResult.failures) {
|
|
5186
|
+
validation.failures.push({
|
|
5187
|
+
path: `$.tool_calls.${f.toolName}`,
|
|
5188
|
+
operator: f.reason,
|
|
5189
|
+
expected: "",
|
|
5190
|
+
found: "",
|
|
5191
|
+
message: f.detail,
|
|
5192
|
+
contract_file: ""
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
timing.cross_step_ms += Date.now() - crossStepStart;
|
|
5197
|
+
let phaseResult = null;
|
|
5198
|
+
const phaseStart = Date.now();
|
|
5199
|
+
if (compiledSession) {
|
|
5200
|
+
phaseResult = validatePhaseTransition(toolCalls, sessionState, compiledSession);
|
|
5201
|
+
if (!phaseResult.legal) {
|
|
5202
|
+
validation.failures.push({
|
|
5203
|
+
path: `$.tool_calls.${phaseResult.blockedTool}`,
|
|
5204
|
+
operator: phaseResult.reason,
|
|
5205
|
+
expected: "",
|
|
5206
|
+
found: phaseResult.attemptedTransition,
|
|
5207
|
+
message: `Phase transition blocked: ${phaseResult.attemptedTransition} (${phaseResult.reason})`,
|
|
5208
|
+
contract_file: ""
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
timing.phase_ms += Date.now() - phaseStart;
|
|
5213
|
+
const argValuesStart = Date.now();
|
|
5214
|
+
for (const tc of toolCalls) {
|
|
5215
|
+
const contract = contracts.find((c) => c.tool === tc.name);
|
|
5216
|
+
if (contract?.argument_value_invariants && contract.argument_value_invariants.length > 0) {
|
|
5217
|
+
let parsedArgs;
|
|
5218
|
+
try {
|
|
5219
|
+
parsedArgs = JSON.parse(tc.arguments);
|
|
5220
|
+
} catch {
|
|
5221
|
+
parsedArgs = {};
|
|
5222
|
+
}
|
|
5223
|
+
const avResult = evaluateArgumentValueInvariants(parsedArgs, contract.argument_value_invariants);
|
|
5224
|
+
if (!avResult.passed) {
|
|
5225
|
+
for (const f of avResult.failures) {
|
|
5226
|
+
validation.failures.push({
|
|
5227
|
+
path: f.path,
|
|
5228
|
+
operator: f.operator,
|
|
5229
|
+
expected: String(f.expected),
|
|
5230
|
+
found: String(f.actual),
|
|
5231
|
+
message: f.detail,
|
|
5232
|
+
contract_file: contract.contract_file ?? contract.tool,
|
|
5233
|
+
_tool_call_id: tc.id,
|
|
5234
|
+
_tool_call_arguments: tc.arguments
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
if (resolvedSessionLimits) {
|
|
5240
|
+
const perToolResult = checkPerToolLimits(sessionState, tc.name, resolvedSessionLimits);
|
|
5241
|
+
if (perToolResult.exceeded) {
|
|
5242
|
+
validation.failures.push({
|
|
5243
|
+
path: `$.tool_calls.${tc.name}`,
|
|
5244
|
+
operator: "session_limit",
|
|
5245
|
+
expected: "",
|
|
5246
|
+
found: "",
|
|
5247
|
+
message: perToolResult.reason ?? "per-tool limit exceeded",
|
|
5248
|
+
contract_file: ""
|
|
5249
|
+
});
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
if (resolvedSessionLimits?.loop_detection) {
|
|
5253
|
+
const loopResult = checkLoopDetection(
|
|
5254
|
+
tc.name,
|
|
5255
|
+
tc.arguments,
|
|
5256
|
+
sessionState,
|
|
5257
|
+
resolvedSessionLimits.loop_detection
|
|
5258
|
+
);
|
|
5259
|
+
if (loopResult.triggered) {
|
|
5260
|
+
validation.failures.push({
|
|
5261
|
+
path: `$.tool_calls.${tc.name}`,
|
|
5262
|
+
operator: "loop_detected",
|
|
5263
|
+
expected: `< ${loopResult.threshold} occurrences in window ${loopResult.window}`,
|
|
5264
|
+
found: String(loopResult.matchCount),
|
|
5265
|
+
message: `Loop detected: ${tc.name} repeated ${loopResult.matchCount} times in last ${loopResult.window} steps`,
|
|
5266
|
+
contract_file: ""
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
timing.argument_values_ms += Date.now() - argValuesStart;
|
|
5272
|
+
let policyVerdicts = null;
|
|
5273
|
+
const policyStart = Date.now();
|
|
5274
|
+
if (compiledSession?.policyProgram && compiledSession.principal !== null && compiledSession.principal !== void 0) {
|
|
5275
|
+
policyVerdicts = /* @__PURE__ */ new Map();
|
|
5276
|
+
for (const tc of toolCalls) {
|
|
5277
|
+
const verdict = evaluatePolicy(
|
|
5278
|
+
tc.name,
|
|
5279
|
+
compiledSession.principal,
|
|
5280
|
+
(() => {
|
|
5281
|
+
try {
|
|
5282
|
+
return JSON.parse(tc.arguments);
|
|
5283
|
+
} catch {
|
|
5284
|
+
return {};
|
|
5285
|
+
}
|
|
5286
|
+
})(),
|
|
5287
|
+
sessionState,
|
|
5288
|
+
compiledSession.policyProgram
|
|
5289
|
+
);
|
|
5290
|
+
policyVerdicts.set(tc.name, verdict);
|
|
5291
|
+
if (!verdict.allowed) {
|
|
5292
|
+
validation.failures.push({
|
|
5293
|
+
path: `$.tool_calls.${tc.name}`,
|
|
5294
|
+
operator: "policy_denied",
|
|
5295
|
+
expected: "allowed",
|
|
5296
|
+
found: verdict.reason ?? "denied",
|
|
5297
|
+
message: `Policy denied: ${tc.name} \u2014 ${verdict.reason}`,
|
|
5298
|
+
contract_file: ""
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
timing.policy_ms += Date.now() - policyStart;
|
|
5304
|
+
if (mode === "shadow") {
|
|
5305
|
+
const shadowGateStart = Date.now();
|
|
5306
|
+
const shadowDecision = validation.failures.length > 0 ? {
|
|
5307
|
+
action: "block",
|
|
5308
|
+
tool_calls: toolCalls,
|
|
5309
|
+
blocked: buildBlockedCalls(toolCalls, validation.failures, validation.unmatchedBlocked),
|
|
5310
|
+
response_modification: gateMode
|
|
5311
|
+
} : { action: "allow", tool_calls: toolCalls };
|
|
5312
|
+
const shadowDelta = {
|
|
5313
|
+
would_have_blocked: shadowDecision.action === "block" ? shadowDecision.blocked : [],
|
|
5314
|
+
would_have_narrowed: narrowResult?.removed ?? [],
|
|
5315
|
+
current_phase: sessionState.currentPhase,
|
|
5316
|
+
legal_next_phases: compiledSession ? getLegalNextPhases(sessionState, compiledSession) : []
|
|
5317
|
+
};
|
|
5318
|
+
lastShadowDeltaValue = shadowDelta;
|
|
5319
|
+
shadowEvaluationCount++;
|
|
5320
|
+
timing.gate_ms += Date.now() - shadowGateStart;
|
|
5321
|
+
captureDecision(shadowDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, shadowDelta, timing);
|
|
5322
|
+
return response;
|
|
5323
|
+
}
|
|
5324
|
+
if (isCompatAdvisory) {
|
|
5325
|
+
const advisoryGateStart = Date.now();
|
|
5326
|
+
const advisoryDecision = buildDecision(toolCalls, validation, gateMode, compiledSession);
|
|
5327
|
+
timing.gate_ms += Date.now() - advisoryGateStart;
|
|
5328
|
+
const advisoryFinalizeStart = Date.now();
|
|
5329
|
+
if (advisoryDecision.action === "allow" || advisoryDecision.action === "block") {
|
|
5330
|
+
const completedStep = buildCompletedStep(
|
|
5331
|
+
sessionState.totalStepCount,
|
|
5332
|
+
sessionId,
|
|
5333
|
+
toolCalls,
|
|
5334
|
+
contracts,
|
|
5335
|
+
response,
|
|
5336
|
+
provider,
|
|
5337
|
+
effectiveTier,
|
|
5338
|
+
compiledSession
|
|
5339
|
+
);
|
|
5340
|
+
if (phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase) {
|
|
5341
|
+
completedStep.phaseTransition = `${sessionState.currentPhase} \u2192 ${phaseResult.newPhase}`;
|
|
5342
|
+
completedStep.phase = phaseResult.newPhase;
|
|
5343
|
+
} else {
|
|
5344
|
+
completedStep.phase = sessionState.currentPhase;
|
|
5345
|
+
}
|
|
5346
|
+
const prevVersion = sessionState.stateVersion;
|
|
5347
|
+
sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
|
|
5348
|
+
syncStateToStore(prevVersion, sessionState);
|
|
5349
|
+
}
|
|
5350
|
+
if (advisoryDecision.action === "block") {
|
|
5351
|
+
sessionState = recordDecisionOutcome(sessionState, "blocked");
|
|
5352
|
+
emitDiagnostic2(diagnostics, {
|
|
5353
|
+
type: "replay_compat_advisory",
|
|
5354
|
+
session_id: sessionId,
|
|
5355
|
+
would_block: advisoryDecision.blocked,
|
|
5356
|
+
details: advisoryDecision.blocked.map((b) => `${b.tool_name}: ${b.reason}`).join("; ")
|
|
5357
|
+
});
|
|
5358
|
+
} else {
|
|
5359
|
+
sessionState = recordDecisionOutcome(sessionState, "allowed");
|
|
5360
|
+
}
|
|
5361
|
+
timing.finalize_ms += Date.now() - advisoryFinalizeStart;
|
|
5362
|
+
captureDecision(advisoryDecision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
|
|
5363
|
+
return response;
|
|
5364
|
+
}
|
|
5365
|
+
const enforceGateStart = Date.now();
|
|
5366
|
+
const decision = buildDecision(toolCalls, validation, gateMode, compiledSession);
|
|
5367
|
+
timing.gate_ms += Date.now() - enforceGateStart;
|
|
5368
|
+
if (decision.action === "allow") {
|
|
5369
|
+
const enforceFinalizeStart = Date.now();
|
|
5370
|
+
const completedStep = buildCompletedStep(
|
|
5371
|
+
sessionState.totalStepCount,
|
|
5372
|
+
sessionId,
|
|
5373
|
+
toolCalls,
|
|
5374
|
+
contracts,
|
|
5375
|
+
response,
|
|
5376
|
+
provider,
|
|
5377
|
+
effectiveTier,
|
|
5378
|
+
compiledSession
|
|
5379
|
+
);
|
|
5380
|
+
if (phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase) {
|
|
5381
|
+
completedStep.phaseTransition = `${sessionState.currentPhase} \u2192 ${phaseResult.newPhase}`;
|
|
5382
|
+
completedStep.phase = phaseResult.newPhase;
|
|
5383
|
+
} else {
|
|
5384
|
+
completedStep.phase = sessionState.currentPhase;
|
|
5385
|
+
}
|
|
5386
|
+
const prevVersionAllow = sessionState.stateVersion;
|
|
5387
|
+
sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
|
|
5388
|
+
sessionState = recordDecisionOutcome(sessionState, "allowed");
|
|
5389
|
+
syncStateToStore(prevVersionAllow, sessionState);
|
|
5390
|
+
timing.finalize_ms += Date.now() - enforceFinalizeStart;
|
|
5391
|
+
if (isActiveGovern && !attemptDegraded && attemptPendingCalls && attemptPendingCalls.size > 0) {
|
|
5392
|
+
for (const [toolCallId, pending] of attemptPendingCalls) {
|
|
5393
|
+
deferredReceipts.set(toolCallId, {
|
|
5394
|
+
pendingCallId: pending.pendingCallId,
|
|
5395
|
+
toolName: pending.toolName,
|
|
5396
|
+
argumentsHash: stripHashPrefix(pending.argumentsHash)
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
|
|
5401
|
+
return response;
|
|
5402
|
+
}
|
|
5403
|
+
sessionState = recordDecisionOutcome(sessionState, "blocked");
|
|
5404
|
+
if (isActiveGovern && !attemptDegraded && attemptPendingCalls && attemptPendingCalls.size > 0) {
|
|
5405
|
+
const rtBlockReceiptStart = Date.now();
|
|
5406
|
+
const blockedToolCallIds = new Set(
|
|
5407
|
+
decision.action === "block" ? decision.blocked.map((b) => {
|
|
5408
|
+
const tc = toolCalls.find((c) => c.name === b.tool_name && c.arguments === b.arguments);
|
|
5409
|
+
return tc?.id;
|
|
5410
|
+
}).filter((id) => id != null) : []
|
|
5411
|
+
);
|
|
5412
|
+
const receiptNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
5413
|
+
for (const [toolCallId, pending] of attemptPendingCalls) {
|
|
5414
|
+
if (blockedToolCallIds.has(toolCallId) || gateMode === "reject_all") {
|
|
5415
|
+
try {
|
|
5416
|
+
await runtimeClient.submitReceipt({
|
|
5417
|
+
sessionId,
|
|
5418
|
+
leaseFence,
|
|
5419
|
+
pendingCallId: pending.pendingCallId,
|
|
5420
|
+
executorKind: "WRAPPED_EXECUTOR",
|
|
5421
|
+
toolName: pending.toolName,
|
|
5422
|
+
argumentsHash: stripHashPrefix(pending.argumentsHash),
|
|
5423
|
+
status: "DISCARDED",
|
|
5424
|
+
startedAt: receiptNow,
|
|
5425
|
+
completedAt: receiptNow
|
|
5426
|
+
});
|
|
5427
|
+
} catch (err) {
|
|
5428
|
+
attemptDegraded = true;
|
|
5429
|
+
if (runtimeClient.isCircuitOpen()) runtimeDegraded = true;
|
|
5430
|
+
emitDiagnostic2(diagnostics, {
|
|
5431
|
+
type: "replay_inactive",
|
|
5432
|
+
reason: "runtime_receipt_failed",
|
|
5433
|
+
error_message: err instanceof Error ? err.message : String(err)
|
|
5434
|
+
});
|
|
5435
|
+
break;
|
|
5436
|
+
}
|
|
5437
|
+
} else {
|
|
5438
|
+
deferredReceipts.set(toolCallId, {
|
|
5439
|
+
pendingCallId: pending.pendingCallId,
|
|
5440
|
+
toolName: pending.toolName,
|
|
5441
|
+
argumentsHash: stripHashPrefix(pending.argumentsHash)
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
timing.runtime_ms += Date.now() - rtBlockReceiptStart;
|
|
5446
|
+
}
|
|
5447
|
+
if (resolvedSessionLimits?.circuit_breaker) {
|
|
5448
|
+
const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
|
|
5449
|
+
if (cbResult.triggered) {
|
|
5450
|
+
killed = true;
|
|
5451
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5452
|
+
sessionState = killSession(sessionState);
|
|
5453
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
if (attempt < maxRetries) {
|
|
5457
|
+
lastError = new ReplayContractError(
|
|
5458
|
+
`Blocked on attempt ${attempt + 1}`,
|
|
5459
|
+
decision,
|
|
5460
|
+
decision.blocked[0]?.contract_file ?? "",
|
|
5461
|
+
decision.blocked[0]?.failures ?? []
|
|
5462
|
+
);
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
captureDecision(decision, response, request, guardStart, requestToolNames, crossStepResult, narrowResult, phaseResult, policyVerdicts, null, void 0, timing);
|
|
5466
|
+
return applyGateDecision(decision, response, provider, gateMode, opts.onBlock);
|
|
5467
|
+
}
|
|
5468
|
+
if (lastError) throw lastError;
|
|
5469
|
+
throw new ReplayInternalError("Retry loop exhausted without result", { sessionId });
|
|
5470
|
+
} catch (err) {
|
|
5471
|
+
if (err instanceof ReplayContractError || err instanceof ReplayKillError) {
|
|
5472
|
+
throw err;
|
|
5473
|
+
}
|
|
5474
|
+
sessionState = recordDecisionOutcome(sessionState, "error");
|
|
5475
|
+
if (resolvedSessionLimits?.circuit_breaker) {
|
|
5476
|
+
const cbResult = checkCircuitBreaker(sessionState, resolvedSessionLimits.circuit_breaker);
|
|
5477
|
+
if (cbResult.triggered) {
|
|
5478
|
+
killed = true;
|
|
5479
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5480
|
+
sessionState = killSession(sessionState);
|
|
5481
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
if (onError === "block") {
|
|
5485
|
+
throw new ReplayInternalError("Enforcement pipeline internal error", { cause: err, sessionId });
|
|
5486
|
+
}
|
|
5487
|
+
sessionState = { ...sessionState, totalUnguardedCalls: sessionState.totalUnguardedCalls + 1 };
|
|
5488
|
+
if (sessionState.totalUnguardedCalls >= maxUnguardedCalls) {
|
|
5489
|
+
killed = true;
|
|
5490
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5491
|
+
sessionState = killSession(sessionState);
|
|
5492
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
5493
|
+
}
|
|
5494
|
+
return terminalInfo.originalCreate.apply(this, args);
|
|
5495
|
+
}
|
|
5496
|
+
};
|
|
5497
|
+
const wrapperClient = createWrapperClient(client, provider, enforcementCreate);
|
|
5498
|
+
const bypassCreate = function replayBypassProxy(...args) {
|
|
5499
|
+
bypassDetected = true;
|
|
5500
|
+
emitDiagnostic2(diagnostics, { type: "replay_bypass_detected", session_id: sessionId });
|
|
5501
|
+
if (runtimeClient && runtimeSession) {
|
|
5502
|
+
runtimeClient.reportBypass({
|
|
5503
|
+
sessionId,
|
|
5504
|
+
source: "bypass_proxy",
|
|
5505
|
+
detail: "Direct call on original client detected"
|
|
5506
|
+
}).catch(() => {
|
|
5507
|
+
});
|
|
5508
|
+
}
|
|
5509
|
+
return terminalInfo.originalCreate.apply(this, args);
|
|
5510
|
+
};
|
|
5511
|
+
terminalInfo.terminal[terminalInfo.methodName] = bypassCreate;
|
|
5512
|
+
setReplayAttached(client);
|
|
5513
|
+
emitDiagnostic2(diagnostics, {
|
|
5514
|
+
type: "replay_activated",
|
|
5515
|
+
session_id: sessionId,
|
|
5516
|
+
provider,
|
|
5517
|
+
agent,
|
|
5518
|
+
mode
|
|
5519
|
+
});
|
|
5520
|
+
const session = {
|
|
5521
|
+
client: wrapperClient,
|
|
5522
|
+
async flush() {
|
|
5523
|
+
if (!buffer) {
|
|
5524
|
+
return { captured: 0, sent: 0, active: false, errors: [] };
|
|
5525
|
+
}
|
|
5526
|
+
return buffer.flush();
|
|
5527
|
+
},
|
|
5528
|
+
restore() {
|
|
5529
|
+
if (restored) return;
|
|
5530
|
+
restored = true;
|
|
5531
|
+
if (terminalInfo.terminal[terminalInfo.methodName] === bypassCreate) {
|
|
5532
|
+
terminalInfo.terminal[terminalInfo.methodName] = terminalInfo.originalCreate;
|
|
5533
|
+
}
|
|
5534
|
+
clearReplayAttached(client);
|
|
5535
|
+
buffer?.close();
|
|
5536
|
+
},
|
|
5537
|
+
kill() {
|
|
5538
|
+
if (killed) return;
|
|
5539
|
+
killed = true;
|
|
5540
|
+
killedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5541
|
+
const prevVersionKill = sessionState.stateVersion;
|
|
5542
|
+
sessionState = killSession(sessionState);
|
|
5543
|
+
syncStateToStore(prevVersionKill, sessionState);
|
|
5544
|
+
emitDiagnostic2(diagnostics, { type: "replay_kill", session_id: sessionId });
|
|
5545
|
+
if (runtimeClient && runtimeSession && leaseFence) {
|
|
5546
|
+
runtimeClient.killSession({
|
|
5547
|
+
sessionId,
|
|
5548
|
+
leaseFence,
|
|
5549
|
+
reason: "sdk_kill"
|
|
5550
|
+
}).catch(() => {
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
buffer?.flush().catch(() => {
|
|
5554
|
+
});
|
|
5555
|
+
},
|
|
5556
|
+
getHealth() {
|
|
5557
|
+
const isAuthoritative = runtimeSession != null && !runtimeDegraded;
|
|
5558
|
+
const effectiveProtection = runtimeDegraded ? "protect" : protectionLevel;
|
|
5559
|
+
let durability;
|
|
5560
|
+
if (isAuthoritative) {
|
|
5561
|
+
durability = runtimeClient?.isCircuitOpen() ? "degraded-local" : "server";
|
|
5562
|
+
} else {
|
|
5563
|
+
durability = runtimeDegraded ? "degraded-local" : "inactive";
|
|
5564
|
+
}
|
|
5565
|
+
let authorityState;
|
|
5566
|
+
if (killed) authorityState = "killed";
|
|
5567
|
+
else if (bypassDetected) authorityState = "compromised";
|
|
5568
|
+
else if (isAuthoritative) authorityState = "active";
|
|
5569
|
+
else authorityState = "advisory";
|
|
5570
|
+
return {
|
|
5571
|
+
status: killed ? "inactive" : sessionState.consecutiveErrorCount > 0 ? "degraded" : "healthy",
|
|
5572
|
+
authorityState,
|
|
5573
|
+
protectionLevel: effectiveProtection,
|
|
5574
|
+
durability,
|
|
5575
|
+
tier: sessionState.tier,
|
|
5576
|
+
compatEnforcement,
|
|
5577
|
+
cluster_detected: false,
|
|
5578
|
+
bypass_detected: bypassDetected,
|
|
5579
|
+
totalSteps: sessionState.totalStepCount,
|
|
5580
|
+
totalBlocks: sessionState.totalBlockCount,
|
|
5581
|
+
totalErrors: sessionState.consecutiveErrorCount,
|
|
5582
|
+
killed,
|
|
5583
|
+
shadowEvaluations: shadowEvaluationCount
|
|
5584
|
+
};
|
|
5585
|
+
},
|
|
5586
|
+
/**
|
|
5587
|
+
* v2: Return redacted session state snapshot.
|
|
5588
|
+
* @see specs/replay-v2.md § getState() contract
|
|
5589
|
+
*/
|
|
5590
|
+
getState() {
|
|
5591
|
+
return buildStateSnapshot(sessionState, toNarrowingSnapshot(lastNarrowResult));
|
|
5592
|
+
},
|
|
5593
|
+
getLastNarrowing() {
|
|
5594
|
+
return toNarrowingSnapshot(lastNarrowResult);
|
|
5595
|
+
},
|
|
5596
|
+
getLastShadowDelta() {
|
|
5597
|
+
return lastShadowDeltaValue;
|
|
5598
|
+
},
|
|
5599
|
+
/**
|
|
5600
|
+
* v3: Manually restrict available tools within compiled legal space.
|
|
5601
|
+
* @see specs/replay-v3.md § narrow() / widen()
|
|
5602
|
+
*/
|
|
5603
|
+
narrow(toolFilter) {
|
|
5604
|
+
if (killed || restored) return;
|
|
5605
|
+
manualFilter = toolFilter.length > 0 ? toolFilter : null;
|
|
5606
|
+
sessionState = { ...sessionState, controlRevision: sessionState.controlRevision + 1 };
|
|
5607
|
+
if (runtimeClient && runtimeSession && leaseFence) {
|
|
5608
|
+
runtimeClient.setToolFilter({
|
|
5609
|
+
sessionId,
|
|
5610
|
+
leaseFence,
|
|
5611
|
+
allowedTools: manualFilter
|
|
5612
|
+
}).catch(() => {
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
},
|
|
5616
|
+
/**
|
|
5617
|
+
* v3: Remove manual restriction, return to contract-driven narrowing.
|
|
5618
|
+
* @see specs/replay-v3.md § narrow() / widen()
|
|
5619
|
+
*/
|
|
5620
|
+
widen() {
|
|
5621
|
+
if (killed || restored) return;
|
|
5622
|
+
if (manualFilter === null) return;
|
|
5623
|
+
manualFilter = null;
|
|
5624
|
+
sessionState = { ...sessionState, controlRevision: sessionState.controlRevision + 1 };
|
|
5625
|
+
if (runtimeClient && runtimeSession && leaseFence) {
|
|
5626
|
+
runtimeClient.setToolFilter({
|
|
5627
|
+
sessionId,
|
|
5628
|
+
leaseFence,
|
|
5629
|
+
allowedTools: null
|
|
5630
|
+
}).catch(() => {
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
},
|
|
5634
|
+
tools: wrapToolsWithDeferredReceipts(
|
|
5635
|
+
buildWrappedToolsMap(opts.tools, compiledSession)
|
|
5636
|
+
),
|
|
5637
|
+
async getWorkflowState() {
|
|
5638
|
+
if (!workflowId || !runtimeClient || runtimeDegraded) return null;
|
|
5639
|
+
if (runtimeInitPromise && !runtimeInitDone) await runtimeInitPromise;
|
|
5640
|
+
if (!runtimeSession?.workflow) return null;
|
|
5641
|
+
try {
|
|
5642
|
+
const state = await runtimeClient.getWorkflowState(workflowId);
|
|
5643
|
+
return {
|
|
5644
|
+
workflowId: state.workflowId,
|
|
5645
|
+
rootSessionId: state.rootSessionId,
|
|
5646
|
+
status: state.status,
|
|
5647
|
+
stateVersion: state.stateVersion,
|
|
5648
|
+
controlRevision: state.controlRevision,
|
|
5649
|
+
totalSessionCount: state.totalSessionCount,
|
|
5650
|
+
activeSessionCount: state.activeSessionCount,
|
|
5651
|
+
totalStepCount: state.totalStepCount,
|
|
5652
|
+
totalCost: state.totalCost,
|
|
5653
|
+
totalHandoffCount: state.totalHandoffCount,
|
|
5654
|
+
unresolvedHandoffCount: state.unresolvedHandoffCount,
|
|
5655
|
+
lastEventSeq: state.lastEventSeq,
|
|
5656
|
+
killScope: state.killScope,
|
|
5657
|
+
createdAt: state.createdAt,
|
|
5658
|
+
updatedAt: state.updatedAt
|
|
5659
|
+
};
|
|
5660
|
+
} catch {
|
|
5661
|
+
return null;
|
|
5662
|
+
}
|
|
5663
|
+
},
|
|
5664
|
+
async handoff(offer) {
|
|
5665
|
+
if (!workflowId || !runtimeClient || runtimeDegraded) return null;
|
|
5666
|
+
if (runtimeInitPromise && !runtimeInitDone) await runtimeInitPromise;
|
|
5667
|
+
if (!runtimeSession?.workflow) return null;
|
|
5668
|
+
try {
|
|
5669
|
+
const result = await runtimeClient.offerHandoff({
|
|
5670
|
+
sessionId,
|
|
5671
|
+
workflowId,
|
|
5672
|
+
fromRole: runtimeSession.workflow.role,
|
|
5673
|
+
toRole: offer.toRole,
|
|
5674
|
+
handoffId: offer.handoffId,
|
|
5675
|
+
artifactRefs: offer.artifactRefs,
|
|
5676
|
+
summary: offer.summary
|
|
5677
|
+
});
|
|
5678
|
+
return {
|
|
5679
|
+
handoffId: result.handoffId,
|
|
5680
|
+
eventSeq: result.eventSeq,
|
|
5681
|
+
stateVersion: result.stateVersion
|
|
5682
|
+
};
|
|
5683
|
+
} catch {
|
|
5684
|
+
return null;
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
};
|
|
5688
|
+
return session;
|
|
5689
|
+
function wrapToolsWithDeferredReceipts(baseTools) {
|
|
5690
|
+
const wrapped = {};
|
|
5691
|
+
for (const [toolName, executor] of Object.entries(baseTools)) {
|
|
5692
|
+
wrapped[toolName] = async (args) => {
|
|
5693
|
+
const result = await executor(args);
|
|
5694
|
+
if (runtimeClient && leaseFence && !runtimeDegraded) {
|
|
5695
|
+
for (const [callId, deferred] of deferredReceipts) {
|
|
5696
|
+
if (deferred.toolName === toolName) {
|
|
5697
|
+
deferredReceipts.delete(callId);
|
|
5698
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5699
|
+
try {
|
|
5700
|
+
const receiptResult = await runtimeClient.submitReceipt({
|
|
5701
|
+
sessionId,
|
|
5702
|
+
leaseFence,
|
|
5703
|
+
pendingCallId: deferred.pendingCallId,
|
|
5704
|
+
executorKind: "WRAPPED_EXECUTOR",
|
|
5705
|
+
toolName: deferred.toolName,
|
|
5706
|
+
argumentsHash: deferred.argumentsHash,
|
|
5707
|
+
status: "SUCCEEDED",
|
|
5708
|
+
startedAt: now,
|
|
5709
|
+
completedAt: now
|
|
5710
|
+
});
|
|
5711
|
+
if (receiptResult.stateAdvanced) {
|
|
5712
|
+
sessionState = { ...sessionState, stateVersion: receiptResult.stateVersion };
|
|
5713
|
+
}
|
|
5714
|
+
} catch {
|
|
5715
|
+
}
|
|
5716
|
+
break;
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
return result;
|
|
5721
|
+
};
|
|
5722
|
+
}
|
|
5723
|
+
return wrapped;
|
|
5724
|
+
}
|
|
5725
|
+
function captureDecision(decision, response, request, guardStart, requestToolNames, crossStep, narrowing = null, phaseResult = null, policyVerdictMap = null, constraintVerdictVal = null, shadowDelta = void 0, timingParam) {
|
|
5726
|
+
if (!buffer && !store) return;
|
|
5727
|
+
if (timingParam) {
|
|
5728
|
+
timingParam.total_ms = Date.now() - guardStart;
|
|
5729
|
+
timingParam.enforcement_ms = timingParam.total_ms - timingParam.llm_call_ms;
|
|
5730
|
+
}
|
|
5731
|
+
const guardOverheadMs = timingParam ? timingParam.enforcement_ms : Date.now() - guardStart;
|
|
5732
|
+
const phaseTransitionStr = phaseResult && !phaseResult.legal ? phaseResult.attemptedTransition : phaseResult && phaseResult.legal && phaseResult.newPhase !== sessionState.currentPhase ? `${sessionState.currentPhase} \u2192 ${phaseResult.newPhase}` : null;
|
|
5733
|
+
const primaryTool = decision.tool_calls[0]?.name;
|
|
5734
|
+
const capturedPolicyVerdict = primaryTool && policyVerdictMap ? policyVerdictMap.get(primaryTool) ?? null : null;
|
|
5735
|
+
const replayMeta = {
|
|
5736
|
+
session_id: sessionId,
|
|
5737
|
+
step_index: sessionState.totalStepCount,
|
|
5738
|
+
mode,
|
|
5739
|
+
decision,
|
|
5740
|
+
contract_hashes: contracts.map((c) => c.tool_schema_hash ?? c.tool),
|
|
5741
|
+
guard_overhead_ms: guardOverheadMs,
|
|
5742
|
+
timing: timingParam,
|
|
5743
|
+
commit_tier: sessionState.tier,
|
|
5744
|
+
principal: opts.principal ?? null,
|
|
5745
|
+
policy_verdict: capturedPolicyVerdict,
|
|
5746
|
+
execution_constraint_verdict: constraintVerdictVal,
|
|
5747
|
+
counterfactual: {
|
|
5748
|
+
tools_removed: narrowing?.removed ?? [],
|
|
5749
|
+
calls_blocked: decision.action === "block" ? decision.blocked : []
|
|
5750
|
+
},
|
|
5751
|
+
// v2 additions
|
|
5752
|
+
cross_step: crossStep ? { passed: crossStep.passed, failures: crossStep.failures.map((f) => ({ toolName: f.toolName, reason: f.reason, detail: f.detail })) } : null,
|
|
5753
|
+
session_state_hash: null,
|
|
5754
|
+
state_version: sessionState.stateVersion,
|
|
5755
|
+
// v3 additions
|
|
5756
|
+
narrowing,
|
|
5757
|
+
phase: sessionState.currentPhase,
|
|
5758
|
+
phase_transition: phaseTransitionStr,
|
|
5759
|
+
shadow_delta: shadowDelta,
|
|
5760
|
+
receipt: null
|
|
5761
|
+
};
|
|
5762
|
+
const capturedCall = {
|
|
5763
|
+
schema_version: CAPTURE_SCHEMA_VERSION_CURRENT,
|
|
5764
|
+
agent,
|
|
5765
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5766
|
+
provider,
|
|
5767
|
+
model_id: typeof request.model === "string" ? request.model : "",
|
|
5768
|
+
primary_tool_name: decision.tool_calls[0]?.name ?? null,
|
|
5769
|
+
tool_names: decision.tool_calls.map((tc) => tc.name),
|
|
5770
|
+
request: {
|
|
5771
|
+
tools: requestToolNames.map((name) => ({ name }))
|
|
5772
|
+
},
|
|
5773
|
+
response: {
|
|
5774
|
+
tool_calls: decision.tool_calls.map((tc) => ({
|
|
5775
|
+
name: tc.name,
|
|
5776
|
+
arguments: tc.arguments
|
|
5777
|
+
})),
|
|
5778
|
+
content: null
|
|
5779
|
+
},
|
|
5780
|
+
latency_ms: Date.now() - guardStart,
|
|
5781
|
+
sdk_session_id: sessionId
|
|
5782
|
+
};
|
|
5783
|
+
try {
|
|
5784
|
+
const serialized = JSON.stringify({ ...capturedCall, replay: replayMeta });
|
|
5785
|
+
const { redacted } = redactCapture(serialized);
|
|
5786
|
+
const redactedCall = JSON.parse(redacted);
|
|
5787
|
+
if (buffer) buffer.push(redactedCall);
|
|
5788
|
+
appendCaptureToStore(redactedCall);
|
|
5789
|
+
} catch {
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
function resolveContracts(opts) {
|
|
5794
|
+
if (opts.contracts) {
|
|
5795
|
+
return loadContracts(opts.contracts);
|
|
5796
|
+
}
|
|
5797
|
+
if (opts.contractsDir) {
|
|
5798
|
+
return loadContracts(opts.contractsDir);
|
|
5799
|
+
}
|
|
5800
|
+
throw new ReplayConfigError("compilation_failed", "No contracts or contractsDir provided");
|
|
5801
|
+
}
|
|
5802
|
+
var SESSION_YAML_NAMES2 = ["session.yaml", "session.yml"];
|
|
5803
|
+
function discoverSessionYaml(opts) {
|
|
5804
|
+
if (opts.sessionYamlPath) {
|
|
5805
|
+
const resolved = (0, import_node_path3.resolve)(opts.sessionYamlPath);
|
|
5806
|
+
const raw = (0, import_node_fs3.readFileSync)(resolved, "utf8");
|
|
5807
|
+
return (0, import_contracts_core6.parseSessionYaml)(raw);
|
|
5808
|
+
}
|
|
5809
|
+
if (opts.contractsDir) {
|
|
5810
|
+
const dir = (0, import_node_path3.resolve)(opts.contractsDir);
|
|
5811
|
+
for (const name of SESSION_YAML_NAMES2) {
|
|
5812
|
+
const candidate = (0, import_node_path3.join)(dir, name);
|
|
5813
|
+
if ((0, import_node_fs3.existsSync)(candidate)) {
|
|
5814
|
+
const raw = (0, import_node_fs3.readFileSync)(candidate, "utf8");
|
|
5815
|
+
return (0, import_contracts_core6.parseSessionYaml)(raw);
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
return null;
|
|
5820
|
+
}
|
|
5821
|
+
var WORKFLOW_YAML_NAMES = ["workflow.yaml", "workflow.yml"];
|
|
5822
|
+
function discoverWorkflowYaml(opts, workflowOpts) {
|
|
5823
|
+
let raw = null;
|
|
5824
|
+
if (workflowOpts.workflowYamlPath) {
|
|
5825
|
+
const resolved = (0, import_node_path3.resolve)(workflowOpts.workflowYamlPath);
|
|
5826
|
+
raw = (0, import_node_fs3.readFileSync)(resolved, "utf8");
|
|
5827
|
+
}
|
|
5828
|
+
if (!raw && opts.contractsDir) {
|
|
5829
|
+
const dir = (0, import_node_path3.resolve)(opts.contractsDir);
|
|
5830
|
+
for (const name of WORKFLOW_YAML_NAMES) {
|
|
5831
|
+
const candidate = (0, import_node_path3.join)(dir, name);
|
|
5832
|
+
if ((0, import_node_fs3.existsSync)(candidate)) {
|
|
5833
|
+
raw = (0, import_node_fs3.readFileSync)(candidate, "utf8");
|
|
5834
|
+
break;
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
if (!raw) return null;
|
|
5839
|
+
const parsed = (0, import_contracts_core6.parseWorkflowYaml)(raw);
|
|
5840
|
+
return (0, import_contracts_core6.compileWorkflow)(parsed);
|
|
5841
|
+
}
|
|
5842
|
+
function generateWorkflowId() {
|
|
5843
|
+
return `rw_${import_node_crypto5.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
5844
|
+
}
|
|
5845
|
+
function validateConfig(contracts, opts) {
|
|
5846
|
+
const hasPolicyBlock = contracts.some((c) => c.policy != null);
|
|
5847
|
+
if (hasPolicyBlock && opts.principal === void 0) {
|
|
5848
|
+
const toolsWithPolicy = contracts.filter((c) => c.policy != null).map((c) => c.tool).join(", ");
|
|
5849
|
+
return new ReplayConfigError(
|
|
5850
|
+
"policy_without_principal",
|
|
5851
|
+
`Contracts with policy blocks (${toolsWithPolicy}) require a principal in replay() options`
|
|
5852
|
+
);
|
|
5853
|
+
}
|
|
5854
|
+
const toolsMap = opts.tools ?? {};
|
|
5855
|
+
const toolsWithConstraints = contracts.filter((c) => c.execution_constraints != null).filter((c) => !(c.tool in toolsMap));
|
|
5856
|
+
if (toolsWithConstraints.length > 0) {
|
|
5857
|
+
const names = toolsWithConstraints.map((c) => c.tool).join(", ");
|
|
5858
|
+
return new ReplayConfigError(
|
|
5859
|
+
"constraints_without_wrapper",
|
|
5860
|
+
`Contracts with execution_constraints (${names}) require matching entries in the tools map`
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
return null;
|
|
5864
|
+
}
|
|
5865
|
+
function resolveTerminal(client, provider) {
|
|
5866
|
+
try {
|
|
5867
|
+
if (provider === "openai") {
|
|
5868
|
+
const chat = client.chat;
|
|
5869
|
+
const completions = chat.completions;
|
|
5870
|
+
const create2 = completions.create;
|
|
5871
|
+
if (typeof create2 !== "function") return null;
|
|
5872
|
+
return {
|
|
5873
|
+
terminal: completions,
|
|
5874
|
+
methodName: "create",
|
|
5875
|
+
originalCreate: create2
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
const messages = client.messages;
|
|
5879
|
+
const create = messages.create;
|
|
5880
|
+
if (typeof create !== "function") return null;
|
|
5881
|
+
return {
|
|
5882
|
+
terminal: messages,
|
|
5883
|
+
methodName: "create",
|
|
5884
|
+
originalCreate: create
|
|
5885
|
+
};
|
|
5886
|
+
} catch {
|
|
5887
|
+
return null;
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
function createWrapperClient(originalClient, provider, enforcementCreate) {
|
|
5891
|
+
if (provider === "openai") {
|
|
5892
|
+
const origChat = originalClient.chat;
|
|
5893
|
+
const origComp = origChat.completions;
|
|
5894
|
+
const compWrapper = Object.create(origComp);
|
|
5895
|
+
compWrapper.create = enforcementCreate;
|
|
5896
|
+
const chatWrapper = Object.create(origChat);
|
|
5897
|
+
chatWrapper.completions = compWrapper;
|
|
5898
|
+
const wrapper2 = Object.create(originalClient);
|
|
5899
|
+
wrapper2.chat = chatWrapper;
|
|
5900
|
+
return wrapper2;
|
|
5901
|
+
}
|
|
5902
|
+
const origMessages = originalClient.messages;
|
|
5903
|
+
const msgWrapper = Object.create(origMessages);
|
|
5904
|
+
msgWrapper.create = enforcementCreate;
|
|
5905
|
+
const wrapper = Object.create(originalClient);
|
|
5906
|
+
wrapper.messages = msgWrapper;
|
|
5907
|
+
return wrapper;
|
|
5908
|
+
}
|
|
5909
|
+
function validateResponse2(response, toolCalls, contracts, requestToolNames, unmatchedPolicy, provider) {
|
|
5910
|
+
const failures = [];
|
|
5911
|
+
const unmatchedBlocked = [];
|
|
5912
|
+
const matched = matchContracts(contracts, toolCalls, void 0);
|
|
5913
|
+
const unmatched = findUnmatchedTools(toolCalls, matched);
|
|
5914
|
+
if (unmatchedPolicy === "block" && unmatched.length > 0) {
|
|
5915
|
+
for (const tc of unmatched) {
|
|
5916
|
+
unmatchedBlocked.push({
|
|
5917
|
+
tool_name: tc.name,
|
|
5918
|
+
arguments: tc.arguments,
|
|
5919
|
+
reason: "unmatched_tool_blocked",
|
|
5920
|
+
contract_file: "",
|
|
5921
|
+
failures: [{
|
|
5922
|
+
path: "$.tool_calls",
|
|
5923
|
+
operator: "contract_match",
|
|
5924
|
+
expected: "known tool",
|
|
5925
|
+
found: tc.name,
|
|
5926
|
+
message: `No contract for tool "${tc.name}"`
|
|
5927
|
+
}]
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
for (const contract of matched) {
|
|
5932
|
+
const outputInvariants = contract.assertions.output_invariants;
|
|
5933
|
+
if (outputInvariants.length > 0) {
|
|
5934
|
+
const normalizedResponse = buildNormalizedResponse(response, toolCalls);
|
|
5935
|
+
const result = (0, import_contracts_core6.evaluateInvariants)(normalizedResponse, outputInvariants, process.env);
|
|
5936
|
+
for (const failure of result) {
|
|
5937
|
+
failures.push({
|
|
5938
|
+
path: failure.path,
|
|
5939
|
+
operator: failure.rule,
|
|
5940
|
+
expected: failure.detail,
|
|
5941
|
+
found: failure.detail,
|
|
5942
|
+
message: failure.detail,
|
|
5943
|
+
contract_file: contract.contract_file ?? contract.tool
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
if (contract.expected_tool_calls && contract.expected_tool_calls.length > 0) {
|
|
5948
|
+
const result = (0, import_contracts_core6.evaluateExpectedToolCalls)(
|
|
5949
|
+
toolCalls,
|
|
5950
|
+
contract.expected_tool_calls,
|
|
5951
|
+
contract.pass_threshold ?? 1,
|
|
5952
|
+
contract.tool_call_match_mode ?? "any",
|
|
5953
|
+
process.env
|
|
5954
|
+
);
|
|
5955
|
+
for (const failure of result.failures) {
|
|
5956
|
+
failures.push({
|
|
5957
|
+
path: failure.path,
|
|
5958
|
+
operator: failure.rule,
|
|
5959
|
+
expected: failure.detail,
|
|
5960
|
+
found: failure.detail,
|
|
5961
|
+
message: failure.detail,
|
|
5962
|
+
contract_file: contract.contract_file ?? contract.tool
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
const formatResult = evaluateResponseFormatInvariants(response, contracts, requestToolNames, provider);
|
|
5968
|
+
failures.push(...formatResult.failures);
|
|
5969
|
+
return { failures, unmatchedBlocked, matchedContracts: matched };
|
|
5970
|
+
}
|
|
5971
|
+
function buildNormalizedResponse(_response, toolCalls) {
|
|
5972
|
+
return {
|
|
5973
|
+
tool_calls: toolCalls.map((tc) => {
|
|
5974
|
+
let parsedArgs;
|
|
5975
|
+
try {
|
|
5976
|
+
parsedArgs = JSON.parse(tc.arguments);
|
|
5977
|
+
} catch {
|
|
5978
|
+
parsedArgs = null;
|
|
5979
|
+
}
|
|
5980
|
+
return {
|
|
5981
|
+
id: tc.id,
|
|
5982
|
+
name: tc.name,
|
|
5983
|
+
function: { name: tc.name },
|
|
5984
|
+
arguments: parsedArgs
|
|
5985
|
+
};
|
|
5986
|
+
})
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
function buildDecision(toolCalls, validation, gateMode, compiled) {
|
|
5990
|
+
const blocked = [
|
|
5991
|
+
...validation.unmatchedBlocked,
|
|
5992
|
+
...buildBlockedCalls(toolCalls, validation.failures, [])
|
|
5993
|
+
];
|
|
5994
|
+
if (compiled) {
|
|
5995
|
+
const failedTools = new Set(validation.failures.map((f) => f.path.split(".").pop() ?? ""));
|
|
5996
|
+
const alreadyBlocked = new Set(blocked.map((b) => b.tool_name));
|
|
5997
|
+
for (const tc of toolCalls) {
|
|
5998
|
+
if (alreadyBlocked.has(tc.name)) continue;
|
|
5999
|
+
if (!failedTools.has(tc.name)) continue;
|
|
6000
|
+
const contract = compiled.perToolContracts.get(tc.name);
|
|
6001
|
+
if (contract?.effectiveGate === "block") {
|
|
6002
|
+
blocked.push({
|
|
6003
|
+
tool_name: tc.name,
|
|
6004
|
+
arguments: tc.arguments,
|
|
6005
|
+
reason: "risk_gate_blocked",
|
|
6006
|
+
contract_file: "",
|
|
6007
|
+
failures: [{
|
|
6008
|
+
path: `$.tool_calls.${tc.name}`,
|
|
6009
|
+
operator: "effective_gate",
|
|
6010
|
+
expected: "allow",
|
|
6011
|
+
found: "block",
|
|
6012
|
+
message: `Tool '${tc.name}' blocked by risk_defaults (side_effect: ${contract.side_effect ?? "unknown"}, effectiveGate: block)`,
|
|
6013
|
+
contract_file: ""
|
|
6014
|
+
}]
|
|
6015
|
+
});
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
if (blocked.length === 0) {
|
|
6020
|
+
return { action: "allow", tool_calls: toolCalls };
|
|
6021
|
+
}
|
|
6022
|
+
return {
|
|
6023
|
+
action: "block",
|
|
6024
|
+
tool_calls: toolCalls,
|
|
6025
|
+
blocked,
|
|
6026
|
+
response_modification: gateMode
|
|
6027
|
+
};
|
|
6028
|
+
}
|
|
6029
|
+
function buildBlockedCalls(toolCalls, failures, additionalBlocked) {
|
|
6030
|
+
if (failures.length === 0) return additionalBlocked;
|
|
6031
|
+
const failuresByKey = /* @__PURE__ */ new Map();
|
|
6032
|
+
for (const failure of failures) {
|
|
6033
|
+
const fr = failure;
|
|
6034
|
+
let key;
|
|
6035
|
+
let toolName;
|
|
6036
|
+
let args;
|
|
6037
|
+
if (fr._tool_call_id) {
|
|
6038
|
+
key = fr._tool_call_id;
|
|
6039
|
+
const tc = toolCalls.find((c) => c.id === fr._tool_call_id);
|
|
6040
|
+
toolName = tc?.name ?? extractToolNameFromFailure(failure, toolCalls);
|
|
6041
|
+
args = fr._tool_call_arguments ?? tc?.arguments ?? "";
|
|
6042
|
+
} else {
|
|
6043
|
+
toolName = extractToolNameFromFailure(failure, toolCalls);
|
|
6044
|
+
const tc = toolCalls.find((c) => c.name === toolName);
|
|
6045
|
+
key = `name:${toolName}`;
|
|
6046
|
+
args = tc?.arguments ?? "";
|
|
6047
|
+
}
|
|
6048
|
+
const existing = failuresByKey.get(key);
|
|
6049
|
+
if (existing) {
|
|
6050
|
+
existing.failures.push(failure);
|
|
6051
|
+
} else {
|
|
6052
|
+
failuresByKey.set(key, { toolName, arguments: args, failures: [failure] });
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
const blocked = [...additionalBlocked];
|
|
6056
|
+
for (const [, entry] of failuresByKey) {
|
|
6057
|
+
const reason = determineBlockReason(entry.failures);
|
|
6058
|
+
blocked.push({
|
|
6059
|
+
tool_name: entry.toolName,
|
|
6060
|
+
arguments: entry.arguments,
|
|
6061
|
+
reason,
|
|
6062
|
+
contract_file: entry.failures[0]?.contract_file ?? "",
|
|
6063
|
+
failures: entry.failures
|
|
6064
|
+
});
|
|
6065
|
+
}
|
|
6066
|
+
return blocked;
|
|
6067
|
+
}
|
|
6068
|
+
function extractToolNameFromFailure(failure, toolCalls) {
|
|
6069
|
+
const pathMatch = failure.path?.match(/\$\.tool_calls\.(\w+)/);
|
|
6070
|
+
if (pathMatch) {
|
|
6071
|
+
const candidate = pathMatch[1];
|
|
6072
|
+
if (toolCalls.some((tc) => tc.name === candidate)) return candidate;
|
|
6073
|
+
}
|
|
6074
|
+
if (failure.contract_file) {
|
|
6075
|
+
const candidate = toolCalls.find((tc) => failure.contract_file.includes(tc.name));
|
|
6076
|
+
if (candidate) return candidate.name;
|
|
6077
|
+
}
|
|
6078
|
+
return toolCalls[0]?.name ?? "_response";
|
|
6079
|
+
}
|
|
6080
|
+
function determineBlockReason(failures) {
|
|
6081
|
+
for (const f of failures) {
|
|
6082
|
+
if (f.operator === "response_format") return "response_format_invalid";
|
|
6083
|
+
if (f.operator === "contract_match") return "unmatched_tool_blocked";
|
|
6084
|
+
if (f.operator === "precondition_not_met") return "precondition_not_met";
|
|
6085
|
+
if (f.operator === "forbidden_tool") return "forbidden_tool";
|
|
6086
|
+
if (f.operator === "session_limit") return "session_limit_exceeded";
|
|
6087
|
+
if (f.operator === "loop_detected") return "loop_detected";
|
|
6088
|
+
if (f.operator === "policy_denied") return "policy_denied";
|
|
6089
|
+
if (f.operator === "execution_constraint_violated") return "execution_constraint_violated";
|
|
6090
|
+
if (f.operator === "exact_match" || f.operator === "argument_value_mismatch") return "argument_value_mismatch";
|
|
6091
|
+
}
|
|
6092
|
+
return "output_invariant_failed";
|
|
6093
|
+
}
|
|
6094
|
+
function evaluateInputInvariants(request, contracts) {
|
|
6095
|
+
const failures = [];
|
|
6096
|
+
const requestToolNames = extractRequestToolNames(request);
|
|
6097
|
+
const requestToolSet = new Set(requestToolNames);
|
|
6098
|
+
for (const contract of contracts) {
|
|
6099
|
+
if (!requestToolSet.has(contract.tool)) continue;
|
|
6100
|
+
if (contract.assertions.input_invariants.length === 0) continue;
|
|
6101
|
+
const result = (0, import_contracts_core6.evaluateInvariants)(request, contract.assertions.input_invariants, process.env);
|
|
6102
|
+
for (const failure of result) {
|
|
6103
|
+
failures.push({
|
|
6104
|
+
path: failure.path,
|
|
6105
|
+
operator: failure.rule,
|
|
6106
|
+
expected: failure.detail,
|
|
6107
|
+
found: failure.detail,
|
|
6108
|
+
message: failure.detail,
|
|
6109
|
+
contract_file: contract.contract_file ?? contract.tool
|
|
6110
|
+
});
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
return failures;
|
|
6114
|
+
}
|
|
6115
|
+
function extractRequestToolNames(request) {
|
|
6116
|
+
const tools = request.tools;
|
|
6117
|
+
if (!Array.isArray(tools)) return [];
|
|
6118
|
+
return tools.map((tool) => {
|
|
6119
|
+
const record = toRecord10(tool);
|
|
6120
|
+
const name = typeof record.name === "string" ? record.name : typeof toRecord10(record.function).name === "string" ? toRecord10(record.function).name : void 0;
|
|
6121
|
+
return name;
|
|
6122
|
+
}).filter((name) => name !== void 0 && name.length > 0);
|
|
6123
|
+
}
|
|
6124
|
+
function buildContractError2(decision) {
|
|
6125
|
+
if (decision.action !== "block") {
|
|
6126
|
+
throw new Error("Cannot build contract error from allow decision");
|
|
6127
|
+
}
|
|
6128
|
+
const first = decision.blocked[0];
|
|
6129
|
+
return new ReplayContractError(
|
|
6130
|
+
`Tool call blocked: ${first?.tool_name ?? "unknown"} \u2014 ${first?.reason ?? "unknown"}`,
|
|
6131
|
+
decision,
|
|
6132
|
+
first?.contract_file ?? "",
|
|
6133
|
+
first?.failures ?? []
|
|
6134
|
+
);
|
|
6135
|
+
}
|
|
6136
|
+
function buildCompletedStep(stepIndex, sessionId, toolCalls, contracts, response, provider, tier = "strong", compiled) {
|
|
6137
|
+
const contractByTool = new Map(contracts.map((c) => [c.tool, c]));
|
|
6138
|
+
const completedToolCalls = toolCalls.map((tc) => {
|
|
6139
|
+
const contract = contractByTool.get(tc.name);
|
|
6140
|
+
const compiledContract = compiled?.perToolContracts.get(tc.name);
|
|
6141
|
+
let parsedArgs;
|
|
6142
|
+
try {
|
|
6143
|
+
parsedArgs = JSON.parse(tc.arguments);
|
|
6144
|
+
} catch {
|
|
6145
|
+
parsedArgs = void 0;
|
|
6146
|
+
}
|
|
6147
|
+
let resourceValues = null;
|
|
6148
|
+
const preconditions = compiledContract?.preconditions ?? contract?.preconditions;
|
|
6149
|
+
if (preconditions && parsedArgs) {
|
|
6150
|
+
for (const p of preconditions) {
|
|
6151
|
+
if (p.resource) {
|
|
6152
|
+
const resourcePath = typeof p.resource === "string" ? p.resource : p.resource.path;
|
|
6153
|
+
const val = extractResourceValue(parsedArgs, resourcePath);
|
|
6154
|
+
if (val !== void 0) {
|
|
6155
|
+
if (!resourceValues) resourceValues = {};
|
|
6156
|
+
resourceValues[resourcePath] = val;
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
const resolvedCommitRequirement = compiledContract?.commit_requirement ?? contract?.commit_requirement ?? "acknowledged";
|
|
6162
|
+
return {
|
|
6163
|
+
toolName: tc.name,
|
|
6164
|
+
arguments_hash: computeArgumentsHash(tc.arguments),
|
|
6165
|
+
proposal_decision: "allowed",
|
|
6166
|
+
execution_state: "outcome_recorded",
|
|
6167
|
+
evidence_level: "acknowledged",
|
|
6168
|
+
commit_requirement: resolvedCommitRequirement,
|
|
6169
|
+
commit_state: "committed",
|
|
6170
|
+
commit_tier: tier,
|
|
6171
|
+
executor_attested: false,
|
|
6172
|
+
contractFile: contract?.contract_file ?? null,
|
|
6173
|
+
resourceValues,
|
|
6174
|
+
policyVerdict: null,
|
|
6175
|
+
constraintVerdict: null
|
|
6176
|
+
};
|
|
6177
|
+
});
|
|
6178
|
+
const respRec = toRecord10(response);
|
|
6179
|
+
const choices = Array.isArray(respRec.choices) ? respRec.choices : [];
|
|
6180
|
+
const firstChoice = toRecord10(choices[0]);
|
|
6181
|
+
const finish_reason = typeof firstChoice.finish_reason === "string" ? firstChoice.finish_reason : null;
|
|
6182
|
+
const model = typeof respRec.model === "string" ? respRec.model : null;
|
|
6183
|
+
const usageRec = toRecord10(respRec.usage);
|
|
6184
|
+
const usage = typeof usageRec.prompt_tokens === "number" && typeof usageRec.completion_tokens === "number" ? { prompt_tokens: usageRec.prompt_tokens, completion_tokens: usageRec.completion_tokens } : null;
|
|
6185
|
+
return {
|
|
6186
|
+
stepIndex,
|
|
6187
|
+
stepId: `${sessionId}_step_${stepIndex}`,
|
|
6188
|
+
toolCalls: completedToolCalls,
|
|
6189
|
+
proposal_decision: "allowed",
|
|
6190
|
+
commit_state: "committed",
|
|
6191
|
+
commit_tier: tier,
|
|
6192
|
+
max_evidence_level: "acknowledged",
|
|
6193
|
+
invariantFailures: [],
|
|
6194
|
+
phase: null,
|
|
6195
|
+
phaseTransition: null,
|
|
6196
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6197
|
+
outputExtract: {},
|
|
6198
|
+
finish_reason,
|
|
6199
|
+
model,
|
|
6200
|
+
usage
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
function extractResourceValue(args, path) {
|
|
6204
|
+
const cleanPath = path.startsWith("$.") ? path.slice(2) : path;
|
|
6205
|
+
if (cleanPath === "" || cleanPath === "$") return args;
|
|
6206
|
+
const segments = cleanPath.split(".");
|
|
6207
|
+
let current = args;
|
|
6208
|
+
for (const seg of segments) {
|
|
6209
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
6210
|
+
current = current[seg];
|
|
6211
|
+
}
|
|
6212
|
+
return current;
|
|
6213
|
+
}
|
|
6214
|
+
function setAtPath(obj, path, value) {
|
|
6215
|
+
const cleanPath = path.startsWith("$.") ? path.slice(2) : path;
|
|
6216
|
+
if (cleanPath === "" || cleanPath === "$") return;
|
|
6217
|
+
const segments = cleanPath.split(".");
|
|
6218
|
+
let current = obj;
|
|
6219
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
6220
|
+
if (current[segments[i]] === void 0 || current[segments[i]] === null || typeof current[segments[i]] !== "object") {
|
|
6221
|
+
current[segments[i]] = {};
|
|
6222
|
+
}
|
|
6223
|
+
current = current[segments[i]];
|
|
6224
|
+
}
|
|
6225
|
+
current[segments[segments.length - 1]] = value;
|
|
6226
|
+
}
|
|
6227
|
+
function collectWithOutputPaths(toolName, contracts) {
|
|
6228
|
+
const paths = /* @__PURE__ */ new Set();
|
|
6229
|
+
for (const contract of contracts) {
|
|
6230
|
+
if (!contract.preconditions) continue;
|
|
6231
|
+
for (const p of contract.preconditions) {
|
|
6232
|
+
if (p.requires_prior_tool === toolName && p.with_output) {
|
|
6233
|
+
for (const assertion of p.with_output) {
|
|
6234
|
+
paths.add(assertion.path);
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
return Array.from(paths);
|
|
6240
|
+
}
|
|
6241
|
+
function extractOutputFromToolResults(toolResults, state, contracts) {
|
|
6242
|
+
const updates = /* @__PURE__ */ new Map();
|
|
6243
|
+
for (const result of toolResults) {
|
|
6244
|
+
if (result.toolName === "unknown") continue;
|
|
6245
|
+
const paths = collectWithOutputPaths(result.toolName, contracts);
|
|
6246
|
+
if (paths.length === 0) continue;
|
|
6247
|
+
let parsed;
|
|
6248
|
+
try {
|
|
6249
|
+
parsed = typeof result.content === "string" ? JSON.parse(result.content) : result.content;
|
|
6250
|
+
} catch {
|
|
6251
|
+
continue;
|
|
6252
|
+
}
|
|
6253
|
+
if (parsed === null || typeof parsed !== "object") continue;
|
|
6254
|
+
const extract = {};
|
|
6255
|
+
for (const path of paths) {
|
|
6256
|
+
const value = extractResourceValue(parsed, path);
|
|
6257
|
+
if (value !== void 0) {
|
|
6258
|
+
setAtPath(extract, path, value);
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
if (Object.keys(extract).length === 0) continue;
|
|
6262
|
+
let matchingStep;
|
|
6263
|
+
for (let i = state.steps.length - 1; i >= 0; i--) {
|
|
6264
|
+
if (state.steps[i].toolCalls.some((tc) => tc.toolName === result.toolName)) {
|
|
6265
|
+
matchingStep = state.steps[i];
|
|
6266
|
+
break;
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
if (matchingStep) {
|
|
6270
|
+
const existing = updates.get(matchingStep.stepIndex) ?? {};
|
|
6271
|
+
updates.set(matchingStep.stepIndex, { ...existing, ...extract });
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
return updates;
|
|
6275
|
+
}
|
|
6276
|
+
function applyOutputExtracts(state, updates) {
|
|
6277
|
+
if (updates.size === 0) return state;
|
|
6278
|
+
const newSteps = state.steps.map((step) => {
|
|
6279
|
+
const extract = updates.get(step.stepIndex);
|
|
6280
|
+
if (!extract) return step;
|
|
6281
|
+
return { ...step, outputExtract: { ...step.outputExtract, ...extract } };
|
|
6282
|
+
});
|
|
6283
|
+
const newCache = new Map(state.satisfiedPreconditions);
|
|
6284
|
+
for (const step of newSteps) {
|
|
6285
|
+
if (!updates.has(step.stepIndex)) continue;
|
|
6286
|
+
for (const tc of step.toolCalls) {
|
|
6287
|
+
newCache.set(tc.toolName, step.outputExtract);
|
|
6288
|
+
if (tc.resourceValues) {
|
|
6289
|
+
for (const [_path, value] of Object.entries(tc.resourceValues)) {
|
|
6290
|
+
newCache.set(`${tc.toolName}:${JSON.stringify(value)}`, step.outputExtract);
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
const newLastStep = state.lastStep && updates.has(state.lastStep.stepIndex) ? newSteps.find((s) => s.stepIndex === state.lastStep.stepIndex) ?? state.lastStep : state.lastStep;
|
|
6296
|
+
return {
|
|
6297
|
+
...state,
|
|
6298
|
+
steps: newSteps,
|
|
6299
|
+
satisfiedPreconditions: newCache,
|
|
6300
|
+
lastStep: newLastStep
|
|
6301
|
+
};
|
|
6302
|
+
}
|
|
6303
|
+
function resolveSessionLimits(contracts) {
|
|
6304
|
+
for (const c of contracts) {
|
|
6305
|
+
if (c.session_limits) return c.session_limits;
|
|
6306
|
+
}
|
|
6307
|
+
return null;
|
|
6308
|
+
}
|
|
6309
|
+
function buildStateSnapshot(state, lastNarrowing = null) {
|
|
6310
|
+
const lastStep = state.lastStep ? {
|
|
6311
|
+
stepIndex: state.lastStep.stepIndex,
|
|
6312
|
+
stepId: state.lastStep.stepId,
|
|
6313
|
+
toolCalls: state.lastStep.toolCalls.map((tc) => ({
|
|
6314
|
+
toolName: tc.toolName,
|
|
6315
|
+
arguments_hash: tc.arguments_hash,
|
|
6316
|
+
contractFile: tc.contractFile
|
|
6317
|
+
})),
|
|
6318
|
+
invariantFailures: state.lastStep.invariantFailures,
|
|
6319
|
+
phase: state.lastStep.phase,
|
|
6320
|
+
phaseTransition: state.lastStep.phaseTransition,
|
|
6321
|
+
completedAt: state.lastStep.completedAt,
|
|
6322
|
+
outputExtract: {},
|
|
6323
|
+
// FIX-14: Redacted per specs/replay-v2.md § getState()
|
|
6324
|
+
finish_reason: state.lastStep.finish_reason,
|
|
6325
|
+
model: state.lastStep.model
|
|
6326
|
+
} : null;
|
|
6327
|
+
return Object.freeze({
|
|
6328
|
+
sessionId: state.sessionId,
|
|
6329
|
+
agent: state.agent,
|
|
6330
|
+
principal: null,
|
|
6331
|
+
// Redacted — may contain user-scoped identities
|
|
6332
|
+
startedAt: state.startedAt,
|
|
6333
|
+
stateVersion: state.stateVersion,
|
|
6334
|
+
controlRevision: state.controlRevision,
|
|
6335
|
+
currentPhase: state.currentPhase,
|
|
6336
|
+
totalStepCount: state.totalStepCount,
|
|
6337
|
+
totalToolCalls: state.totalToolCalls,
|
|
6338
|
+
totalCost: state.totalCost,
|
|
6339
|
+
actualCost: state.actualCost,
|
|
6340
|
+
toolCallCounts: Object.fromEntries(state.toolCallCounts),
|
|
6341
|
+
forbiddenTools: Array.from(state.forbiddenTools),
|
|
6342
|
+
satisfiedPreconditions: Object.fromEntries(
|
|
6343
|
+
Array.from(state.satisfiedPreconditions.keys()).map((k) => [k, {}])
|
|
6344
|
+
),
|
|
6345
|
+
// FIX-14: Values redacted (contain raw outputExtract)
|
|
6346
|
+
lastStep,
|
|
6347
|
+
lastNarrowing,
|
|
6348
|
+
killed: state.killed,
|
|
6349
|
+
totalUnguardedCalls: state.totalUnguardedCalls,
|
|
6350
|
+
consecutiveBlockCount: state.consecutiveBlockCount,
|
|
6351
|
+
totalBlockCount: state.totalBlockCount
|
|
6352
|
+
});
|
|
6353
|
+
}
|
|
6354
|
+
var EMPTY_STATE_SNAPSHOT = Object.freeze({
|
|
6355
|
+
sessionId: "",
|
|
6356
|
+
agent: null,
|
|
6357
|
+
principal: null,
|
|
6358
|
+
startedAt: /* @__PURE__ */ new Date(0),
|
|
6359
|
+
stateVersion: 0,
|
|
6360
|
+
controlRevision: 0,
|
|
6361
|
+
currentPhase: null,
|
|
6362
|
+
totalStepCount: 0,
|
|
6363
|
+
totalToolCalls: 0,
|
|
6364
|
+
totalCost: 0,
|
|
6365
|
+
actualCost: 0,
|
|
6366
|
+
toolCallCounts: {},
|
|
6367
|
+
forbiddenTools: [],
|
|
6368
|
+
satisfiedPreconditions: {},
|
|
6369
|
+
lastStep: null,
|
|
6370
|
+
lastNarrowing: null,
|
|
6371
|
+
killed: false,
|
|
6372
|
+
totalUnguardedCalls: 0,
|
|
6373
|
+
consecutiveBlockCount: 0,
|
|
6374
|
+
totalBlockCount: 0
|
|
6375
|
+
});
|
|
6376
|
+
function createInactiveSession(client, sessionId, reason) {
|
|
6377
|
+
return {
|
|
6378
|
+
client,
|
|
6379
|
+
flush: () => Promise.resolve({ captured: 0, sent: 0, active: false, errors: [] }),
|
|
6380
|
+
restore() {
|
|
6381
|
+
},
|
|
6382
|
+
kill() {
|
|
6383
|
+
},
|
|
6384
|
+
getHealth: () => ({
|
|
6385
|
+
status: "inactive",
|
|
6386
|
+
authorityState: "inactive",
|
|
6387
|
+
protectionLevel: "monitor",
|
|
6388
|
+
durability: "inactive",
|
|
6389
|
+
tier: "compat",
|
|
6390
|
+
compatEnforcement: "protective",
|
|
6391
|
+
cluster_detected: false,
|
|
6392
|
+
bypass_detected: false,
|
|
6393
|
+
totalSteps: 0,
|
|
6394
|
+
totalBlocks: 0,
|
|
6395
|
+
totalErrors: 0,
|
|
6396
|
+
killed: false,
|
|
6397
|
+
shadowEvaluations: 0
|
|
6398
|
+
}),
|
|
6399
|
+
getState: () => EMPTY_STATE_SNAPSHOT,
|
|
6400
|
+
getLastNarrowing: () => null,
|
|
6401
|
+
getLastShadowDelta: () => null,
|
|
6402
|
+
narrow() {
|
|
6403
|
+
},
|
|
6404
|
+
widen() {
|
|
6405
|
+
},
|
|
6406
|
+
tools: {},
|
|
6407
|
+
getWorkflowState: () => Promise.resolve(null),
|
|
6408
|
+
handoff: () => Promise.resolve(null)
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
function createBlockingInactiveSession(client, sessionId, detail, configError) {
|
|
6412
|
+
const error = configError ?? new ReplayConfigError("compilation_failed", detail);
|
|
6413
|
+
const provider = detectProviderSafe(client);
|
|
6414
|
+
const blockingCreate = () => {
|
|
6415
|
+
throw error;
|
|
6416
|
+
};
|
|
6417
|
+
const wrapperClient = provider ? createWrapperClient(client, provider, blockingCreate) : client;
|
|
6418
|
+
return {
|
|
6419
|
+
client: wrapperClient,
|
|
6420
|
+
flush: () => Promise.resolve({ captured: 0, sent: 0, active: false, errors: [] }),
|
|
6421
|
+
restore() {
|
|
6422
|
+
},
|
|
6423
|
+
kill() {
|
|
6424
|
+
},
|
|
6425
|
+
getHealth: () => ({
|
|
6426
|
+
status: "inactive",
|
|
6427
|
+
authorityState: "inactive",
|
|
6428
|
+
protectionLevel: "monitor",
|
|
6429
|
+
durability: "inactive",
|
|
6430
|
+
tier: "compat",
|
|
6431
|
+
compatEnforcement: "protective",
|
|
6432
|
+
cluster_detected: false,
|
|
6433
|
+
bypass_detected: false,
|
|
6434
|
+
totalSteps: 0,
|
|
6435
|
+
totalBlocks: 0,
|
|
6436
|
+
totalErrors: 0,
|
|
6437
|
+
killed: false,
|
|
6438
|
+
shadowEvaluations: 0
|
|
6439
|
+
}),
|
|
6440
|
+
getState: () => EMPTY_STATE_SNAPSHOT,
|
|
6441
|
+
getLastNarrowing: () => null,
|
|
6442
|
+
getLastShadowDelta: () => null,
|
|
6443
|
+
narrow() {
|
|
6444
|
+
},
|
|
6445
|
+
widen() {
|
|
6446
|
+
},
|
|
6447
|
+
tools: {},
|
|
6448
|
+
getWorkflowState: () => Promise.resolve(null),
|
|
6449
|
+
handoff: () => Promise.resolve(null)
|
|
6450
|
+
};
|
|
6451
|
+
}
|
|
6452
|
+
function toNarrowingSnapshot(result) {
|
|
6453
|
+
if (!result || result.removed.length === 0) return null;
|
|
6454
|
+
return {
|
|
6455
|
+
removed: result.removed.map((r) => ({
|
|
6456
|
+
tool: r.tool,
|
|
6457
|
+
reason: r.reason,
|
|
6458
|
+
...r.detail != null ? { detail: r.detail } : {}
|
|
6459
|
+
})),
|
|
6460
|
+
removedCount: result.removed.length,
|
|
6461
|
+
allowedCount: result.allowed.length
|
|
6462
|
+
};
|
|
6463
|
+
}
|
|
6464
|
+
function buildNarrowingInjectionMessage(narrowResult) {
|
|
6465
|
+
const lines = narrowResult.removed.map((r) => {
|
|
6466
|
+
const reason = r.reason === "policy_denied" ? "restricted" : r.reason;
|
|
6467
|
+
const detail = r.reason === "policy_denied" ? "" : r.detail ? `: ${r.detail}` : "";
|
|
6468
|
+
return `- ${r.tool}: ${reason}${detail}`;
|
|
6469
|
+
});
|
|
6470
|
+
return `[System: The following tools are not available for this request:
|
|
6471
|
+
${lines.join("\n")}
|
|
6472
|
+
Please work with the available tools.]`;
|
|
6473
|
+
}
|
|
6474
|
+
function injectNarrowingSystemMessage(request, message, provider) {
|
|
6475
|
+
if (provider === "openai") {
|
|
6476
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
6477
|
+
request.messages = [{ role: "system", content: message }, ...messages];
|
|
6478
|
+
} else {
|
|
6479
|
+
const existing = request.system;
|
|
6480
|
+
if (typeof existing === "string") {
|
|
6481
|
+
request.system = message + "\n\n" + existing;
|
|
6482
|
+
} else if (Array.isArray(existing)) {
|
|
6483
|
+
request.system = [{ type: "text", text: message }, ...existing];
|
|
6484
|
+
} else {
|
|
6485
|
+
request.system = message;
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6489
|
+
function isObserveWrapped(client) {
|
|
6490
|
+
return Boolean(client[OBSERVE_WRAPPED]);
|
|
6491
|
+
}
|
|
6492
|
+
function isReplayAttached2(client) {
|
|
6493
|
+
return Boolean(client[REPLAY_ATTACHED2]);
|
|
6494
|
+
}
|
|
6495
|
+
function setReplayAttached(client) {
|
|
6496
|
+
try {
|
|
6497
|
+
client[REPLAY_ATTACHED2] = true;
|
|
6498
|
+
} catch {
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
function clearReplayAttached(client) {
|
|
6502
|
+
try {
|
|
6503
|
+
delete client[REPLAY_ATTACHED2];
|
|
6504
|
+
} catch {
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
function detectProviderSafe(client) {
|
|
6508
|
+
try {
|
|
6509
|
+
return detectProvider(client);
|
|
6510
|
+
} catch {
|
|
6511
|
+
return null;
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
function resolveApiKey2(opts) {
|
|
6515
|
+
if (typeof opts.apiKey === "string" && opts.apiKey.length > 0) {
|
|
6516
|
+
return opts.apiKey;
|
|
6517
|
+
}
|
|
6518
|
+
const envKey = typeof process !== "undefined" ? process.env.REPLAYCI_API_KEY : void 0;
|
|
6519
|
+
return typeof envKey === "string" && envKey.length > 0 ? envKey : void 0;
|
|
6520
|
+
}
|
|
6521
|
+
function generateSessionId2() {
|
|
6522
|
+
return `rs_${import_node_crypto5.default.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
6523
|
+
}
|
|
6524
|
+
function stripHashPrefix(hash) {
|
|
6525
|
+
return hash.startsWith("sha256:") ? hash.slice(7) : hash;
|
|
6526
|
+
}
|
|
6527
|
+
function emitDiagnostic2(diagnostics, event) {
|
|
6528
|
+
try {
|
|
6529
|
+
diagnostics?.(event);
|
|
6530
|
+
} catch {
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
function toRecord10(value) {
|
|
6534
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
6535
|
+
}
|
|
6536
|
+
function determineProtectionLevel(mode, tools, contracts) {
|
|
6537
|
+
if (mode === "shadow" || mode === "log-only") return "monitor";
|
|
6538
|
+
if (!tools || Object.keys(tools).length === 0) return "protect";
|
|
6539
|
+
const stateBearingTools = contracts.filter(isStateBearing);
|
|
6540
|
+
if (stateBearingTools.length === 0) return "protect";
|
|
6541
|
+
const wrappedTools = new Set(Object.keys(tools));
|
|
6542
|
+
const allWrapped = stateBearingTools.every((c) => wrappedTools.has(c.tool));
|
|
6543
|
+
return allWrapped ? "govern" : "protect";
|
|
6544
|
+
}
|
|
6545
|
+
function isStateBearing(contract) {
|
|
6546
|
+
if (contract.commit_requirement != null && contract.commit_requirement !== "none") return true;
|
|
6547
|
+
if (contract.transitions != null) return true;
|
|
6548
|
+
if (contract.execution_constraints != null) return true;
|
|
6549
|
+
if (contract.forbids_after != null && contract.forbids_after.length > 0) return true;
|
|
6550
|
+
return false;
|
|
6551
|
+
}
|
|
6552
|
+
function deriveRuntimeRequest(protectionLevel, mode) {
|
|
6553
|
+
if (protectionLevel === "govern") {
|
|
6554
|
+
return { requestedMode: "authoritative", requestedTier: "strong" };
|
|
6555
|
+
}
|
|
6556
|
+
if (protectionLevel === "protect" && mode === "enforce") {
|
|
6557
|
+
return { requestedMode: "advisory", requestedTier: "compat" };
|
|
6558
|
+
}
|
|
6559
|
+
return { requestedMode: "advisory", requestedTier: "compat" };
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
// src/memoryStore.ts
|
|
6563
|
+
var MemoryStore = class {
|
|
6564
|
+
state = null;
|
|
6565
|
+
captures = [];
|
|
6566
|
+
compareAndSet(currentVersion, newState) {
|
|
6567
|
+
if (this.state === null) {
|
|
6568
|
+
this.state = newState;
|
|
6569
|
+
return { success: true };
|
|
6570
|
+
}
|
|
6571
|
+
if (this.state.stateVersion !== currentVersion) {
|
|
6572
|
+
return { success: false };
|
|
6573
|
+
}
|
|
6574
|
+
this.state = newState;
|
|
6575
|
+
return { success: true };
|
|
6576
|
+
}
|
|
6577
|
+
load() {
|
|
6578
|
+
return this.state;
|
|
6579
|
+
}
|
|
6580
|
+
appendCapture(capture) {
|
|
6581
|
+
this.captures.push(capture);
|
|
6582
|
+
}
|
|
6583
|
+
/** @internal Test-only: get all captured calls. */
|
|
6584
|
+
getCapturedCalls() {
|
|
6585
|
+
return this.captures;
|
|
6586
|
+
}
|
|
6587
|
+
};
|
|
2816
6588
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2817
6589
|
0 && (module.exports = {
|
|
6590
|
+
MemoryStore,
|
|
6591
|
+
ReplayConfigError,
|
|
6592
|
+
ReplayContractError,
|
|
6593
|
+
ReplayKillError,
|
|
6594
|
+
RuntimeClient,
|
|
6595
|
+
RuntimeClientError,
|
|
6596
|
+
createRuntimeClient,
|
|
2818
6597
|
observe,
|
|
2819
6598
|
prepareContracts,
|
|
6599
|
+
replay,
|
|
2820
6600
|
validate
|
|
2821
6601
|
});
|