@atbash/sdk 0.22.0-dev.0 → 0.23.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/browser.d.mts +100 -10
- package/dist/browser.mjs +334 -58
- package/dist/index.d.mts +100 -10
- package/dist/index.d.ts +100 -10
- package/dist/index.js +205 -59
- package/dist/index.mjs +203 -59
- package/dist/testing.d.mts +15 -0
- package/dist/testing.d.ts +15 -0
- package/dist/testing.js +33 -0
- package/dist/testing.mjs +8 -0
- package/index.d.ts +3 -0
- package/index.js +55 -54
- package/package.json +15 -7
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ __export(src_ts_exports, {
|
|
|
89
89
|
isEnvelope: () => isEnvelope,
|
|
90
90
|
isJailHeld: () => isJailHeld,
|
|
91
91
|
isValidPrivateKey: () => isValidPrivateKey,
|
|
92
|
+
judgeVerifyKeys: () => judgeVerifyKeys,
|
|
92
93
|
keyFingerprintOf: () => keyFingerprintOf,
|
|
93
94
|
keyPathCandidates: () => keyPathCandidates,
|
|
94
95
|
loadAgent: () => loadAgent,
|
|
@@ -115,6 +116,7 @@ __export(src_ts_exports, {
|
|
|
115
116
|
saveUserConfig: () => saveUserConfig,
|
|
116
117
|
scanMemory: () => scanMemory,
|
|
117
118
|
scanMemoryBatch: () => scanMemoryBatch,
|
|
119
|
+
setLoopbackJudgePinsForTests: () => setLoopbackJudgePinsForTests,
|
|
118
120
|
setupTelemetry: () => setupTelemetry,
|
|
119
121
|
shutdownTelemetry: () => shutdownTelemetry,
|
|
120
122
|
signEncryptedToolCall: () => signEncryptedToolCall,
|
|
@@ -233,6 +235,23 @@ function isAllowlistedJudgeHost(endpoint) {
|
|
|
233
235
|
return false;
|
|
234
236
|
}
|
|
235
237
|
}
|
|
238
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
239
|
+
var loopbackJudgePins = [];
|
|
240
|
+
function setLoopbackJudgePinsForTests(keys) {
|
|
241
|
+
loopbackJudgePins = keys.map((k) => k.trim().toLowerCase());
|
|
242
|
+
}
|
|
243
|
+
function judgeVerifyKeys(endpoint, configuredKey) {
|
|
244
|
+
const host = new URL(endpoint).hostname.toLowerCase();
|
|
245
|
+
const pins = LOOPBACK_HOSTS.has(host) ? loopbackJudgePins : native.pinnedJudgeVerifyKeys(host);
|
|
246
|
+
const key = configuredKey?.trim().toLowerCase();
|
|
247
|
+
if (pins.length === 0) return key ? [key] : [];
|
|
248
|
+
if (key && !pins.includes(key)) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`[atbash] verifyPubKey ${key} is not the response-signing key pinned for ${host}. Refusing to load \u2014 an Atbash host's replies are checked against the key built into the SDK; remove verifyPubKey, or set it to the pinned key.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return pins;
|
|
254
|
+
}
|
|
236
255
|
function validateJudgeEndpoint(judge) {
|
|
237
256
|
const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
|
|
238
257
|
const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
|
|
@@ -347,7 +366,7 @@ function isJailHeld(pubkey) {
|
|
|
347
366
|
return holds.has(pubkey);
|
|
348
367
|
}
|
|
349
368
|
function recordBlock(pubkey) {
|
|
350
|
-
const now =
|
|
369
|
+
const now = performance.now();
|
|
351
370
|
for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
|
|
352
371
|
blockedAt.set(pubkey, now);
|
|
353
372
|
}
|
|
@@ -736,7 +755,7 @@ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
|
736
755
|
var import_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
|
|
737
756
|
var import_sdk_logs = require("@opentelemetry/sdk-logs");
|
|
738
757
|
var import_node_crypto = require("crypto");
|
|
739
|
-
var SDK_VERSION = true ? "0.
|
|
758
|
+
var SDK_VERSION = true ? "0.23.0-dev.0" : "";
|
|
740
759
|
var SDK_LANGUAGE = "node";
|
|
741
760
|
function sdkResourceAttributes() {
|
|
742
761
|
const attrs = {
|
|
@@ -1001,7 +1020,10 @@ function forcedChainNetwork(flagValue) {
|
|
|
1001
1020
|
|
|
1002
1021
|
// src-ts/client.ts
|
|
1003
1022
|
var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
|
|
1023
|
+
var TOOL_CALL_IDS_FOR_TESTS = /* @__PURE__ */ Symbol.for("@atbash/sdk.toolCallIdsForTests");
|
|
1004
1024
|
function generateToolCallId() {
|
|
1025
|
+
const fixed = globalThis[TOOL_CALL_IDS_FOR_TESTS];
|
|
1026
|
+
if (fixed && fixed.ids.length > 0) return fixed.ids[fixed.next++ % fixed.ids.length];
|
|
1005
1027
|
return `tc-${Date.now()}-${randomHex(4)}`;
|
|
1006
1028
|
}
|
|
1007
1029
|
function resolveConstructorChain(options) {
|
|
@@ -1038,12 +1060,21 @@ var Atbash = class _Atbash {
|
|
|
1038
1060
|
orgName;
|
|
1039
1061
|
/** Default judge response-signing pubkey, if configured (see fromConfig). */
|
|
1040
1062
|
verifyPubKey;
|
|
1063
|
+
/**
|
|
1064
|
+
* The keys every judge reply must be signed by: the host's pinned keys, or
|
|
1065
|
+
* the configured `verifyPubKey` on a host with none. Empty means unchecked.
|
|
1066
|
+
*/
|
|
1067
|
+
verifyKeys;
|
|
1041
1068
|
/** Default org encryption key — see {@link AtbashOptions.orgEncryptionPubKey}. */
|
|
1042
1069
|
orgEncryptionPubKey;
|
|
1043
1070
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
1044
1071
|
failClosed;
|
|
1045
1072
|
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
1046
1073
|
requireEnforcement;
|
|
1074
|
+
/** When true, a signed non-enforcing mode never relaxes a verdict. */
|
|
1075
|
+
enforceLocally;
|
|
1076
|
+
/** Results whose enforcement mode came signed and bound to their call. */
|
|
1077
|
+
modeTrusted = /* @__PURE__ */ new WeakSet();
|
|
1047
1078
|
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
1048
1079
|
decisionTimeoutMs;
|
|
1049
1080
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
@@ -1107,9 +1138,11 @@ var Atbash = class _Atbash {
|
|
|
1107
1138
|
this._forcedNetwork = forcedChainNetwork();
|
|
1108
1139
|
this.orgName = options.orgName;
|
|
1109
1140
|
this.verifyPubKey = validated.verifyPubKey ?? void 0;
|
|
1141
|
+
this.verifyKeys = judgeVerifyKeys(this.endpoint, this.verifyPubKey);
|
|
1110
1142
|
this.orgEncryptionPubKey = options.orgEncryptionPubKey;
|
|
1111
1143
|
this.failClosed = options.failClosed !== false;
|
|
1112
1144
|
this.requireEnforcement = options.requireEnforcement !== false;
|
|
1145
|
+
this.enforceLocally = options.enforceLocally === true;
|
|
1113
1146
|
this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
|
|
1114
1147
|
this.debug = options.debug === true;
|
|
1115
1148
|
this.logger = options.logger ?? {};
|
|
@@ -1118,7 +1151,7 @@ var Atbash = class _Atbash {
|
|
|
1118
1151
|
if (this.endpoint !== DEFAULT_ENDPOINT) {
|
|
1119
1152
|
this.logger.warn?.("[atbash] running on non-default judge endpoint", {
|
|
1120
1153
|
endpoint: this.endpoint,
|
|
1121
|
-
verifying: this.
|
|
1154
|
+
verifying: this.verifyKeys.length > 0 ? "with response-signature pubkey configured" : "without signature verification"
|
|
1122
1155
|
});
|
|
1123
1156
|
}
|
|
1124
1157
|
if (isAllowlistedJudgeHost(this.endpoint)) {
|
|
@@ -1156,12 +1189,12 @@ var Atbash = class _Atbash {
|
|
|
1156
1189
|
_Atbash.environmentLogged = true;
|
|
1157
1190
|
const brief = (rid) => rid ? `${rid.slice(0, 8)}\u2026` : "(unset)";
|
|
1158
1191
|
this.logger.info?.(
|
|
1159
|
-
`[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.
|
|
1192
|
+
`[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyKeys.length > 0 ? "on" : "off"}`,
|
|
1160
1193
|
{
|
|
1161
1194
|
judgeEndpoint: this.endpoint,
|
|
1162
1195
|
privateBlockchainRid: native.DEFAULT_PRIVATE_BLOCKCHAIN_RID,
|
|
1163
1196
|
activeBlockchainRid: this.blockchainRid,
|
|
1164
|
-
responseSignatureCheck:
|
|
1197
|
+
responseSignatureCheck: this.verifyKeys.length > 0
|
|
1165
1198
|
}
|
|
1166
1199
|
);
|
|
1167
1200
|
}
|
|
@@ -1279,6 +1312,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1279
1312
|
verifyPubKey: validated.verifyPubKey ?? void 0,
|
|
1280
1313
|
failClosed: options.failClosed,
|
|
1281
1314
|
requireEnforcement: options.requireEnforcement,
|
|
1315
|
+
enforceLocally: options.enforceLocally,
|
|
1282
1316
|
decisionTimeoutMs: options.decisionTimeoutMs,
|
|
1283
1317
|
// ATBASH_DEBUG lets an operator turn diagnostics on without editing a
|
|
1284
1318
|
// host's plugin config, which is usually the harder half.
|
|
@@ -1413,22 +1447,34 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1413
1447
|
* response bytes via the Rust core's `verifySignature`.
|
|
1414
1448
|
*/
|
|
1415
1449
|
async judgeAction(action, context = "", options = {}) {
|
|
1450
|
+
return this.judgeTracked(action, context, options, false);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* judgeAction, optionally asking the judge for the true verdict and the
|
|
1454
|
+
* organisation's enforcement mode so this client applies the mode itself.
|
|
1455
|
+
*
|
|
1456
|
+
* Only auditToolCall opts in: judgeAction's callers keep the server-side
|
|
1457
|
+
* downgrade they gate on today.
|
|
1458
|
+
*
|
|
1459
|
+
* @param clientEnforces - Send `X-Atbash-Client-Enforcement` when the reply can be trusted.
|
|
1460
|
+
*/
|
|
1461
|
+
judgeTracked(action, context, options, clientEnforces) {
|
|
1416
1462
|
return this.track("judgeAction", this.auth.pubkey, async () => {
|
|
1417
1463
|
try {
|
|
1418
|
-
return await this._judgeAction(action, context, options);
|
|
1464
|
+
return await this._judgeAction(action, context, options, clientEnforces);
|
|
1419
1465
|
} catch (err) {
|
|
1420
1466
|
if (!isEncryptionStateMismatch(err)) throw err;
|
|
1421
1467
|
this._orgKeyFromChain = null;
|
|
1422
|
-
return await this._judgeAction(action, context, options);
|
|
1468
|
+
return await this._judgeAction(action, context, options, clientEnforces);
|
|
1423
1469
|
}
|
|
1424
1470
|
});
|
|
1425
1471
|
}
|
|
1426
|
-
async _judgeAction(action, context, options) {
|
|
1472
|
+
async _judgeAction(action, context, options, clientEnforces = false) {
|
|
1427
1473
|
if (!action?.trim()) {
|
|
1428
1474
|
throw new Error("action is required and cannot be empty.");
|
|
1429
1475
|
}
|
|
1430
1476
|
if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
|
|
1431
|
-
const startedAt =
|
|
1477
|
+
const startedAt = performance.now();
|
|
1432
1478
|
let chainOpts = options.chainOpts;
|
|
1433
1479
|
if (options.orgName && this._explicitChain) {
|
|
1434
1480
|
chainOpts = options.chainOpts ?? {
|
|
@@ -1485,17 +1531,23 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1485
1531
|
if (options.toolArgsJson) body.tool_args_json = options.toolArgsJson;
|
|
1486
1532
|
if (options.resolved) body.resolved = options.resolved;
|
|
1487
1533
|
if (options.mode) body.mode = options.mode;
|
|
1534
|
+
const verifyKeys = this.effectiveVerifyKeys(options.verifyPubKey);
|
|
1535
|
+
const optIn = clientEnforces && (verifyKeys.length > 0 || this.enforceLocally);
|
|
1536
|
+
const optInValue = this.enforceLocally ? "strict" : "1";
|
|
1488
1537
|
let resp;
|
|
1489
1538
|
try {
|
|
1490
|
-
resp = await this.http.post(
|
|
1539
|
+
resp = await this.http.post(
|
|
1540
|
+
"/api/v1/judge",
|
|
1541
|
+
body,
|
|
1542
|
+
optIn ? { "X-Atbash-Client-Enforcement": optInValue } : void 0
|
|
1543
|
+
);
|
|
1491
1544
|
} catch (err) {
|
|
1492
1545
|
throw this.transportError(err);
|
|
1493
1546
|
}
|
|
1494
1547
|
if (!resp.ok) throw await this.httpError(resp);
|
|
1495
1548
|
const bodyBytes = Buffer.from(await resp.arrayBuffer());
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
verifyResponseSignature(bodyBytes, resp, verifyPubKey);
|
|
1549
|
+
if (verifyKeys.length > 0) {
|
|
1550
|
+
verifyResponseSignature(bodyBytes, resp, verifyKeys);
|
|
1499
1551
|
}
|
|
1500
1552
|
const parsed = tryParseJson(bodyBytes.toString("utf-8"));
|
|
1501
1553
|
if (!isRecord2(parsed)) {
|
|
@@ -1515,6 +1567,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1515
1567
|
provider: wireText(data.provider),
|
|
1516
1568
|
latencyMs: wireNumber(data.latency_ms),
|
|
1517
1569
|
toolCallId: wireText(data.tool_call_id, logResult.toolCallId),
|
|
1570
|
+
...wireText(data.judgment_id) ? { judgmentId: wireText(data.judgment_id) } : {},
|
|
1518
1571
|
onChain: Boolean(data.on_chain),
|
|
1519
1572
|
enforced: Boolean(data.enforced),
|
|
1520
1573
|
enforcementMode: wireText(data.enforcement_mode),
|
|
@@ -1522,23 +1575,46 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1522
1575
|
score,
|
|
1523
1576
|
refusal: explicitRefusal(data)
|
|
1524
1577
|
};
|
|
1525
|
-
|
|
1578
|
+
if (optIn) {
|
|
1579
|
+
const echo = data.client_enforcement;
|
|
1580
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1581
|
+
throw new SignatureVerificationError(
|
|
1582
|
+
"the reply does not confirm the enforcement this client asked for"
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
if (verifyKeys.length > 0) {
|
|
1586
|
+
const statedId = wireText(data.tool_call_id);
|
|
1587
|
+
const statedJudgment = wireText(data.judgment_id);
|
|
1588
|
+
if (statedId && statedId !== logResult.toolCallId && statedId !== judgmentId || statedJudgment && statedJudgment !== judgmentId) {
|
|
1589
|
+
throw new SignatureVerificationError(
|
|
1590
|
+
`the signed reply is for call ${statedJudgment || statedId}, not this one`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (statedId && echo === optInValue) this.modeTrusted.add(result);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return this.applyJailHold(result, startedAt, wireText(data.on_chain_status));
|
|
1526
1597
|
}
|
|
1527
1598
|
/**
|
|
1528
|
-
* Start a local hold on an enforced
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1599
|
+
* Start a local hold on an enforced BLOCK that jails: one already on chain,
|
|
1600
|
+
* or one the judge answered before its chain write landed
|
|
1601
|
+
* (`on_chain_status: "pending"`), which jails once it lands. Refuse any other
|
|
1602
|
+
* result that lands while the agent is held, or whose request was already in
|
|
1603
|
+
* flight when the block arrived: the server judged it on pre-jail state.
|
|
1604
|
+
*
|
|
1605
|
+
* @param onChainStatus - The reply's `on_chain_status`; empty when absent.
|
|
1532
1606
|
*/
|
|
1533
|
-
applyJailHold(result, startedAt) {
|
|
1607
|
+
applyJailHold(result, startedAt, onChainStatus = "") {
|
|
1534
1608
|
const pubkey = this.auth.pubkey;
|
|
1535
|
-
if (result.verdict === "BLOCK" && result.enforced
|
|
1609
|
+
if (result.verdict === "BLOCK" && result.enforced) {
|
|
1536
1610
|
recordBlock(pubkey);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1611
|
+
if (result.onChain || onChainStatus === "pending") {
|
|
1612
|
+
holdJail(
|
|
1613
|
+
pubkey,
|
|
1614
|
+
() => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
|
|
1615
|
+
() => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1542
1618
|
return result;
|
|
1543
1619
|
}
|
|
1544
1620
|
return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
|
|
@@ -1593,13 +1669,18 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1593
1669
|
try {
|
|
1594
1670
|
this.logger.info?.("[atbash] judge API called", { tool: toolName });
|
|
1595
1671
|
const result = await withDeadline(
|
|
1596
|
-
this.
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1672
|
+
this.judgeTracked(
|
|
1673
|
+
actionText,
|
|
1674
|
+
contextText,
|
|
1675
|
+
{
|
|
1676
|
+
toolName,
|
|
1677
|
+
toolArgsJson: argsJson,
|
|
1678
|
+
orgName: this.orgName,
|
|
1679
|
+
// Passed through unredacted — caller vouches for the contents.
|
|
1680
|
+
resolved: input.resolved
|
|
1681
|
+
},
|
|
1682
|
+
true
|
|
1683
|
+
),
|
|
1603
1684
|
this.decisionTimeoutMs
|
|
1604
1685
|
);
|
|
1605
1686
|
return this.decide(result);
|
|
@@ -1609,7 +1690,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1609
1690
|
return {
|
|
1610
1691
|
allow: false,
|
|
1611
1692
|
verdict: "ERROR",
|
|
1612
|
-
reason: `The judge's reply
|
|
1693
|
+
reason: `The judge's reply was not trusted (${errorMessage(err)}).`
|
|
1613
1694
|
};
|
|
1614
1695
|
}
|
|
1615
1696
|
if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
|
|
@@ -1619,20 +1700,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1619
1700
|
}
|
|
1620
1701
|
}
|
|
1621
1702
|
/**
|
|
1622
|
-
* The verify
|
|
1623
|
-
* may only narrow what the client accepts: on a client pinned to
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1703
|
+
* The verify keys for one call (R4, governor tier-2 review). A per-call key
|
|
1704
|
+
* may only narrow what the client accepts: on a client pinned to judge keys
|
|
1705
|
+
* it must be one of them, never a replacement; on an unpinned client it
|
|
1706
|
+
* adds the check.
|
|
1626
1707
|
*/
|
|
1627
|
-
|
|
1628
|
-
if (perCall === void 0) return this.
|
|
1708
|
+
effectiveVerifyKeys(perCall) {
|
|
1709
|
+
if (perCall === void 0) return this.verifyKeys;
|
|
1629
1710
|
const key = perCall.trim().toLowerCase();
|
|
1630
|
-
if (this.
|
|
1711
|
+
if (this.verifyKeys.length > 0 && !this.verifyKeys.includes(key)) {
|
|
1631
1712
|
throw new SignatureVerificationError(
|
|
1632
1713
|
"a per-call verifyPubKey cannot replace the judge key this client is pinned to"
|
|
1633
1714
|
);
|
|
1634
1715
|
}
|
|
1635
|
-
return key;
|
|
1716
|
+
return [key];
|
|
1636
1717
|
}
|
|
1637
1718
|
/**
|
|
1638
1719
|
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
@@ -1644,7 +1725,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1644
1725
|
* never overrides a refusal.
|
|
1645
1726
|
*/
|
|
1646
1727
|
decide(result) {
|
|
1647
|
-
const
|
|
1728
|
+
const decided = this.applyMode(result);
|
|
1729
|
+
const inner = result.judgmentId ? { ...decided, judgmentId: result.judgmentId } : decided;
|
|
1648
1730
|
if (!result.enforcementMode) return inner;
|
|
1649
1731
|
return {
|
|
1650
1732
|
...inner,
|
|
@@ -1652,6 +1734,50 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1652
1734
|
enforcementMode: result.enforcementMode
|
|
1653
1735
|
};
|
|
1654
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Apply the organisation's enforcement mode to the judge's true verdict.
|
|
1739
|
+
*
|
|
1740
|
+
* Only a mode that arrived signed and bound to this call counts; a missing,
|
|
1741
|
+
* unsigned or unknown mode, or enforceLocally, means enforce. Under
|
|
1742
|
+
* `shadow`, `monitor` or `off` a BLOCK or HOLD runs, reported in
|
|
1743
|
+
* `wouldVerdict`. An ERROR stays an error: it is not a verdict.
|
|
1744
|
+
*/
|
|
1745
|
+
applyMode(result) {
|
|
1746
|
+
const mode = result.enforcementMode;
|
|
1747
|
+
const relaxes = this.modeTrusted.has(result) && (mode === "shadow" || mode === "monitor" || mode === "off");
|
|
1748
|
+
if (result.status === "shadow_recorded" && result.verdict === "No verdict") {
|
|
1749
|
+
if (!relaxes || this.enforceLocally) {
|
|
1750
|
+
return {
|
|
1751
|
+
allow: false,
|
|
1752
|
+
verdict: "ERROR",
|
|
1753
|
+
reason: "Atbash recorded this action without judging it (the organisation is not enforcing), and this client requires a verdict.",
|
|
1754
|
+
toolCallId: result.toolCallId
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
this.logger.info?.(`[atbash] recorded in ${mode} mode; not judged yet`, {
|
|
1758
|
+
toolCallId: result.toolCallId
|
|
1759
|
+
});
|
|
1760
|
+
return {
|
|
1761
|
+
allow: true,
|
|
1762
|
+
verdict: "ALLOW",
|
|
1763
|
+
reason: `Recorded in ${mode} mode; the verdict appears in the dashboard.`,
|
|
1764
|
+
toolCallId: result.toolCallId
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
const inner = this.decideInner(result);
|
|
1768
|
+
if (!relaxes || this.enforceLocally || inner.allow) return inner;
|
|
1769
|
+
if (inner.verdict !== "BLOCK" && inner.verdict !== "HOLD") return inner;
|
|
1770
|
+
this.logger.warn?.(`[atbash] would ${inner.verdict.toLowerCase()} (${mode} mode): ${inner.reason ?? ""}`, {
|
|
1771
|
+
toolCallId: inner.toolCallId
|
|
1772
|
+
});
|
|
1773
|
+
return {
|
|
1774
|
+
allow: true,
|
|
1775
|
+
verdict: "ALLOW",
|
|
1776
|
+
reason: inner.reason,
|
|
1777
|
+
toolCallId: inner.toolCallId,
|
|
1778
|
+
wouldVerdict: inner.verdict
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1655
1781
|
decideInner(result) {
|
|
1656
1782
|
const toolCallId = result.toolCallId;
|
|
1657
1783
|
const deny = (verdict, reason) => ({
|
|
@@ -1687,7 +1813,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1687
1813
|
return {
|
|
1688
1814
|
allow: false,
|
|
1689
1815
|
verdict: "ERROR",
|
|
1690
|
-
|
|
1816
|
+
// The judge's own reason says why it did not check (the plan, a
|
|
1817
|
+
// deactivated agent); a fixed "audit-tier plan" hid which.
|
|
1818
|
+
reason: result.reason ? `Atbash did not check this action, and this client requires a checked decision. The judge said: ${result.reason}` : "Atbash logged this action but did not check it (audit-tier plan), and this client requires a checked decision. Upgrade the plan, or set requireEnforcement: false for log-only mode.",
|
|
1691
1819
|
toolCallId
|
|
1692
1820
|
};
|
|
1693
1821
|
}
|
|
@@ -1777,32 +1905,52 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1777
1905
|
const pk = agentPubkey ?? this.auth.pubkey;
|
|
1778
1906
|
return this.track("getJudgmentStatus", pk, async () => {
|
|
1779
1907
|
const brid = await this.callBrid(chainOpts);
|
|
1908
|
+
const optIn = this.verifyKeys.length > 0 || this.enforceLocally;
|
|
1909
|
+
const optInValue = this.enforceLocally ? "strict" : "1";
|
|
1780
1910
|
const resp = await this.http.get(
|
|
1781
1911
|
"/api/v1/judge",
|
|
1782
1912
|
{ tool_call_id: judgmentId, agent_pubkey: pk, brid },
|
|
1783
|
-
|
|
1913
|
+
{
|
|
1914
|
+
...this.authHeaders(brid),
|
|
1915
|
+
...optIn ? { "X-Atbash-Client-Enforcement": optInValue } : {}
|
|
1916
|
+
}
|
|
1784
1917
|
);
|
|
1785
1918
|
await this.raiseIfError(resp);
|
|
1786
1919
|
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
1787
|
-
if (this.
|
|
1788
|
-
verifyResponseSignature(bytes, resp, this.
|
|
1920
|
+
if (this.verifyKeys.length > 0) {
|
|
1921
|
+
verifyResponseSignature(bytes, resp, this.verifyKeys);
|
|
1789
1922
|
}
|
|
1790
1923
|
const parsed = tryParseJson(bytes.toString("utf-8"));
|
|
1791
1924
|
const data = isRecord2(parsed) ? parsed : {};
|
|
1792
|
-
const rawId = data.tool_call_id
|
|
1925
|
+
const rawId = data.tool_call_id;
|
|
1793
1926
|
const statedId = wireText(rawId);
|
|
1794
1927
|
const sameCall = rawId === void 0 || rawId === null || statedId === judgmentId;
|
|
1795
|
-
|
|
1928
|
+
let modeTrusted = false;
|
|
1929
|
+
if (optIn) {
|
|
1930
|
+
const echo = data.client_enforcement;
|
|
1931
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1932
|
+
throw new SignatureVerificationError(
|
|
1933
|
+
"the status does not confirm the enforcement this client asked for"
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
modeTrusted = this.verifyKeys.length > 0 && echo === optInValue && statedId === judgmentId;
|
|
1937
|
+
}
|
|
1938
|
+
const status = {
|
|
1796
1939
|
status: normalizeStatus(data.status),
|
|
1797
1940
|
verdict: normalizeVerdict(data.verdict),
|
|
1798
1941
|
allow: sameCall && this.executable(data),
|
|
1799
1942
|
reason: wireText(data.reason),
|
|
1800
1943
|
judgmentId: statedId || judgmentId,
|
|
1801
|
-
|
|
1802
|
-
cached: optBool(data.cached),
|
|
1803
|
-
// The Dashboard sends response_time_ms; responseTimeMs is the spec name.
|
|
1804
|
-
responseTimeMs: optNumber(data.responseTimeMs ?? data.response_time_ms)
|
|
1944
|
+
responseTimeMs: optNumber(data.response_time_ms)
|
|
1805
1945
|
};
|
|
1946
|
+
const mode = wireText(data.enforcement_mode);
|
|
1947
|
+
if (!modeTrusted || this.enforceLocally || !(mode === "shadow" || mode === "monitor" || mode === "off") || status.verdict !== "BLOCK" && status.verdict !== "HOLD") {
|
|
1948
|
+
return status;
|
|
1949
|
+
}
|
|
1950
|
+
this.logger.warn?.(`[atbash] would ${status.verdict.toLowerCase()} (${mode} mode): ${status.reason}`, {
|
|
1951
|
+
toolCallId: judgmentId
|
|
1952
|
+
});
|
|
1953
|
+
return { ...status, verdict: "ALLOW", allow: true, wouldVerdict: status.verdict };
|
|
1806
1954
|
});
|
|
1807
1955
|
}
|
|
1808
1956
|
/* ── risk-engine queries (action-dispatched GET) ───────────────────────── */
|
|
@@ -2304,14 +2452,14 @@ async function withDeadline(p, ms) {
|
|
|
2304
2452
|
clearTimeout(timer);
|
|
2305
2453
|
}
|
|
2306
2454
|
}
|
|
2307
|
-
function verifyResponseSignature(bodyBytes, resp,
|
|
2455
|
+
function verifyResponseSignature(bodyBytes, resp, verifyKeys) {
|
|
2308
2456
|
const sig = resp.headers.get("X-Atbash-Signature");
|
|
2309
2457
|
if (!sig) {
|
|
2310
2458
|
throw new SignatureVerificationError("missing X-Atbash-Signature header");
|
|
2311
2459
|
}
|
|
2312
|
-
let ok;
|
|
2460
|
+
let ok = false;
|
|
2313
2461
|
try {
|
|
2314
|
-
ok = native.verifySignature(bodyBytes, sig,
|
|
2462
|
+
ok = verifyKeys.some((key) => native.verifySignature(bodyBytes, sig, key));
|
|
2315
2463
|
} catch (err) {
|
|
2316
2464
|
throw new SignatureVerificationError(
|
|
2317
2465
|
`signature verification threw: ${errorMessage(err)}`
|
|
@@ -2403,10 +2551,6 @@ function optNumber(v) {
|
|
|
2403
2551
|
const n = Number(v);
|
|
2404
2552
|
return Number.isFinite(n) ? n : void 0;
|
|
2405
2553
|
}
|
|
2406
|
-
function optBool(v) {
|
|
2407
|
-
if (v === null || v === void 0) return void 0;
|
|
2408
|
-
return Boolean(v);
|
|
2409
|
-
}
|
|
2410
2554
|
function tryParseJson(text) {
|
|
2411
2555
|
try {
|
|
2412
2556
|
return JSON.parse(text);
|
|
@@ -4086,6 +4230,7 @@ function diffMemorySnapshots(before, after) {
|
|
|
4086
4230
|
isEnvelope,
|
|
4087
4231
|
isJailHeld,
|
|
4088
4232
|
isValidPrivateKey,
|
|
4233
|
+
judgeVerifyKeys,
|
|
4089
4234
|
keyFingerprintOf,
|
|
4090
4235
|
keyPathCandidates,
|
|
4091
4236
|
loadAgent,
|
|
@@ -4112,6 +4257,7 @@ function diffMemorySnapshots(before, after) {
|
|
|
4112
4257
|
saveUserConfig,
|
|
4113
4258
|
scanMemory,
|
|
4114
4259
|
scanMemoryBatch,
|
|
4260
|
+
setLoopbackJudgePinsForTests,
|
|
4115
4261
|
setupTelemetry,
|
|
4116
4262
|
shutdownTelemetry,
|
|
4117
4263
|
signEncryptedToolCall,
|