@atbash/sdk 0.22.0-dev.0 → 0.24.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 +6 -1
- package/dist/browser.d.mts +147 -10
- package/dist/browser.mjs +356 -59
- package/dist/index.d.mts +147 -10
- package/dist/index.d.ts +147 -10
- package/dist/index.js +227 -60
- package/dist/index.mjs +225 -60
- 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.24.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)) {
|
|
@@ -1389,6 +1439,12 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1389
1439
|
const data = parsed;
|
|
1390
1440
|
const rawScore = data.score;
|
|
1391
1441
|
const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
|
|
1442
|
+
const statedId = wireText(data.tool_call_id);
|
|
1443
|
+
const statedJudgment = wireText(data.judgment_id);
|
|
1444
|
+
const statedStatus = wireText(data.on_chain_status);
|
|
1445
|
+
const statedJudgeStatus = typeof data.status === "string" ? data.status : "";
|
|
1446
|
+
const boundToCall = (statedId === logResult.toolCallId || statedId === judgmentId) && (!statedJudgment || statedJudgment === judgmentId);
|
|
1447
|
+
const signatureChecked = verifyKeys.length > 0;
|
|
1392
1448
|
const result = {
|
|
1393
1449
|
verdict: normalizeVerdict(data.verdict),
|
|
1394
1450
|
allow: this.executable(data),
|
|
@@ -1398,30 +1454,65 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1398
1454
|
provider: wireText(data.provider),
|
|
1399
1455
|
latencyMs: wireNumber(data.latency_ms),
|
|
1400
1456
|
toolCallId: wireText(data.tool_call_id, logResult.toolCallId),
|
|
1457
|
+
...wireText(data.judgment_id) ? { judgmentId: wireText(data.judgment_id) } : {},
|
|
1401
1458
|
onChain: Boolean(data.on_chain),
|
|
1459
|
+
// Only a stated boolean is a statement: `onChain` above reads silence as false.
|
|
1460
|
+
...typeof data.on_chain === "boolean" ? {
|
|
1461
|
+
record: {
|
|
1462
|
+
onChain: data.on_chain,
|
|
1463
|
+
...statedStatus ? { status: statedStatus } : {},
|
|
1464
|
+
boundToCall,
|
|
1465
|
+
signatureChecked,
|
|
1466
|
+
...statedJudgeStatus ? { judgeStatus: statedJudgeStatus } : {},
|
|
1467
|
+
// A judge status marks a reply that carries no verdict: a shadow
|
|
1468
|
+
// ack, or an answer that was only logged.
|
|
1469
|
+
verdictRecorded: data.on_chain && boundToCall && signatureChecked && !statedJudgeStatus
|
|
1470
|
+
}
|
|
1471
|
+
} : {},
|
|
1402
1472
|
enforced: Boolean(data.enforced),
|
|
1403
1473
|
enforcementMode: wireText(data.enforcement_mode),
|
|
1404
1474
|
status: wireText(data.status),
|
|
1405
1475
|
score,
|
|
1406
1476
|
refusal: explicitRefusal(data)
|
|
1407
1477
|
};
|
|
1408
|
-
|
|
1478
|
+
if (optIn) {
|
|
1479
|
+
const echo = data.client_enforcement;
|
|
1480
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1481
|
+
throw new SignatureVerificationError(
|
|
1482
|
+
"the reply does not confirm the enforcement this client asked for"
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
if (verifyKeys.length > 0) {
|
|
1486
|
+
if (statedId && statedId !== logResult.toolCallId && statedId !== judgmentId || statedJudgment && statedJudgment !== judgmentId) {
|
|
1487
|
+
throw new SignatureVerificationError(
|
|
1488
|
+
`the signed reply is for call ${statedJudgment || statedId}, not this one`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
if (statedId && echo === optInValue) this.modeTrusted.add(result);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return this.applyJailHold(result, startedAt, statedStatus);
|
|
1409
1495
|
}
|
|
1410
1496
|
/**
|
|
1411
|
-
* Start a local hold on an enforced
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1497
|
+
* Start a local hold on an enforced BLOCK that jails: one already on chain,
|
|
1498
|
+
* or one the judge answered before its chain write landed
|
|
1499
|
+
* (`on_chain_status: "pending"`), which jails once it lands. Refuse any other
|
|
1500
|
+
* result that lands while the agent is held, or whose request was already in
|
|
1501
|
+
* flight when the block arrived: the server judged it on pre-jail state.
|
|
1502
|
+
*
|
|
1503
|
+
* @param onChainStatus - The reply's `on_chain_status`; empty when absent.
|
|
1415
1504
|
*/
|
|
1416
|
-
applyJailHold(result, startedAt) {
|
|
1505
|
+
applyJailHold(result, startedAt, onChainStatus = "") {
|
|
1417
1506
|
const pubkey = this.auth.pubkey;
|
|
1418
|
-
if (result.verdict === "BLOCK" && result.enforced
|
|
1507
|
+
if (result.verdict === "BLOCK" && result.enforced) {
|
|
1419
1508
|
recordBlock(pubkey);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1509
|
+
if (result.onChain || onChainStatus === "pending") {
|
|
1510
|
+
holdJail(
|
|
1511
|
+
pubkey,
|
|
1512
|
+
() => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
|
|
1513
|
+
() => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1425
1516
|
return result;
|
|
1426
1517
|
}
|
|
1427
1518
|
return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
|
|
@@ -1432,7 +1523,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1432
1523
|
verdict: "BLOCK",
|
|
1433
1524
|
allow: false,
|
|
1434
1525
|
actionType: "block",
|
|
1435
|
-
reason:
|
|
1526
|
+
reason: `Agent is jailed. All actions are blocked until the organization owner unjails this agent at ${this.endpoint.replace(/\/+$/, "")}/risk-engine/agents.`,
|
|
1436
1527
|
confidence: 1,
|
|
1437
1528
|
provider: "policy",
|
|
1438
1529
|
latencyMs: 0,
|
|
@@ -1476,13 +1567,18 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1476
1567
|
try {
|
|
1477
1568
|
this.logger.info?.("[atbash] judge API called", { tool: toolName });
|
|
1478
1569
|
const result = await withDeadline(
|
|
1479
|
-
this.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1570
|
+
this.judgeTracked(
|
|
1571
|
+
actionText,
|
|
1572
|
+
contextText,
|
|
1573
|
+
{
|
|
1574
|
+
toolName,
|
|
1575
|
+
toolArgsJson: argsJson,
|
|
1576
|
+
orgName: this.orgName,
|
|
1577
|
+
// Passed through unredacted — caller vouches for the contents.
|
|
1578
|
+
resolved: input.resolved
|
|
1579
|
+
},
|
|
1580
|
+
true
|
|
1581
|
+
),
|
|
1486
1582
|
this.decisionTimeoutMs
|
|
1487
1583
|
);
|
|
1488
1584
|
return this.decide(result);
|
|
@@ -1492,7 +1588,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1492
1588
|
return {
|
|
1493
1589
|
allow: false,
|
|
1494
1590
|
verdict: "ERROR",
|
|
1495
|
-
reason: `The judge's reply
|
|
1591
|
+
reason: `The judge's reply was not trusted (${errorMessage(err)}).`
|
|
1496
1592
|
};
|
|
1497
1593
|
}
|
|
1498
1594
|
if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
|
|
@@ -1502,20 +1598,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1502
1598
|
}
|
|
1503
1599
|
}
|
|
1504
1600
|
/**
|
|
1505
|
-
* The verify
|
|
1506
|
-
* may only narrow what the client accepts: on a client pinned to
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1601
|
+
* The verify keys for one call (R4, governor tier-2 review). A per-call key
|
|
1602
|
+
* may only narrow what the client accepts: on a client pinned to judge keys
|
|
1603
|
+
* it must be one of them, never a replacement; on an unpinned client it
|
|
1604
|
+
* adds the check.
|
|
1509
1605
|
*/
|
|
1510
|
-
|
|
1511
|
-
if (perCall === void 0) return this.
|
|
1606
|
+
effectiveVerifyKeys(perCall) {
|
|
1607
|
+
if (perCall === void 0) return this.verifyKeys;
|
|
1512
1608
|
const key = perCall.trim().toLowerCase();
|
|
1513
|
-
if (this.
|
|
1609
|
+
if (this.verifyKeys.length > 0 && !this.verifyKeys.includes(key)) {
|
|
1514
1610
|
throw new SignatureVerificationError(
|
|
1515
1611
|
"a per-call verifyPubKey cannot replace the judge key this client is pinned to"
|
|
1516
1612
|
);
|
|
1517
1613
|
}
|
|
1518
|
-
return key;
|
|
1614
|
+
return [key];
|
|
1519
1615
|
}
|
|
1520
1616
|
/**
|
|
1521
1617
|
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
@@ -1527,7 +1623,12 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1527
1623
|
* never overrides a refusal.
|
|
1528
1624
|
*/
|
|
1529
1625
|
decide(result) {
|
|
1530
|
-
const
|
|
1626
|
+
const decided = this.applyMode(result);
|
|
1627
|
+
const inner = {
|
|
1628
|
+
...decided,
|
|
1629
|
+
...result.judgmentId ? { judgmentId: result.judgmentId } : {},
|
|
1630
|
+
...result.record ? { record: result.record } : {}
|
|
1631
|
+
};
|
|
1531
1632
|
if (!result.enforcementMode) return inner;
|
|
1532
1633
|
return {
|
|
1533
1634
|
...inner,
|
|
@@ -1535,6 +1636,50 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1535
1636
|
enforcementMode: result.enforcementMode
|
|
1536
1637
|
};
|
|
1537
1638
|
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Apply the organisation's enforcement mode to the judge's true verdict.
|
|
1641
|
+
*
|
|
1642
|
+
* Only a mode that arrived signed and bound to this call counts; a missing,
|
|
1643
|
+
* unsigned or unknown mode, or enforceLocally, means enforce. Under
|
|
1644
|
+
* `shadow`, `monitor` or `off` a BLOCK or HOLD runs, reported in
|
|
1645
|
+
* `wouldVerdict`. An ERROR stays an error: it is not a verdict.
|
|
1646
|
+
*/
|
|
1647
|
+
applyMode(result) {
|
|
1648
|
+
const mode = result.enforcementMode;
|
|
1649
|
+
const relaxes = this.modeTrusted.has(result) && (mode === "shadow" || mode === "monitor" || mode === "off");
|
|
1650
|
+
if (result.status === "shadow_recorded" && result.verdict === "No verdict") {
|
|
1651
|
+
if (!relaxes || this.enforceLocally) {
|
|
1652
|
+
return {
|
|
1653
|
+
allow: false,
|
|
1654
|
+
verdict: "ERROR",
|
|
1655
|
+
reason: "Atbash recorded this action without judging it (the organisation is not enforcing), and this client requires a verdict.",
|
|
1656
|
+
toolCallId: result.toolCallId
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
this.logger.info?.(`[atbash] recorded in ${mode} mode; not judged yet`, {
|
|
1660
|
+
toolCallId: result.toolCallId
|
|
1661
|
+
});
|
|
1662
|
+
return {
|
|
1663
|
+
allow: true,
|
|
1664
|
+
verdict: "ALLOW",
|
|
1665
|
+
reason: `Recorded in ${mode} mode; the verdict appears in the dashboard.`,
|
|
1666
|
+
toolCallId: result.toolCallId
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const inner = this.decideInner(result);
|
|
1670
|
+
if (!relaxes || this.enforceLocally || inner.allow) return inner;
|
|
1671
|
+
if (inner.verdict !== "BLOCK" && inner.verdict !== "HOLD") return inner;
|
|
1672
|
+
this.logger.warn?.(`[atbash] would ${inner.verdict.toLowerCase()} (${mode} mode): ${inner.reason ?? ""}`, {
|
|
1673
|
+
toolCallId: inner.toolCallId
|
|
1674
|
+
});
|
|
1675
|
+
return {
|
|
1676
|
+
allow: true,
|
|
1677
|
+
verdict: "ALLOW",
|
|
1678
|
+
reason: inner.reason,
|
|
1679
|
+
toolCallId: inner.toolCallId,
|
|
1680
|
+
wouldVerdict: inner.verdict
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1538
1683
|
decideInner(result) {
|
|
1539
1684
|
const toolCallId = result.toolCallId;
|
|
1540
1685
|
const deny = (verdict, reason) => ({
|
|
@@ -1570,7 +1715,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1570
1715
|
return {
|
|
1571
1716
|
allow: false,
|
|
1572
1717
|
verdict: "ERROR",
|
|
1573
|
-
|
|
1718
|
+
// The judge's own reason says why it did not check (the plan, a
|
|
1719
|
+
// deactivated agent); a fixed "audit-tier plan" hid which.
|
|
1720
|
+
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
1721
|
toolCallId
|
|
1575
1722
|
};
|
|
1576
1723
|
}
|
|
@@ -1660,32 +1807,52 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
|
|
|
1660
1807
|
const pk = agentPubkey ?? this.auth.pubkey;
|
|
1661
1808
|
return this.track("getJudgmentStatus", pk, async () => {
|
|
1662
1809
|
const brid = await this.callBrid(chainOpts);
|
|
1810
|
+
const optIn = this.verifyKeys.length > 0 || this.enforceLocally;
|
|
1811
|
+
const optInValue = this.enforceLocally ? "strict" : "1";
|
|
1663
1812
|
const resp = await this.http.get(
|
|
1664
1813
|
"/api/v1/judge",
|
|
1665
1814
|
{ tool_call_id: judgmentId, agent_pubkey: pk, brid },
|
|
1666
|
-
|
|
1815
|
+
{
|
|
1816
|
+
...this.authHeaders(brid),
|
|
1817
|
+
...optIn ? { "X-Atbash-Client-Enforcement": optInValue } : {}
|
|
1818
|
+
}
|
|
1667
1819
|
);
|
|
1668
1820
|
await this.raiseIfError(resp);
|
|
1669
1821
|
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
1670
|
-
if (this.
|
|
1671
|
-
verifyResponseSignature(bytes, resp, this.
|
|
1822
|
+
if (this.verifyKeys.length > 0) {
|
|
1823
|
+
verifyResponseSignature(bytes, resp, this.verifyKeys);
|
|
1672
1824
|
}
|
|
1673
1825
|
const parsed = tryParseJson(bytes.toString("utf-8"));
|
|
1674
1826
|
const data = isRecord2(parsed) ? parsed : {};
|
|
1675
|
-
const rawId = data.tool_call_id
|
|
1827
|
+
const rawId = data.tool_call_id;
|
|
1676
1828
|
const statedId = wireText(rawId);
|
|
1677
1829
|
const sameCall = rawId === void 0 || rawId === null || statedId === judgmentId;
|
|
1678
|
-
|
|
1830
|
+
let modeTrusted = false;
|
|
1831
|
+
if (optIn) {
|
|
1832
|
+
const echo = data.client_enforcement;
|
|
1833
|
+
if (echo !== void 0 && echo !== optInValue || echo === void 0 && this.enforceLocally) {
|
|
1834
|
+
throw new SignatureVerificationError(
|
|
1835
|
+
"the status does not confirm the enforcement this client asked for"
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
modeTrusted = this.verifyKeys.length > 0 && echo === optInValue && statedId === judgmentId;
|
|
1839
|
+
}
|
|
1840
|
+
const status = {
|
|
1679
1841
|
status: normalizeStatus(data.status),
|
|
1680
1842
|
verdict: normalizeVerdict(data.verdict),
|
|
1681
1843
|
allow: sameCall && this.executable(data),
|
|
1682
1844
|
reason: wireText(data.reason),
|
|
1683
1845
|
judgmentId: statedId || judgmentId,
|
|
1684
|
-
|
|
1685
|
-
cached: optBool(data.cached),
|
|
1686
|
-
// The Dashboard sends response_time_ms; responseTimeMs is the spec name.
|
|
1687
|
-
responseTimeMs: optNumber(data.responseTimeMs ?? data.response_time_ms)
|
|
1846
|
+
responseTimeMs: optNumber(data.response_time_ms)
|
|
1688
1847
|
};
|
|
1848
|
+
const mode = wireText(data.enforcement_mode);
|
|
1849
|
+
if (!modeTrusted || this.enforceLocally || !(mode === "shadow" || mode === "monitor" || mode === "off") || status.verdict !== "BLOCK" && status.verdict !== "HOLD") {
|
|
1850
|
+
return status;
|
|
1851
|
+
}
|
|
1852
|
+
this.logger.warn?.(`[atbash] would ${status.verdict.toLowerCase()} (${mode} mode): ${status.reason}`, {
|
|
1853
|
+
toolCallId: judgmentId
|
|
1854
|
+
});
|
|
1855
|
+
return { ...status, verdict: "ALLOW", allow: true, wouldVerdict: status.verdict };
|
|
1689
1856
|
});
|
|
1690
1857
|
}
|
|
1691
1858
|
/* ── risk-engine queries (action-dispatched GET) ───────────────────────── */
|
|
@@ -2187,14 +2354,14 @@ async function withDeadline(p, ms) {
|
|
|
2187
2354
|
clearTimeout(timer);
|
|
2188
2355
|
}
|
|
2189
2356
|
}
|
|
2190
|
-
function verifyResponseSignature(bodyBytes, resp,
|
|
2357
|
+
function verifyResponseSignature(bodyBytes, resp, verifyKeys) {
|
|
2191
2358
|
const sig = resp.headers.get("X-Atbash-Signature");
|
|
2192
2359
|
if (!sig) {
|
|
2193
2360
|
throw new SignatureVerificationError("missing X-Atbash-Signature header");
|
|
2194
2361
|
}
|
|
2195
|
-
let ok;
|
|
2362
|
+
let ok = false;
|
|
2196
2363
|
try {
|
|
2197
|
-
ok = native.verifySignature(bodyBytes, sig,
|
|
2364
|
+
ok = verifyKeys.some((key) => native.verifySignature(bodyBytes, sig, key));
|
|
2198
2365
|
} catch (err) {
|
|
2199
2366
|
throw new SignatureVerificationError(
|
|
2200
2367
|
`signature verification threw: ${errorMessage(err)}`
|
|
@@ -2286,10 +2453,6 @@ function optNumber(v) {
|
|
|
2286
2453
|
const n = Number(v);
|
|
2287
2454
|
return Number.isFinite(n) ? n : void 0;
|
|
2288
2455
|
}
|
|
2289
|
-
function optBool(v) {
|
|
2290
|
-
if (v === null || v === void 0) return void 0;
|
|
2291
|
-
return Boolean(v);
|
|
2292
|
-
}
|
|
2293
2456
|
function tryParseJson(text) {
|
|
2294
2457
|
try {
|
|
2295
2458
|
return JSON.parse(text);
|
|
@@ -3968,6 +4131,7 @@ export {
|
|
|
3968
4131
|
isEnvelope,
|
|
3969
4132
|
isJailHeld,
|
|
3970
4133
|
isValidPrivateKey,
|
|
4134
|
+
judgeVerifyKeys,
|
|
3971
4135
|
keyFingerprintOf,
|
|
3972
4136
|
keyPathCandidates,
|
|
3973
4137
|
loadAgent,
|
|
@@ -3994,6 +4158,7 @@ export {
|
|
|
3994
4158
|
saveUserConfig,
|
|
3995
4159
|
scanMemory,
|
|
3996
4160
|
scanMemoryBatch,
|
|
4161
|
+
setLoopbackJudgePinsForTests,
|
|
3997
4162
|
setupTelemetry,
|
|
3998
4163
|
shutdownTelemetry,
|
|
3999
4164
|
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
|