@atbash/sdk 0.21.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 +94 -16
- package/dist/browser.d.mts +109 -12
- package/dist/browser.mjs +339 -59
- package/dist/index.d.mts +109 -12
- package/dist/index.d.ts +109 -12
- package/dist/index.js +235 -63
- package/dist/index.mjs +233 -63
- 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.mjs
CHANGED
|
@@ -104,6 +104,23 @@ function isAllowlistedJudgeHost(endpoint) {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
108
|
+
var loopbackJudgePins = [];
|
|
109
|
+
function setLoopbackJudgePinsForTests(keys) {
|
|
110
|
+
loopbackJudgePins = keys.map((k) => k.trim().toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
function judgeVerifyKeys(endpoint, configuredKey) {
|
|
113
|
+
const host = new URL(endpoint).hostname.toLowerCase();
|
|
114
|
+
const pins = LOOPBACK_HOSTS.has(host) ? loopbackJudgePins : native.pinnedJudgeVerifyKeys(host);
|
|
115
|
+
const key = configuredKey?.trim().toLowerCase();
|
|
116
|
+
if (pins.length === 0) return key ? [key] : [];
|
|
117
|
+
if (key && !pins.includes(key)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`[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.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return pins;
|
|
123
|
+
}
|
|
107
124
|
function validateJudgeEndpoint(judge) {
|
|
108
125
|
const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
|
|
109
126
|
const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
|
|
@@ -218,7 +235,7 @@ function isJailHeld(pubkey) {
|
|
|
218
235
|
return holds.has(pubkey);
|
|
219
236
|
}
|
|
220
237
|
function recordBlock(pubkey) {
|
|
221
|
-
const now =
|
|
238
|
+
const now = performance.now();
|
|
222
239
|
for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
|
|
223
240
|
blockedAt.set(pubkey, now);
|
|
224
241
|
}
|
|
@@ -613,7 +630,7 @@ import {
|
|
|
613
630
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
614
631
|
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
615
632
|
import { createHash } from "crypto";
|
|
616
|
-
var SDK_VERSION = true ? "0.
|
|
633
|
+
var SDK_VERSION = true ? "0.23.0-dev.0" : "";
|
|
617
634
|
var SDK_LANGUAGE = "node";
|
|
618
635
|
function sdkResourceAttributes() {
|
|
619
636
|
const attrs = {
|
|
@@ -884,7 +901,10 @@ function forcedChainNetwork(flagValue) {
|
|
|
884
901
|
|
|
885
902
|
// src-ts/client.ts
|
|
886
903
|
var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
|
|
904
|
+
var TOOL_CALL_IDS_FOR_TESTS = /* @__PURE__ */ Symbol.for("@atbash/sdk.toolCallIdsForTests");
|
|
887
905
|
function generateToolCallId() {
|
|
906
|
+
const fixed = globalThis[TOOL_CALL_IDS_FOR_TESTS];
|
|
907
|
+
if (fixed && fixed.ids.length > 0) return fixed.ids[fixed.next++ % fixed.ids.length];
|
|
888
908
|
return `tc-${Date.now()}-${randomHex(4)}`;
|
|
889
909
|
}
|
|
890
910
|
function resolveConstructorChain(options) {
|
|
@@ -921,12 +941,21 @@ var Atbash = class _Atbash {
|
|
|
921
941
|
orgName;
|
|
922
942
|
/** Default judge response-signing pubkey, if configured (see fromConfig). */
|
|
923
943
|
verifyPubKey;
|
|
944
|
+
/**
|
|
945
|
+
* The keys every judge reply must be signed by: the host's pinned keys, or
|
|
946
|
+
* the configured `verifyPubKey` on a host with none. Empty means unchecked.
|
|
947
|
+
*/
|
|
948
|
+
verifyKeys;
|
|
924
949
|
/** Default org encryption key — see {@link AtbashOptions.orgEncryptionPubKey}. */
|
|
925
950
|
orgEncryptionPubKey;
|
|
926
951
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
927
952
|
failClosed;
|
|
928
953
|
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
929
954
|
requireEnforcement;
|
|
955
|
+
/** When true, a signed non-enforcing mode never relaxes a verdict. */
|
|
956
|
+
enforceLocally;
|
|
957
|
+
/** Results whose enforcement mode came signed and bound to their call. */
|
|
958
|
+
modeTrusted = /* @__PURE__ */ new WeakSet();
|
|
930
959
|
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
931
960
|
decisionTimeoutMs;
|
|
932
961
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
@@ -990,9 +1019,11 @@ var Atbash = class _Atbash {
|
|
|
990
1019
|
this._forcedNetwork = forcedChainNetwork();
|
|
991
1020
|
this.orgName = options.orgName;
|
|
992
1021
|
this.verifyPubKey = validated.verifyPubKey ?? void 0;
|
|
1022
|
+
this.verifyKeys = judgeVerifyKeys(this.endpoint, this.verifyPubKey);
|
|
993
1023
|
this.orgEncryptionPubKey = options.orgEncryptionPubKey;
|
|
994
1024
|
this.failClosed = options.failClosed !== false;
|
|
995
1025
|
this.requireEnforcement = options.requireEnforcement !== false;
|
|
1026
|
+
this.enforceLocally = options.enforceLocally === true;
|
|
996
1027
|
this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
|
|
997
1028
|
this.debug = options.debug === true;
|
|
998
1029
|
this.logger = options.logger ?? {};
|
|
@@ -1001,7 +1032,7 @@ var Atbash = class _Atbash {
|
|
|
1001
1032
|
if (this.endpoint !== DEFAULT_ENDPOINT) {
|
|
1002
1033
|
this.logger.warn?.("[atbash] running on non-default judge endpoint", {
|
|
1003
1034
|
endpoint: this.endpoint,
|
|
1004
|
-
verifying: this.
|
|
1035
|
+
verifying: this.verifyKeys.length > 0 ? "with response-signature pubkey configured" : "without signature verification"
|
|
1005
1036
|
});
|
|
1006
1037
|
}
|
|
1007
1038
|
if (isAllowlistedJudgeHost(this.endpoint)) {
|
|
@@ -1039,12 +1070,12 @@ var Atbash = class _Atbash {
|
|
|
1039
1070
|
_Atbash.environmentLogged = true;
|
|
1040
1071
|
const brief = (rid) => rid ? `${rid.slice(0, 8)}\u2026` : "(unset)";
|
|
1041
1072
|
this.logger.info?.(
|
|
1042
|
-
`[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.
|
|
1073
|
+
`[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyKeys.length > 0 ? "on" : "off"}`,
|
|
1043
1074
|
{
|
|
1044
1075
|
judgeEndpoint: this.endpoint,
|
|
1045
1076
|
privateBlockchainRid: native.DEFAULT_PRIVATE_BLOCKCHAIN_RID,
|
|
1046
1077
|
activeBlockchainRid: this.blockchainRid,
|
|
1047
|
-
responseSignatureCheck:
|
|
1078
|
+
responseSignatureCheck: this.verifyKeys.length > 0
|
|
1048
1079
|
}
|
|
1049
1080
|
);
|
|
1050
1081
|
}
|
|
@@ -1162,6 +1193,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1162
1193
|
verifyPubKey: validated.verifyPubKey ?? void 0,
|
|
1163
1194
|
failClosed: options.failClosed,
|
|
1164
1195
|
requireEnforcement: options.requireEnforcement,
|
|
1196
|
+
enforceLocally: options.enforceLocally,
|
|
1165
1197
|
decisionTimeoutMs: options.decisionTimeoutMs,
|
|
1166
1198
|
// ATBASH_DEBUG lets an operator turn diagnostics on without editing a
|
|
1167
1199
|
// host's plugin config, which is usually the harder half.
|
|
@@ -1296,22 +1328,34 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1296
1328
|
* response bytes via the Rust core's `verifySignature`.
|
|
1297
1329
|
*/
|
|
1298
1330
|
async judgeAction(action, context = "", options = {}) {
|
|
1331
|
+
return this.judgeTracked(action, context, options, false);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* judgeAction, optionally asking the judge for the true verdict and the
|
|
1335
|
+
* organisation's enforcement mode so this client applies the mode itself.
|
|
1336
|
+
*
|
|
1337
|
+
* Only auditToolCall opts in: judgeAction's callers keep the server-side
|
|
1338
|
+
* downgrade they gate on today.
|
|
1339
|
+
*
|
|
1340
|
+
* @param clientEnforces - Send `X-Atbash-Client-Enforcement` when the reply can be trusted.
|
|
1341
|
+
*/
|
|
1342
|
+
judgeTracked(action, context, options, clientEnforces) {
|
|
1299
1343
|
return this.track("judgeAction", this.auth.pubkey, async () => {
|
|
1300
1344
|
try {
|
|
1301
|
-
return await this._judgeAction(action, context, options);
|
|
1345
|
+
return await this._judgeAction(action, context, options, clientEnforces);
|
|
1302
1346
|
} catch (err) {
|
|
1303
1347
|
if (!isEncryptionStateMismatch(err)) throw err;
|
|
1304
1348
|
this._orgKeyFromChain = null;
|
|
1305
|
-
return await this._judgeAction(action, context, options);
|
|
1349
|
+
return await this._judgeAction(action, context, options, clientEnforces);
|
|
1306
1350
|
}
|
|
1307
1351
|
});
|
|
1308
1352
|
}
|
|
1309
|
-
async _judgeAction(action, context, options) {
|
|
1353
|
+
async _judgeAction(action, context, options, clientEnforces = false) {
|
|
1310
1354
|
if (!action?.trim()) {
|
|
1311
1355
|
throw new Error("action is required and cannot be empty.");
|
|
1312
1356
|
}
|
|
1313
1357
|
if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
|
|
1314
|
-
const startedAt =
|
|
1358
|
+
const startedAt = performance.now();
|
|
1315
1359
|
let chainOpts = options.chainOpts;
|
|
1316
1360
|
if (options.orgName && this._explicitChain) {
|
|
1317
1361
|
chainOpts = options.chainOpts ?? {
|
|
@@ -1368,17 +1412,23 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1368
1412
|
if (options.toolArgsJson) body.tool_args_json = options.toolArgsJson;
|
|
1369
1413
|
if (options.resolved) body.resolved = options.resolved;
|
|
1370
1414
|
if (options.mode) body.mode = options.mode;
|
|
1415
|
+
const verifyKeys = this.effectiveVerifyKeys(options.verifyPubKey);
|
|
1416
|
+
const optIn = clientEnforces && (verifyKeys.length > 0 || this.enforceLocally);
|
|
1417
|
+
const optInValue = this.enforceLocally ? "strict" : "1";
|
|
1371
1418
|
let resp;
|
|
1372
1419
|
try {
|
|
1373
|
-
resp = await this.http.post(
|
|
1420
|
+
resp = await this.http.post(
|
|
1421
|
+
"/api/v1/judge",
|
|
1422
|
+
body,
|
|
1423
|
+
optIn ? { "X-Atbash-Client-Enforcement": optInValue } : void 0
|
|
1424
|
+
);
|
|
1374
1425
|
} catch (err) {
|
|
1375
1426
|
throw this.transportError(err);
|
|
1376
1427
|
}
|
|
1377
1428
|
if (!resp.ok) throw await this.httpError(resp);
|
|
1378
1429
|
const bodyBytes = Buffer.from(await resp.arrayBuffer());
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
verifyResponseSignature(bodyBytes, resp, verifyPubKey);
|
|
1430
|
+
if (verifyKeys.length > 0) {
|
|
1431
|
+
verifyResponseSignature(bodyBytes, resp, verifyKeys);
|
|
1382
1432
|
}
|
|
1383
1433
|
const parsed = tryParseJson(bodyBytes.toString("utf-8"));
|
|
1384
1434
|
if (!isRecord2(parsed)) {
|
|
@@ -1398,6 +1448,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1398
1448
|
provider: wireText(data.provider),
|
|
1399
1449
|
latencyMs: wireNumber(data.latency_ms),
|
|
1400
1450
|
toolCallId: wireText(data.tool_call_id, logResult.toolCallId),
|
|
1451
|
+
...wireText(data.judgment_id) ? { judgmentId: wireText(data.judgment_id) } : {},
|
|
1401
1452
|
onChain: Boolean(data.on_chain),
|
|
1402
1453
|
enforced: Boolean(data.enforced),
|
|
1403
1454
|
enforcementMode: wireText(data.enforcement_mode),
|
|
@@ -1405,23 +1456,46 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1405
1456
|
score,
|
|
1406
1457
|
refusal: explicitRefusal(data)
|
|
1407
1458
|
};
|
|
1408
|
-
|
|
1459
|
+
if (optIn) {
|
|
1460
|
+
const echo = data.client_enforcement;
|
|
1461
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1462
|
+
throw new SignatureVerificationError(
|
|
1463
|
+
"the reply does not confirm the enforcement this client asked for"
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
if (verifyKeys.length > 0) {
|
|
1467
|
+
const statedId = wireText(data.tool_call_id);
|
|
1468
|
+
const statedJudgment = wireText(data.judgment_id);
|
|
1469
|
+
if (statedId && statedId !== logResult.toolCallId && statedId !== judgmentId || statedJudgment && statedJudgment !== judgmentId) {
|
|
1470
|
+
throw new SignatureVerificationError(
|
|
1471
|
+
`the signed reply is for call ${statedJudgment || statedId}, not this one`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
if (statedId && echo === optInValue) this.modeTrusted.add(result);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return this.applyJailHold(result, startedAt, wireText(data.on_chain_status));
|
|
1409
1478
|
}
|
|
1410
1479
|
/**
|
|
1411
|
-
* Start a local hold on an enforced
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1480
|
+
* Start a local hold on an enforced BLOCK that jails: one already on chain,
|
|
1481
|
+
* or one the judge answered before its chain write landed
|
|
1482
|
+
* (`on_chain_status: "pending"`), which jails once it lands. Refuse any other
|
|
1483
|
+
* result that lands while the agent is held, or whose request was already in
|
|
1484
|
+
* flight when the block arrived: the server judged it on pre-jail state.
|
|
1485
|
+
*
|
|
1486
|
+
* @param onChainStatus - The reply's `on_chain_status`; empty when absent.
|
|
1415
1487
|
*/
|
|
1416
|
-
applyJailHold(result, startedAt) {
|
|
1488
|
+
applyJailHold(result, startedAt, onChainStatus = "") {
|
|
1417
1489
|
const pubkey = this.auth.pubkey;
|
|
1418
|
-
if (result.verdict === "BLOCK" && result.enforced
|
|
1490
|
+
if (result.verdict === "BLOCK" && result.enforced) {
|
|
1419
1491
|
recordBlock(pubkey);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1492
|
+
if (result.onChain || onChainStatus === "pending") {
|
|
1493
|
+
holdJail(
|
|
1494
|
+
pubkey,
|
|
1495
|
+
() => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
|
|
1496
|
+
() => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1425
1499
|
return result;
|
|
1426
1500
|
}
|
|
1427
1501
|
return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
|
|
@@ -1476,13 +1550,18 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1476
1550
|
try {
|
|
1477
1551
|
this.logger.info?.("[atbash] judge API called", { tool: toolName });
|
|
1478
1552
|
const result = await withDeadline(
|
|
1479
|
-
this.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1553
|
+
this.judgeTracked(
|
|
1554
|
+
actionText,
|
|
1555
|
+
contextText,
|
|
1556
|
+
{
|
|
1557
|
+
toolName,
|
|
1558
|
+
toolArgsJson: argsJson,
|
|
1559
|
+
orgName: this.orgName,
|
|
1560
|
+
// Passed through unredacted — caller vouches for the contents.
|
|
1561
|
+
resolved: input.resolved
|
|
1562
|
+
},
|
|
1563
|
+
true
|
|
1564
|
+
),
|
|
1486
1565
|
this.decisionTimeoutMs
|
|
1487
1566
|
);
|
|
1488
1567
|
return this.decide(result);
|
|
@@ -1492,7 +1571,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1492
1571
|
return {
|
|
1493
1572
|
allow: false,
|
|
1494
1573
|
verdict: "ERROR",
|
|
1495
|
-
reason: `The judge's reply
|
|
1574
|
+
reason: `The judge's reply was not trusted (${errorMessage(err)}).`
|
|
1496
1575
|
};
|
|
1497
1576
|
}
|
|
1498
1577
|
if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
|
|
@@ -1502,20 +1581,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1502
1581
|
}
|
|
1503
1582
|
}
|
|
1504
1583
|
/**
|
|
1505
|
-
* The verify
|
|
1506
|
-
* may only narrow what the client accepts: on a client pinned to
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1584
|
+
* The verify keys for one call (R4, governor tier-2 review). A per-call key
|
|
1585
|
+
* may only narrow what the client accepts: on a client pinned to judge keys
|
|
1586
|
+
* it must be one of them, never a replacement; on an unpinned client it
|
|
1587
|
+
* adds the check.
|
|
1509
1588
|
*/
|
|
1510
|
-
|
|
1511
|
-
if (perCall === void 0) return this.
|
|
1589
|
+
effectiveVerifyKeys(perCall) {
|
|
1590
|
+
if (perCall === void 0) return this.verifyKeys;
|
|
1512
1591
|
const key = perCall.trim().toLowerCase();
|
|
1513
|
-
if (this.
|
|
1592
|
+
if (this.verifyKeys.length > 0 && !this.verifyKeys.includes(key)) {
|
|
1514
1593
|
throw new SignatureVerificationError(
|
|
1515
1594
|
"a per-call verifyPubKey cannot replace the judge key this client is pinned to"
|
|
1516
1595
|
);
|
|
1517
1596
|
}
|
|
1518
|
-
return key;
|
|
1597
|
+
return [key];
|
|
1519
1598
|
}
|
|
1520
1599
|
/**
|
|
1521
1600
|
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
@@ -1527,7 +1606,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1527
1606
|
* never overrides a refusal.
|
|
1528
1607
|
*/
|
|
1529
1608
|
decide(result) {
|
|
1530
|
-
const
|
|
1609
|
+
const decided = this.applyMode(result);
|
|
1610
|
+
const inner = result.judgmentId ? { ...decided, judgmentId: result.judgmentId } : decided;
|
|
1531
1611
|
if (!result.enforcementMode) return inner;
|
|
1532
1612
|
return {
|
|
1533
1613
|
...inner,
|
|
@@ -1535,6 +1615,50 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1535
1615
|
enforcementMode: result.enforcementMode
|
|
1536
1616
|
};
|
|
1537
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Apply the organisation's enforcement mode to the judge's true verdict.
|
|
1620
|
+
*
|
|
1621
|
+
* Only a mode that arrived signed and bound to this call counts; a missing,
|
|
1622
|
+
* unsigned or unknown mode, or enforceLocally, means enforce. Under
|
|
1623
|
+
* `shadow`, `monitor` or `off` a BLOCK or HOLD runs, reported in
|
|
1624
|
+
* `wouldVerdict`. An ERROR stays an error: it is not a verdict.
|
|
1625
|
+
*/
|
|
1626
|
+
applyMode(result) {
|
|
1627
|
+
const mode = result.enforcementMode;
|
|
1628
|
+
const relaxes = this.modeTrusted.has(result) && (mode === "shadow" || mode === "monitor" || mode === "off");
|
|
1629
|
+
if (result.status === "shadow_recorded" && result.verdict === "No verdict") {
|
|
1630
|
+
if (!relaxes || this.enforceLocally) {
|
|
1631
|
+
return {
|
|
1632
|
+
allow: false,
|
|
1633
|
+
verdict: "ERROR",
|
|
1634
|
+
reason: "Atbash recorded this action without judging it (the organisation is not enforcing), and this client requires a verdict.",
|
|
1635
|
+
toolCallId: result.toolCallId
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
this.logger.info?.(`[atbash] recorded in ${mode} mode; not judged yet`, {
|
|
1639
|
+
toolCallId: result.toolCallId
|
|
1640
|
+
});
|
|
1641
|
+
return {
|
|
1642
|
+
allow: true,
|
|
1643
|
+
verdict: "ALLOW",
|
|
1644
|
+
reason: `Recorded in ${mode} mode; the verdict appears in the dashboard.`,
|
|
1645
|
+
toolCallId: result.toolCallId
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
const inner = this.decideInner(result);
|
|
1649
|
+
if (!relaxes || this.enforceLocally || inner.allow) return inner;
|
|
1650
|
+
if (inner.verdict !== "BLOCK" && inner.verdict !== "HOLD") return inner;
|
|
1651
|
+
this.logger.warn?.(`[atbash] would ${inner.verdict.toLowerCase()} (${mode} mode): ${inner.reason ?? ""}`, {
|
|
1652
|
+
toolCallId: inner.toolCallId
|
|
1653
|
+
});
|
|
1654
|
+
return {
|
|
1655
|
+
allow: true,
|
|
1656
|
+
verdict: "ALLOW",
|
|
1657
|
+
reason: inner.reason,
|
|
1658
|
+
toolCallId: inner.toolCallId,
|
|
1659
|
+
wouldVerdict: inner.verdict
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1538
1662
|
decideInner(result) {
|
|
1539
1663
|
const toolCallId = result.toolCallId;
|
|
1540
1664
|
const deny = (verdict, reason) => ({
|
|
@@ -1570,7 +1694,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1570
1694
|
return {
|
|
1571
1695
|
allow: false,
|
|
1572
1696
|
verdict: "ERROR",
|
|
1573
|
-
|
|
1697
|
+
// The judge's own reason says why it did not check (the plan, a
|
|
1698
|
+
// deactivated agent); a fixed "audit-tier plan" hid which.
|
|
1699
|
+
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.",
|
|
1574
1700
|
toolCallId
|
|
1575
1701
|
};
|
|
1576
1702
|
}
|
|
@@ -1660,28 +1786,52 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1660
1786
|
const pk = agentPubkey ?? this.auth.pubkey;
|
|
1661
1787
|
return this.track("getJudgmentStatus", pk, async () => {
|
|
1662
1788
|
const brid = await this.callBrid(chainOpts);
|
|
1789
|
+
const optIn = this.verifyKeys.length > 0 || this.enforceLocally;
|
|
1790
|
+
const optInValue = this.enforceLocally ? "strict" : "1";
|
|
1663
1791
|
const resp = await this.http.get(
|
|
1664
1792
|
"/api/v1/judge",
|
|
1665
1793
|
{ tool_call_id: judgmentId, agent_pubkey: pk, brid },
|
|
1666
|
-
|
|
1794
|
+
{
|
|
1795
|
+
...this.authHeaders(brid),
|
|
1796
|
+
...optIn ? { "X-Atbash-Client-Enforcement": optInValue } : {}
|
|
1797
|
+
}
|
|
1667
1798
|
);
|
|
1668
1799
|
await this.raiseIfError(resp);
|
|
1669
1800
|
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
1670
|
-
if (this.
|
|
1671
|
-
verifyResponseSignature(bytes, resp, this.
|
|
1801
|
+
if (this.verifyKeys.length > 0) {
|
|
1802
|
+
verifyResponseSignature(bytes, resp, this.verifyKeys);
|
|
1672
1803
|
}
|
|
1673
1804
|
const parsed = tryParseJson(bytes.toString("utf-8"));
|
|
1674
1805
|
const data = isRecord2(parsed) ? parsed : {};
|
|
1675
|
-
|
|
1806
|
+
const rawId = data.tool_call_id;
|
|
1807
|
+
const statedId = wireText(rawId);
|
|
1808
|
+
const sameCall = rawId === void 0 || rawId === null || statedId === judgmentId;
|
|
1809
|
+
let modeTrusted = false;
|
|
1810
|
+
if (optIn) {
|
|
1811
|
+
const echo = data.client_enforcement;
|
|
1812
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1813
|
+
throw new SignatureVerificationError(
|
|
1814
|
+
"the status does not confirm the enforcement this client asked for"
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
modeTrusted = this.verifyKeys.length > 0 && echo === optInValue && statedId === judgmentId;
|
|
1818
|
+
}
|
|
1819
|
+
const status = {
|
|
1676
1820
|
status: normalizeStatus(data.status),
|
|
1677
1821
|
verdict: normalizeVerdict(data.verdict),
|
|
1678
|
-
allow: this.executable(data),
|
|
1822
|
+
allow: sameCall && this.executable(data),
|
|
1679
1823
|
reason: wireText(data.reason),
|
|
1680
|
-
judgmentId:
|
|
1681
|
-
|
|
1682
|
-
cached: optBool(data.cached),
|
|
1683
|
-
responseTimeMs: optNumber(data.responseTimeMs)
|
|
1824
|
+
judgmentId: statedId || judgmentId,
|
|
1825
|
+
responseTimeMs: optNumber(data.response_time_ms)
|
|
1684
1826
|
};
|
|
1827
|
+
const mode = wireText(data.enforcement_mode);
|
|
1828
|
+
if (!modeTrusted || this.enforceLocally || !(mode === "shadow" || mode === "monitor" || mode === "off") || status.verdict !== "BLOCK" && status.verdict !== "HOLD") {
|
|
1829
|
+
return status;
|
|
1830
|
+
}
|
|
1831
|
+
this.logger.warn?.(`[atbash] would ${status.verdict.toLowerCase()} (${mode} mode): ${status.reason}`, {
|
|
1832
|
+
toolCallId: judgmentId
|
|
1833
|
+
});
|
|
1834
|
+
return { ...status, verdict: "ALLOW", allow: true, wouldVerdict: status.verdict };
|
|
1685
1835
|
});
|
|
1686
1836
|
}
|
|
1687
1837
|
/* ── risk-engine queries (action-dispatched GET) ───────────────────────── */
|
|
@@ -2183,14 +2333,14 @@ async function withDeadline(p, ms) {
|
|
|
2183
2333
|
clearTimeout(timer);
|
|
2184
2334
|
}
|
|
2185
2335
|
}
|
|
2186
|
-
function verifyResponseSignature(bodyBytes, resp,
|
|
2336
|
+
function verifyResponseSignature(bodyBytes, resp, verifyKeys) {
|
|
2187
2337
|
const sig = resp.headers.get("X-Atbash-Signature");
|
|
2188
2338
|
if (!sig) {
|
|
2189
2339
|
throw new SignatureVerificationError("missing X-Atbash-Signature header");
|
|
2190
2340
|
}
|
|
2191
|
-
let ok;
|
|
2341
|
+
let ok = false;
|
|
2192
2342
|
try {
|
|
2193
|
-
ok = native.verifySignature(bodyBytes, sig,
|
|
2343
|
+
ok = verifyKeys.some((key) => native.verifySignature(bodyBytes, sig, key));
|
|
2194
2344
|
} catch (err) {
|
|
2195
2345
|
throw new SignatureVerificationError(
|
|
2196
2346
|
`signature verification threw: ${errorMessage(err)}`
|
|
@@ -2282,10 +2432,6 @@ function optNumber(v) {
|
|
|
2282
2432
|
const n = Number(v);
|
|
2283
2433
|
return Number.isFinite(n) ? n : void 0;
|
|
2284
2434
|
}
|
|
2285
|
-
function optBool(v) {
|
|
2286
|
-
if (v === null || v === void 0) return void 0;
|
|
2287
|
-
return Boolean(v);
|
|
2288
|
-
}
|
|
2289
2435
|
function tryParseJson(text) {
|
|
2290
2436
|
try {
|
|
2291
2437
|
return JSON.parse(text);
|
|
@@ -2616,14 +2762,15 @@ async function decryptRow(row, key) {
|
|
|
2616
2762
|
updatedAt: parsed.updated_at
|
|
2617
2763
|
};
|
|
2618
2764
|
}
|
|
2619
|
-
async function readRows(action, auth, chainOpts, filePath) {
|
|
2765
|
+
async function readRows(action, auth, chainOpts, filePath, extra = {}) {
|
|
2620
2766
|
const key = await deriveMemoryKey(auth.privkey);
|
|
2621
2767
|
const rows = await memoryQuery(
|
|
2622
2768
|
action,
|
|
2623
2769
|
auth,
|
|
2624
2770
|
resolveBlockchainRid(chainOpts),
|
|
2625
2771
|
chainOpts,
|
|
2626
|
-
filePath
|
|
2772
|
+
filePath,
|
|
2773
|
+
extra
|
|
2627
2774
|
);
|
|
2628
2775
|
return Promise.all((rows ?? []).map((row) => decryptRow(row, key)));
|
|
2629
2776
|
}
|
|
@@ -2643,8 +2790,29 @@ function getRecentAgentMemory(auth, chainOpts, filePath) {
|
|
|
2643
2790
|
function getActiveAgentMemory(auth, chainOpts, filePath) {
|
|
2644
2791
|
return readRows("memory-active", auth, chainOpts, filePath ?? null);
|
|
2645
2792
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2793
|
+
var MEMORY_HISTORY_MAX_COUNT = 200;
|
|
2794
|
+
function memoryHistoryPageArgs(page) {
|
|
2795
|
+
const extra = {};
|
|
2796
|
+
for (const [name, value] of [
|
|
2797
|
+
["max_count", page?.maxCount],
|
|
2798
|
+
["before_id", page?.beforeId]
|
|
2799
|
+
]) {
|
|
2800
|
+
if (value === void 0) continue;
|
|
2801
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2802
|
+
throw new Error(`getMemoryHistory: ${name} must be a positive integer`);
|
|
2803
|
+
}
|
|
2804
|
+
if (name === "max_count" && value > MEMORY_HISTORY_MAX_COUNT) {
|
|
2805
|
+
throw new Error(`getMemoryHistory: max_count must be at most ${MEMORY_HISTORY_MAX_COUNT}`);
|
|
2806
|
+
}
|
|
2807
|
+
extra[name] = value;
|
|
2808
|
+
}
|
|
2809
|
+
return extra;
|
|
2810
|
+
}
|
|
2811
|
+
async function getMemoryHistory(auth, chainOpts, filePath, page) {
|
|
2812
|
+
const rows = await readRows("memory-history", auth, chainOpts, filePath ?? null, memoryHistoryPageArgs(page));
|
|
2813
|
+
const beforeId = page?.beforeId;
|
|
2814
|
+
const older = beforeId === void 0 ? rows : rows.filter((row) => row.id < beforeId);
|
|
2815
|
+
return page?.maxCount === void 0 ? older : older.slice(0, page.maxCount);
|
|
2648
2816
|
}
|
|
2649
2817
|
async function getMemoryById(id, auth, chainOpts) {
|
|
2650
2818
|
native.validateMemoryId(id);
|
|
@@ -3942,6 +4110,7 @@ export {
|
|
|
3942
4110
|
isEnvelope,
|
|
3943
4111
|
isJailHeld,
|
|
3944
4112
|
isValidPrivateKey,
|
|
4113
|
+
judgeVerifyKeys,
|
|
3945
4114
|
keyFingerprintOf,
|
|
3946
4115
|
keyPathCandidates,
|
|
3947
4116
|
loadAgent,
|
|
@@ -3968,6 +4137,7 @@ export {
|
|
|
3968
4137
|
saveUserConfig,
|
|
3969
4138
|
scanMemory,
|
|
3970
4139
|
scanMemoryBatch,
|
|
4140
|
+
setLoopbackJudgePinsForTests,
|
|
3971
4141
|
setupTelemetry,
|
|
3972
4142
|
shutdownTelemetry,
|
|
3973
4143
|
signEncryptedToolCall,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the SDK's own tests, published as `@atbash/sdk/testing` and kept
|
|
3
|
+
* out of the main entry: fixing the call ids in production would let an old
|
|
4
|
+
* signed reply pass as the answer to a new call.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fix the ids the client generates, handed out in turn, so a test can serve a
|
|
8
|
+
* judge reply signed for them. A judge call takes two: its call id, then its
|
|
9
|
+
* judgment id.
|
|
10
|
+
*
|
|
11
|
+
* @param ids - One id or several, used in turn; `undefined` restores random ids.
|
|
12
|
+
*/
|
|
13
|
+
declare function setToolCallIdForTests(ids: string | readonly string[] | undefined): void;
|
|
14
|
+
|
|
15
|
+
export { setToolCallIdForTests };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the SDK's own tests, published as `@atbash/sdk/testing` and kept
|
|
3
|
+
* out of the main entry: fixing the call ids in production would let an old
|
|
4
|
+
* signed reply pass as the answer to a new call.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fix the ids the client generates, handed out in turn, so a test can serve a
|
|
8
|
+
* judge reply signed for them. A judge call takes two: its call id, then its
|
|
9
|
+
* judgment id.
|
|
10
|
+
*
|
|
11
|
+
* @param ids - One id or several, used in turn; `undefined` restores random ids.
|
|
12
|
+
*/
|
|
13
|
+
declare function setToolCallIdForTests(ids: string | readonly string[] | undefined): void;
|
|
14
|
+
|
|
15
|
+
export { setToolCallIdForTests };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src-ts/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
setToolCallIdForTests: () => setToolCallIdForTests
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(testing_exports);
|
|
26
|
+
var TOOL_CALL_IDS_FOR_TESTS = /* @__PURE__ */ Symbol.for("@atbash/sdk.toolCallIdsForTests");
|
|
27
|
+
function setToolCallIdForTests(ids) {
|
|
28
|
+
globalThis[TOOL_CALL_IDS_FOR_TESTS] = ids === void 0 ? void 0 : { ids: typeof ids === "string" ? [ids] : [...ids], next: 0 };
|
|
29
|
+
}
|
|
30
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
31
|
+
0 && (module.exports = {
|
|
32
|
+
setToolCallIdForTests
|
|
33
|
+
});
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src-ts/testing.ts
|
|
2
|
+
var TOOL_CALL_IDS_FOR_TESTS = /* @__PURE__ */ Symbol.for("@atbash/sdk.toolCallIdsForTests");
|
|
3
|
+
function setToolCallIdForTests(ids) {
|
|
4
|
+
globalThis[TOOL_CALL_IDS_FOR_TESTS] = ids === void 0 ? void 0 : { ids: typeof ids === "string" ? [ids] : [...ids], next: 0 };
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
setToolCallIdForTests
|
|
8
|
+
};
|
package/index.d.ts
CHANGED
|
@@ -236,6 +236,9 @@ export declare function parseRollbackRows(value: any): Array<RawRollbackRowJs>
|
|
|
236
236
|
|
|
237
237
|
export declare function parseScoreFromReason(reason: string): ParsedScoreJs
|
|
238
238
|
|
|
239
|
+
/** The response-signing pubkeys pinned for a judge host; empty when it has none. */
|
|
240
|
+
export declare function pinnedJudgeVerifyKeys(host: string): Array<string>
|
|
241
|
+
|
|
239
242
|
export declare const QUERY_GET_ACTIVE_AGENT_MEMORY: string
|
|
240
243
|
|
|
241
244
|
export declare const QUERY_GET_ACTIVE_MEMORY_ID: string
|