@atbash/sdk 0.19.2-dev.0 → 0.21.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/dist/index.js CHANGED
@@ -33,8 +33,6 @@ __export(src_ts_exports, {
33
33
  Atbash: () => Atbash,
34
34
  AtbashAPIError: () => AtbashAPIError,
35
35
  BOOT_SYNC_HINT: () => BOOT_SYNC_HINT,
36
- DEFAULT_BLOCKCHAIN_RID: () => DEFAULT_BLOCKCHAIN_RID,
37
- DEFAULT_CHROMIA_NODE_URLS: () => DEFAULT_CHROMIA_NODE_URLS,
38
36
  DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
39
37
  DEFAULT_MEMORY_PATH_PATTERNS: () => DEFAULT_MEMORY_PATH_PATTERNS,
40
38
  DEFAULT_MEMORY_READ_TOOL_NAMES: () => DEFAULT_MEMORY_READ_TOOL_NAMES,
@@ -42,11 +40,11 @@ __export(src_ts_exports, {
42
40
  EciesDomain: () => EciesDomain,
43
41
  HttpClient: () => HttpClient,
44
42
  HttpTransportError: () => HttpTransportError,
43
+ JudgeRefusalError: () => JudgeRefusalError,
45
44
  KEY_FILENAMES: () => KEY_FILENAMES,
46
45
  MemoryGuardManager: () => MemoryGuardManager,
47
46
  MemoryIntegrityError: () => MemoryIntegrityError,
48
47
  PRIVATE_CHAIN: () => PRIVATE_CHAIN,
49
- PUBLIC_CHAIN: () => PUBLIC_CHAIN,
50
48
  PointerStore: () => PointerStore,
51
49
  SignatureVerificationError: () => SignatureVerificationError,
52
50
  bootSyncFailureLine: () => bootSyncFailureLine,
@@ -60,6 +58,7 @@ __export(src_ts_exports, {
60
58
  columnAad: () => columnAad,
61
59
  commitMemoryVersion: () => commitMemoryVersion,
62
60
  configureEvents: () => configureEvents,
61
+ configureJailHold: () => configureJailHold,
63
62
  containsEvasionCharacters: () => containsEvasionCharacters,
64
63
  containsSecret: () => containsSecret,
65
64
  createFileLogger: () => createFileLogger,
@@ -88,12 +87,14 @@ __export(src_ts_exports, {
88
87
  getRollbackHistory: () => getRollbackHistory,
89
88
  guardMemoryWrite: () => guardMemoryWrite,
90
89
  isEnvelope: () => isEnvelope,
90
+ isJailHeld: () => isJailHeld,
91
91
  isValidPrivateKey: () => isValidPrivateKey,
92
92
  keyFingerprintOf: () => keyFingerprintOf,
93
93
  keyPathCandidates: () => keyPathCandidates,
94
94
  loadAgent: () => loadAgent,
95
95
  loadAgentFromFile: () => loadAgentFromFile,
96
96
  loadUserConfig: () => loadUserConfig,
97
+ memorySlotOf: () => memorySlotOf,
97
98
  normalizeActionForHash: () => normalizeActionForHash,
98
99
  normalizeActionType: () => normalizeActionType,
99
100
  normalizeForMatching: () => normalizeForMatching,
@@ -107,6 +108,7 @@ __export(src_ts_exports, {
107
108
  recordEvent: () => recordEvent,
108
109
  redactJsonStrings: () => redactJsonStrings,
109
110
  redactSecrets: () => redactSecrets,
111
+ releaseJailHolds: () => releaseJailHolds,
110
112
  resolve: () => resolve,
111
113
  resolveKeyPath: () => resolveKeyPath,
112
114
  rollbackMemory: () => rollbackMemory,
@@ -170,32 +172,28 @@ function agentAuthHeaders(auth, blockchainRid) {
170
172
  return { Authorization: `Bearer ${agentAuthBearer(auth, blockchainRid)}` };
171
173
  }
172
174
 
173
- // src-ts/constants.ts
174
- var DEFAULT_ENDPOINT = native.DEFAULT_ENDPOINT;
175
- var DEFAULT_CHROMIA_NODE_URLS = Object.freeze(
176
- native.defaultChromiaNodeUrls()
177
- );
178
- var DEFAULT_BLOCKCHAIN_RID = native.DEFAULT_BLOCKCHAIN_RID;
179
-
180
175
  // src-ts/chain-config.ts
181
176
  var DEFAULT_PRIVATE_NODE_URLS = Object.freeze(
182
177
  native.defaultPrivateNodeUrls()
183
178
  );
184
179
  var DEFAULT_PRIVATE_BLOCKCHAIN_RID = native.DEFAULT_PRIVATE_BLOCKCHAIN_RID;
185
- var PUBLIC_CHAIN = {
186
- network: "public",
187
- blockchainRid: DEFAULT_BLOCKCHAIN_RID,
188
- nodeUrls: DEFAULT_CHROMIA_NODE_URLS
189
- };
190
180
  var PRIVATE_CHAIN = {
191
181
  network: "private",
192
182
  blockchainRid: DEFAULT_PRIVATE_BLOCKCHAIN_RID,
193
183
  nodeUrls: DEFAULT_PRIVATE_NODE_URLS
194
184
  };
195
185
  function chainForNetwork(network) {
196
- return network === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
186
+ if (network !== "private") {
187
+ throw new Error(
188
+ `network must be "private" (the public chain is retired), got ${JSON.stringify(network)}`
189
+ );
190
+ }
191
+ return PRIVATE_CHAIN;
197
192
  }
198
193
 
194
+ // src-ts/constants.ts
195
+ var DEFAULT_ENDPOINT = native.DEFAULT_ENDPOINT;
196
+
199
197
  // src-ts/encrypted-toolcall.ts
200
198
  function columnAad(toolCallId, column) {
201
199
  return `${toolCallId}:${column}`;
@@ -293,12 +291,12 @@ var AtbashAPIError = class extends Error {
293
291
  * ran does not carry. The path is the fact that settles it.
294
292
  */
295
293
  path;
296
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path5) {
297
- super(enrich(status, body, statusText, endpoint, path5));
294
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path4) {
295
+ super(enrich(status, body, statusText, endpoint, path4));
298
296
  this.name = "AtbashAPIError";
299
297
  this.status = status;
300
298
  this.body = body;
301
- this.path = path5;
299
+ this.path = path4;
302
300
  }
303
301
  };
304
302
  var SignatureVerificationError = class extends Error {
@@ -307,9 +305,18 @@ var SignatureVerificationError = class extends Error {
307
305
  this.name = "SignatureVerificationError";
308
306
  }
309
307
  };
310
- function enrich(status, body, statusText, endpoint, path5) {
308
+ var JudgeRefusalError = class extends Error {
309
+ constructor(message) {
310
+ super(message);
311
+ this.name = "JudgeRefusalError";
312
+ }
313
+ };
314
+ function isRefusalStatus(status) {
315
+ return status >= 400 && status < 500 && status !== 408;
316
+ }
317
+ function enrich(status, body, statusText, endpoint, path4) {
311
318
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
312
- let msg = `API error ${status}${path5 ? ` from ${path5}` : ""}: ${body || statusText}`;
319
+ let msg = `API error ${status}${path4 ? ` from ${path4}` : ""}: ${body || statusText}`;
313
320
  const lowered = body.toLowerCase();
314
321
  if (lowered.includes("agent not registered")) {
315
322
  msg += `
@@ -330,6 +337,61 @@ function enrich(status, body, statusText, endpoint, path5) {
330
337
  return msg;
331
338
  }
332
339
 
340
+ // src-ts/jail-hold.ts
341
+ var DEFAULTS = { ceilingMs: 3e4, pollMs: 1e3 };
342
+ var holds = /* @__PURE__ */ new Map();
343
+ var blockedAt = /* @__PURE__ */ new Map();
344
+ var BLOCK_MEMORY_MS = 10 * 6e4;
345
+ var config = { ...DEFAULTS };
346
+ function isJailHeld(pubkey) {
347
+ return holds.has(pubkey);
348
+ }
349
+ function recordBlock(pubkey) {
350
+ const now = Date.now();
351
+ for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
352
+ blockedAt.set(pubkey, now);
353
+ }
354
+ function blockedSince(pubkey, startedAt) {
355
+ const at = blockedAt.get(pubkey);
356
+ return at !== void 0 && startedAt <= at;
357
+ }
358
+ function holdJail(pubkey, confirmJailed, onGiveUp) {
359
+ if (holds.has(pubkey)) return;
360
+ const hold = { timer: null };
361
+ holds.set(pubkey, hold);
362
+ const deadline = Date.now() + config.ceilingMs;
363
+ const tick = async () => {
364
+ if (holds.get(pubkey) !== hold) return;
365
+ let jailed = false;
366
+ try {
367
+ jailed = await confirmJailed();
368
+ } catch {
369
+ }
370
+ if (holds.get(pubkey) !== hold) return;
371
+ if (jailed) {
372
+ holds.delete(pubkey);
373
+ return;
374
+ }
375
+ if (Date.now() >= deadline) {
376
+ holds.delete(pubkey);
377
+ onGiveUp?.();
378
+ return;
379
+ }
380
+ const timer = setTimeout(() => void tick(), config.pollMs);
381
+ timer.unref?.();
382
+ hold.timer = timer;
383
+ };
384
+ void tick();
385
+ }
386
+ function releaseJailHolds() {
387
+ for (const hold of holds.values()) if (hold.timer) clearTimeout(hold.timer);
388
+ holds.clear();
389
+ blockedAt.clear();
390
+ }
391
+ function configureJailHold(next) {
392
+ config = { ...DEFAULTS, ...next };
393
+ }
394
+
333
395
  // src-ts/http/client.ts
334
396
  var HttpClient = class {
335
397
  baseUrl;
@@ -338,8 +400,8 @@ var HttpClient = class {
338
400
  this.baseUrl = baseUrl.replace(/\/+$/, "");
339
401
  this.timeoutMs = timeoutMs;
340
402
  }
341
- buildUrl(path5, query) {
342
- const url = new URL(this.baseUrl + path5);
403
+ buildUrl(path4, query) {
404
+ const url = new URL(this.baseUrl + path4);
343
405
  if (query) {
344
406
  for (const [k, v] of Object.entries(query)) {
345
407
  if (v !== void 0 && v !== null && v !== "") {
@@ -349,14 +411,14 @@ var HttpClient = class {
349
411
  }
350
412
  return url.toString();
351
413
  }
352
- async get(path5, query, headers) {
353
- return this.fetch(this.buildUrl(path5, query), {
414
+ async get(path4, query, headers) {
415
+ return this.fetch(this.buildUrl(path4, query), {
354
416
  method: "GET",
355
417
  ...headers && { headers }
356
418
  });
357
419
  }
358
- async post(path5, body, headers) {
359
- return this.fetch(this.buildUrl(path5), {
420
+ async post(path4, body, headers) {
421
+ return this.fetch(this.buildUrl(path4), {
360
422
  method: "POST",
361
423
  headers: { "Content-Type": "application/json", ...headers },
362
424
  body: JSON.stringify(body)
@@ -674,7 +736,7 @@ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
674
736
  var import_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
675
737
  var import_sdk_logs = require("@opentelemetry/sdk-logs");
676
738
  var import_node_crypto = require("crypto");
677
- var SDK_VERSION = true ? "0.19.2-dev.0" : "";
739
+ var SDK_VERSION = true ? "0.21.0-dev.0" : "";
678
740
  var SDK_LANGUAGE = "node";
679
741
  function sdkResourceAttributes() {
680
742
  const attrs = {
@@ -705,8 +767,8 @@ function isTelemetryOptedOut() {
705
767
  const filePath = (0, import_node_path2.join)(home2, ".config", "atbash", "telemetry.json");
706
768
  const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8").trim();
707
769
  if (!raw) return false;
708
- const config = JSON.parse(raw);
709
- return config.enabled === false;
770
+ const config2 = JSON.parse(raw);
771
+ return config2.enabled === false;
710
772
  } catch {
711
773
  return false;
712
774
  }
@@ -751,26 +813,26 @@ function customCollector() {
751
813
  }
752
814
  return { url: url.replace(/\/+$/, ""), headers: () => headers };
753
815
  }
754
- function setupTelemetry(config) {
755
- if (!config.enabled) return;
816
+ function setupTelemetry(config2) {
817
+ if (!config2.enabled) return;
756
818
  if (isTelemetryOptedOut()) return;
757
- if (config.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config.source)) {
758
- defaultSource = config.source;
819
+ if (config2.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config2.source)) {
820
+ defaultSource = config2.source;
759
821
  sourceExplicitlySet = true;
760
822
  }
761
- if (config.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
762
- requestedExportIntervalMs = config.exportIntervalMs;
823
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
824
+ requestedExportIntervalMs = config2.exportIntervalMs;
763
825
  }
764
826
  if (meterProvider) return;
765
827
  const collector = customCollector();
766
- if (!collector && (!config.endpoint || !config.getAuthHeaders)) return;
828
+ if (!collector && (!config2.endpoint || !config2.getAuthHeaders)) return;
767
829
  if (!collector && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
768
- config.endpoint
830
+ config2.endpoint
769
831
  ))
770
832
  return;
771
833
  const destination = collector ?? {
772
- url: `${config.endpoint.replace(/\/+$/, "")}/api/telemetry`,
773
- headers: () => config.getAuthHeaders()
834
+ url: `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`,
835
+ headers: () => config2.getAuthHeaders()
774
836
  };
775
837
  const proxyUrl = destination.url;
776
838
  const exporter = new import_exporter_metrics_otlp_http.OTLPMetricExporter({
@@ -805,7 +867,7 @@ function setupTelemetry(config) {
805
867
  description: "SDK function execution duration",
806
868
  unit: "ms"
807
869
  });
808
- counterHasher = config.hashAgentKey ?? defaultAgentKeyHasher;
870
+ counterHasher = config2.hashAgentKey ?? defaultAgentKeyHasher;
809
871
  const logExporter = new import_exporter_logs_otlp_http.OTLPLogExporter({
810
872
  // A real collector takes OTLP at its own signal paths; the Atbash proxy
811
873
  // takes a query parameter, because it is one authenticated route.
@@ -824,8 +886,8 @@ function setupTelemetry(config) {
824
886
  });
825
887
  }
826
888
  configureEvents(loggerProvider.getLogger("atbash-sdk"), {
827
- source: sourceOrDefault(config.source),
828
- hashAgentKey: config.hashAgentKey ?? defaultAgentKeyHasher,
889
+ source: sourceOrDefault(config2.source),
890
+ hashAgentKey: config2.hashAgentKey ?? defaultAgentKeyHasher,
829
891
  version: SDK_VERSION
830
892
  });
831
893
  }
@@ -897,13 +959,13 @@ function loadUserConfig() {
897
959
  return {};
898
960
  }
899
961
  }
900
- function saveUserConfig(config) {
962
+ function saveUserConfig(config2) {
901
963
  const dir = getConfigDir();
902
964
  if (!(0, import_node_fs3.existsSync)(dir)) {
903
965
  (0, import_node_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
904
966
  }
905
967
  const filePath = getConfigPath();
906
- (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(config, null, 2) + "\n", {
968
+ (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(config2, null, 2) + "\n", {
907
969
  mode: 384
908
970
  });
909
971
  (0, import_node_fs3.chmodSync)(filePath, 384);
@@ -929,15 +991,16 @@ function resolve(key, flagValue) {
929
991
  function forcedChainNetwork(flagValue) {
930
992
  const raw = resolve("defaultChainNetwork", flagValue);
931
993
  if (!raw) return void 0;
932
- if (raw !== "public" && raw !== "private") {
994
+ if (raw !== "private") {
933
995
  throw new Error(
934
- `ATBASH_DEFAULT_CHAIN_NETWORK / defaultChainNetwork must be "public" or "private", got ${JSON.stringify(raw)} \u2014 unset it to let each org's chain decide.`
996
+ `ATBASH_DEFAULT_CHAIN_NETWORK / defaultChainNetwork must be "private" (the public chain is retired), got ${JSON.stringify(raw)} \u2014 unset it to let each org's chain decide.`
935
997
  );
936
998
  }
937
999
  return raw;
938
1000
  }
939
1001
 
940
1002
  // src-ts/client.ts
1003
+ var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
941
1004
  function generateToolCallId() {
942
1005
  return `tc-${Date.now()}-${randomHex(4)}`;
943
1006
  }
@@ -947,15 +1010,13 @@ function resolveConstructorChain(options) {
947
1010
  const hasBrid = options.blockchainRid !== void 0;
948
1011
  if (hasNodeUrls !== hasBrid) {
949
1012
  throw new Error(
950
- 'nodeUrls and blockchainRid must be provided together \u2014 passing one without the other 404s every chain request. Prefer `chain: PUBLIC_CHAIN | PRIVATE_CHAIN` or `network: "public" | "private"`.'
1013
+ 'nodeUrls and blockchainRid must be provided together \u2014 passing one without the other 404s every chain request. Prefer `chain: PRIVATE_CHAIN` or `network: "private"`.'
951
1014
  );
952
1015
  }
953
1016
  if (hasNodeUrls && hasBrid) {
954
- const brid = options.blockchainRid;
955
- const derivedNetwork = options.network ?? (brid === PUBLIC_CHAIN.blockchainRid ? "public" : brid === PRIVATE_CHAIN.blockchainRid ? "private" : "private");
956
1017
  return {
957
- network: derivedNetwork,
958
- blockchainRid: brid,
1018
+ network: "private",
1019
+ blockchainRid: options.blockchainRid,
959
1020
  nodeUrls: options.nodeUrls
960
1021
  };
961
1022
  }
@@ -1095,10 +1156,9 @@ var Atbash = class _Atbash {
1095
1156
  _Atbash.environmentLogged = true;
1096
1157
  const brief = (rid) => rid ? `${rid.slice(0, 8)}\u2026` : "(unset)";
1097
1158
  this.logger.info?.(
1098
- `[atbash] environment \u2014 judge=${this.endpoint} publicChain=${brief(native.DEFAULT_BLOCKCHAIN_RID)} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyPubKey ? "on" : "off"}`,
1159
+ `[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyPubKey ? "on" : "off"}`,
1099
1160
  {
1100
1161
  judgeEndpoint: this.endpoint,
1101
- publicBlockchainRid: native.DEFAULT_BLOCKCHAIN_RID,
1102
1162
  privateBlockchainRid: native.DEFAULT_PRIVATE_BLOCKCHAIN_RID,
1103
1163
  activeBlockchainRid: this.blockchainRid,
1104
1164
  responseSignatureCheck: Boolean(this.verifyPubKey)
@@ -1286,6 +1346,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1286
1346
  * return the signed tx hex. The server broadcasts to chain.
1287
1347
  */
1288
1348
  async logToolCall(action, context = "", options = {}) {
1349
+ return this._logToolCall(action, context, options, false);
1350
+ }
1351
+ /**
1352
+ * `raisePreflightErrors` (the judge path, like Python's
1353
+ * `raise_preflight_errors`): a failed registration check throws instead of
1354
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
1355
+ * (not registered, or a 4xx) from an outage.
1356
+ */
1357
+ async _logToolCall(action, context, options, raisePreflightErrors) {
1289
1358
  const start = performance.now();
1290
1359
  recordCall("logToolCall", void 0, this.auth.pubkey);
1291
1360
  let exists;
@@ -1295,18 +1364,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1295
1364
  });
1296
1365
  } catch (err) {
1297
1366
  recordDuration("logToolCall", performance.now() - start, "error");
1367
+ if (raisePreflightErrors) throw err;
1298
1368
  return { success: false, toolCallId: null, error: errorMessage(err) };
1299
1369
  }
1300
1370
  if (!exists) {
1301
1371
  recordDuration("logToolCall", performance.now() - start, "error");
1372
+ if (raisePreflightErrors) throw new JudgeRefusalError(AGENT_NOT_REGISTERED);
1302
1373
  return {
1303
1374
  success: false,
1304
1375
  toolCallId: null,
1305
- error: "Agent not registered. Onboard the agent at the dashboard before submitting actions."
1376
+ error: AGENT_NOT_REGISTERED
1306
1377
  };
1307
1378
  }
1308
1379
  const toolCallId = generateToolCallId();
1309
- const brid = options.chainOpts?.blockchainRid || options.chainOpts?.network ? this.bridFromChainOpts(options.chainOpts) : await this.defaultOrgBrid() ?? this.blockchainRid;
1380
+ const brid = await this.callBrid(options.chainOpts);
1310
1381
  const orgKey = options.orgEncryptionPubKey ?? this.orgEncryptionPubKey ?? this._orgKeyFromChain;
1311
1382
  try {
1312
1383
  const signedHex = orgKey ? signEncryptedToolCall(
@@ -1356,9 +1427,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1356
1427
  if (!action?.trim()) {
1357
1428
  throw new Error("action is required and cannot be empty.");
1358
1429
  }
1430
+ if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
1431
+ const startedAt = Date.now();
1359
1432
  let chainOpts = options.chainOpts;
1360
1433
  if (options.orgName && this._explicitChain) {
1361
- chainOpts = options.chainOpts ?? { network: this._defaultChain.network };
1434
+ chainOpts = options.chainOpts ?? {
1435
+ blockchainRid: this._defaultChain.blockchainRid
1436
+ };
1362
1437
  } else if (options.orgName && this.forcedNetwork()) {
1363
1438
  chainOpts = { network: this.forcedNetwork() };
1364
1439
  } else if (options.orgName) {
@@ -1368,8 +1443,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1368
1443
  } else {
1369
1444
  const mapNetwork = await this.getActiveNetworkForOrg(options.orgName);
1370
1445
  if (mapNetwork) {
1371
- const chain = mapNetwork === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
1372
- this._chainCache.set(options.orgName, chain);
1446
+ this._chainCache.set(options.orgName, chainForNetwork(mapNetwork));
1373
1447
  chainOpts = { network: mapNetwork };
1374
1448
  } else if (!chainOpts?.blockchainRid) {
1375
1449
  const resolved = await this.resolveChainFromMap(
@@ -1380,12 +1454,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1380
1454
  }
1381
1455
  }
1382
1456
  }
1383
- const brid = this.bridFromChainOpts(chainOpts);
1384
- const logResult = await this.logToolCall(action, context, {
1385
- toolName: options.toolName,
1386
- toolArgsJson: options.toolArgsJson,
1387
- chainOpts
1388
- });
1457
+ const brid = await this.callBrid(chainOpts);
1458
+ const logResult = await this._logToolCall(
1459
+ action,
1460
+ context,
1461
+ { toolName: options.toolName, toolArgsJson: options.toolArgsJson, chainOpts },
1462
+ true
1463
+ );
1389
1464
  if (!logResult.success || !logResult.toolCallId || !logResult.signedHex) {
1390
1465
  throw new Error(logResult.error || "Failed to sign log_tool_call");
1391
1466
  }
@@ -1422,10 +1497,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1422
1497
  if (verifyPubKey !== void 0) {
1423
1498
  verifyResponseSignature(bodyBytes, resp, verifyPubKey);
1424
1499
  }
1425
- const data = parseJson(bodyBytes);
1500
+ const parsed = tryParseJson(bodyBytes.toString("utf-8"));
1501
+ if (!isRecord2(parsed)) {
1502
+ throw new JudgeRefusalError(
1503
+ "The judge's reply could not be read, so the action was not approved."
1504
+ );
1505
+ }
1506
+ const data = parsed;
1426
1507
  const rawScore = data.score;
1427
1508
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
1428
- return {
1509
+ const result = {
1429
1510
  verdict: normalizeVerdict(data.verdict),
1430
1511
  allow: this.executable(data),
1431
1512
  actionType: normalizeActionType(data.action_type),
@@ -1441,6 +1522,45 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1441
1522
  score,
1442
1523
  refusal: explicitRefusal(data)
1443
1524
  };
1525
+ return this.applyJailHold(result, startedAt);
1526
+ }
1527
+ /**
1528
+ * Start a local hold on an enforced, on-chain BLOCK (the verdict that jails).
1529
+ * Refuse any other result that lands while the agent is held, or whose
1530
+ * request was already in flight when the block arrived: the server judged it
1531
+ * on pre-jail state.
1532
+ */
1533
+ applyJailHold(result, startedAt) {
1534
+ const pubkey = this.auth.pubkey;
1535
+ if (result.verdict === "BLOCK" && result.enforced && result.onChain) {
1536
+ recordBlock(pubkey);
1537
+ holdJail(
1538
+ pubkey,
1539
+ () => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
1540
+ () => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
1541
+ );
1542
+ return result;
1543
+ }
1544
+ return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
1545
+ }
1546
+ /** The server's jailed refusal, produced locally while the agent is held. */
1547
+ localJailBlock(toolCallId = generateToolCallId()) {
1548
+ return {
1549
+ verdict: "BLOCK",
1550
+ allow: false,
1551
+ actionType: "block",
1552
+ reason: "Agent is jailed. All actions are blocked until the organization owner unjails this agent.",
1553
+ confidence: 1,
1554
+ provider: "policy",
1555
+ latencyMs: 0,
1556
+ toolCallId,
1557
+ onChain: false,
1558
+ enforced: true,
1559
+ enforcementMode: "enforce",
1560
+ status: "",
1561
+ score: void 0,
1562
+ refusal: "BLOCK"
1563
+ };
1444
1564
  }
1445
1565
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
1446
1566
  /**
@@ -1492,6 +1612,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1492
1612
  reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
1493
1613
  };
1494
1614
  }
1615
+ if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
1616
+ return { ...this.failJudge(errorMessage(err), err), allow: false };
1617
+ }
1495
1618
  return this.failJudge(errorMessage(err), err);
1496
1619
  }
1497
1620
  }
@@ -1521,6 +1644,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1521
1644
  * never overrides a refusal.
1522
1645
  */
1523
1646
  decide(result) {
1647
+ const inner = this.decideInner(result);
1648
+ if (!result.enforcementMode) return inner;
1649
+ return {
1650
+ ...inner,
1651
+ enforced: result.enforced,
1652
+ enforcementMode: result.enforcementMode
1653
+ };
1654
+ }
1655
+ decideInner(result) {
1524
1656
  const toolCallId = result.toolCallId;
1525
1657
  const deny = (verdict, reason) => ({
1526
1658
  allow: false,
@@ -1634,17 +1766,17 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1634
1766
  /**
1635
1767
  * Return the current status of a previously submitted judgment.
1636
1768
  *
1637
- * `chainOpts` names which chain the judgment was signed against. The
1638
- * server's GET /api/v1/judge routes to that chain when the SDK sends
1639
- * a `brid` query param; without it, the server falls back to public.
1640
- * Callers on the private chain must pass a `chainOpts` (or configure
1641
- * the client on the private chain) — otherwise polling a POSTed
1642
- * judgment on the private chain 404s at the server.
1769
+ * `chainOpts` names which chain the judgment was signed against; the SDK
1770
+ * sends it as the `brid` query param the server routes by. Without it the
1771
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
1772
+ * client's org chain when it has an `orgName`, else the client default.
1773
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
1774
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
1643
1775
  */
1644
1776
  async getJudgmentStatus(judgmentId, agentPubkey, chainOpts) {
1645
1777
  const pk = agentPubkey ?? this.auth.pubkey;
1646
- const brid = this.bridFromChainOpts(chainOpts);
1647
1778
  return this.track("getJudgmentStatus", pk, async () => {
1779
+ const brid = await this.callBrid(chainOpts);
1648
1780
  const resp = await this.http.get(
1649
1781
  "/api/v1/judge",
1650
1782
  { tool_call_id: judgmentId, agent_pubkey: pk, brid },
@@ -1849,21 +1981,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1849
1981
  }
1850
1982
  if (resp.status !== 200) throw await this.httpError(resp);
1851
1983
  const data = await this.json(resp);
1852
- if (data?.network === "public" || data?.network === "private") {
1853
- return data.network;
1854
- }
1984
+ if (data?.network === "private") return data.network;
1855
1985
  return null;
1856
1986
  }
1857
1987
  /**
1858
1988
  * Resolve which chain an org's actions should run against. Cached
1859
1989
  * per-client by orgName. Resolution order:
1860
1990
  * 1. `org_networks` map (authoritative).
1861
- * 2. Per-chain subscription fallback — public + private records
1862
- * are fetched in parallel, with `is_private_blockchain` and
1863
- * `assigned_at` reconciling mixed states.
1864
- * A lookup that names exactly one chain wins outright. Where it names
1865
- * neither (a brand-new org) or cannot choose between them, the client's
1866
- * configured default decides.
1991
+ * 2. Subscription fallback — the org's subscription row on the chain.
1992
+ * A lookup that finds the org wins outright. Where it finds nothing (a
1993
+ * brand-new org), the client's configured default decides.
1867
1994
  */
1868
1995
  async resolveChainForOrg(orgName) {
1869
1996
  const name = orgName.trim();
@@ -1885,35 +2012,14 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1885
2012
  const cached = this._chainCache.get(orgName);
1886
2013
  if (cached) return cached;
1887
2014
  if (mapNetwork) {
1888
- const chain = mapNetwork === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
1889
- this._chainCache.set(orgName, chain);
1890
- return chain;
1891
- }
1892
- const [pubRes, privRes] = await Promise.allSettled([
1893
- this.getOrgSubscription(orgName, "public"),
1894
- this.getOrgSubscription(orgName, "private")
1895
- ]);
1896
- const pubSub = pubRes.status === "fulfilled" ? pubRes.value : null;
1897
- const privSub = privRes.status === "fulfilled" ? privRes.value : null;
1898
- const failure2 = pubRes.status === "rejected" ? pubRes.reason : privRes.status === "rejected" ? privRes.reason : null;
1899
- if (pubSub?.is_private_blockchain) {
1900
- this._chainCache.set(orgName, PRIVATE_CHAIN);
1901
- return PRIVATE_CHAIN;
1902
- }
1903
- if (pubSub && privSub) {
1904
- const chain = privSub.assigned_at === pubSub.assigned_at ? this._defaultChain : privSub.assigned_at > pubSub.assigned_at ? PRIVATE_CHAIN : PUBLIC_CHAIN;
2015
+ const chain = chainForNetwork(mapNetwork);
1905
2016
  this._chainCache.set(orgName, chain);
1906
2017
  return chain;
1907
2018
  }
1908
- if (pubSub) {
1909
- this._chainCache.set(orgName, PUBLIC_CHAIN);
1910
- return PUBLIC_CHAIN;
1911
- }
1912
- if (privSub?.is_private_blockchain) {
1913
- this._chainCache.set(orgName, PRIVATE_CHAIN);
1914
- return PRIVATE_CHAIN;
1915
- }
1916
- if (failure2) {
2019
+ let privSub;
2020
+ try {
2021
+ privSub = await this.getOrgSubscription(orgName, "private");
2022
+ } catch (failure2) {
1917
2023
  const detail = failure2 instanceof Error ? failure2.message : String(failure2);
1918
2024
  throw new AtbashAPIError(
1919
2025
  failure2 instanceof AtbashAPIError ? failure2.status : 0,
@@ -1922,6 +2028,10 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1922
2028
  this.endpoint
1923
2029
  );
1924
2030
  }
2031
+ if (privSub) {
2032
+ this._chainCache.set(orgName, PRIVATE_CHAIN);
2033
+ return PRIVATE_CHAIN;
2034
+ }
1925
2035
  this._chainCache.set(orgName, this._defaultChain);
1926
2036
  return this._defaultChain;
1927
2037
  }
@@ -1978,10 +2088,21 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1978
2088
  */
1979
2089
  bridFromChainOpts(chainOpts) {
1980
2090
  if (chainOpts?.blockchainRid) return chainOpts.blockchainRid;
1981
- if (chainOpts?.network === "private") return PRIVATE_CHAIN.blockchainRid;
1982
- if (chainOpts?.network === "public") return PUBLIC_CHAIN.blockchainRid;
2091
+ if (chainOpts?.network) return chainForNetwork(chainOpts.network).blockchainRid;
1983
2092
  return this.blockchainRid;
1984
2093
  }
2094
+ /**
2095
+ * The chain one call's transactions are signed for: the per-call override
2096
+ * when given, otherwise the client's org, otherwise the client default.
2097
+ * log_tool_call, judge_action and the judgment-status read all use this, so
2098
+ * a request never pairs transactions signed for two different chains.
2099
+ */
2100
+ async callBrid(chainOpts) {
2101
+ if (chainOpts?.blockchainRid || chainOpts?.network) {
2102
+ return this.bridFromChainOpts(chainOpts);
2103
+ }
2104
+ return await this.defaultOrgBrid() ?? this.blockchainRid;
2105
+ }
1985
2106
  /**
1986
2107
  * Resolve the dashboard chain used by agent metadata/policy reads.
1987
2108
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -2027,8 +2148,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
2027
2148
  async riskEnginePost(body, network, brid) {
2028
2149
  let resp;
2029
2150
  try {
2030
- const path5 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2031
- resp = await this.http.post(path5, body, this.authHeaders(brid));
2151
+ const path4 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2152
+ resp = await this.http.post(path4, body, this.authHeaders(brid));
2032
2153
  } catch (err) {
2033
2154
  throw this.transportError(err);
2034
2155
  }
@@ -2289,10 +2410,6 @@ function tryParseJson(text) {
2289
2410
  return void 0;
2290
2411
  }
2291
2412
  }
2292
- function parseJson(bytes) {
2293
- const parsed = tryParseJson(bytes.toString("utf-8"));
2294
- return parsed === void 0 ? {} : parsed;
2295
- }
2296
2413
  function requestPath(url) {
2297
2414
  if (!url) return void 0;
2298
2415
  try {
@@ -2568,13 +2685,13 @@ function resolveBlockchainRid(chainOpts) {
2568
2685
  if (chainOpts?.blockchainRid !== void 0) return chainOpts.blockchainRid;
2569
2686
  if (chainOpts?.nodeUrls !== void 0) {
2570
2687
  throw new Error(
2571
- 'chainOpts.nodeUrls no longer selects a chain on its own \u2014 memory reaches the chain through the Atbash service, not a node. Set `blockchainRid`, or `network: "public" | "private"`.'
2688
+ 'chainOpts.nodeUrls no longer selects a chain on its own \u2014 memory reaches the chain through the Atbash service, not a node. Set `blockchainRid`, or `network: "private"`.'
2572
2689
  );
2573
2690
  }
2574
- const config = chainForNetwork(
2691
+ const config2 = chainForNetwork(
2575
2692
  chainOpts?.network ?? forcedChainNetwork() ?? "private"
2576
2693
  );
2577
- return config.blockchainRid;
2694
+ return config2.blockchainRid;
2578
2695
  }
2579
2696
  async function commitMemoryVersion(plaintext, auth, opts) {
2580
2697
  const key = await deriveMemoryKey(auth.privkey);
@@ -2800,7 +2917,34 @@ function classifyMemoryWrite(event, ctx, opts = {}) {
2800
2917
  // src-ts/memory/guard.ts
2801
2918
  var import_node_fs4 = require("fs");
2802
2919
  var import_node_os4 = __toESM(require("os"));
2920
+
2921
+ // src-ts/memory/slot.ts
2803
2922
  var import_node_path4 = __toESM(require("path"));
2923
+ function asciiLower(s) {
2924
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) | 32));
2925
+ }
2926
+ function spelledAs(target, base, p = import_node_path4.default) {
2927
+ const resolved = p.resolve(base);
2928
+ if (p.sep !== "\\") return resolved;
2929
+ const prefix = resolved.replace(/[\\/]+$/, "");
2930
+ const t = asciiLower(target);
2931
+ const b = asciiLower(prefix);
2932
+ if (t === b || t.startsWith(b + "\\") || t.startsWith(b + "/")) {
2933
+ return target.slice(0, prefix.length);
2934
+ }
2935
+ return resolved;
2936
+ }
2937
+ function memorySlotOf(target, home2, root, extraHomes, p = import_node_path4.default) {
2938
+ const resolved = p.resolve(target);
2939
+ return native.memorySlotForPath(
2940
+ resolved,
2941
+ home2 ? spelledAs(resolved, home2, p) : "",
2942
+ root ? spelledAs(resolved, root, p) : void 0,
2943
+ extraHomes
2944
+ );
2945
+ }
2946
+
2947
+ // src-ts/memory/guard.ts
2804
2948
  function emitDebugProbe(event, ctx, memEntry, logger2) {
2805
2949
  if (!logger2?.info) return;
2806
2950
  const ev = event ?? {};
@@ -2826,7 +2970,7 @@ function emitDebugProbe(event, ctx, memEntry, logger2) {
2826
2970
  }
2827
2971
  function memorySlotFor(key, root, extraHomes) {
2828
2972
  const home2 = process.env.HOME || import_node_os4.default.homedir() || "";
2829
- return native.memorySlotForPath(import_node_path4.default.resolve(key), home2, root, extraHomes);
2973
+ return memorySlotOf(key, home2, root, extraHomes);
2830
2974
  }
2831
2975
  async function resultingDocument(memEntry, event, ctx) {
2832
2976
  const edits = native.extractMemoryEdits(event, ctx);
@@ -3579,12 +3723,7 @@ var MemoryGuardManager = class {
3579
3723
  */
3580
3724
  slotFor(target) {
3581
3725
  const home2 = process.env.HOME || import_node_os5.default.homedir() || "";
3582
- return native.memorySlotForPath(
3583
- import_node_path7.default.resolve(target),
3584
- home2,
3585
- this.opts.memoryRoot,
3586
- this.opts.agentHomeDirs
3587
- );
3726
+ return memorySlotOf(target, home2, this.opts.memoryRoot, this.opts.agentHomeDirs);
3588
3727
  }
3589
3728
  /**
3590
3729
  * Verify every memory file this call reads, each against its own chain slot.
@@ -3865,8 +4004,6 @@ function diffMemorySnapshots(before, after) {
3865
4004
  Atbash,
3866
4005
  AtbashAPIError,
3867
4006
  BOOT_SYNC_HINT,
3868
- DEFAULT_BLOCKCHAIN_RID,
3869
- DEFAULT_CHROMIA_NODE_URLS,
3870
4007
  DEFAULT_ENDPOINT,
3871
4008
  DEFAULT_MEMORY_PATH_PATTERNS,
3872
4009
  DEFAULT_MEMORY_READ_TOOL_NAMES,
@@ -3874,11 +4011,11 @@ function diffMemorySnapshots(before, after) {
3874
4011
  EciesDomain,
3875
4012
  HttpClient,
3876
4013
  HttpTransportError,
4014
+ JudgeRefusalError,
3877
4015
  KEY_FILENAMES,
3878
4016
  MemoryGuardManager,
3879
4017
  MemoryIntegrityError,
3880
4018
  PRIVATE_CHAIN,
3881
- PUBLIC_CHAIN,
3882
4019
  PointerStore,
3883
4020
  SignatureVerificationError,
3884
4021
  bootSyncFailureLine,
@@ -3892,6 +4029,7 @@ function diffMemorySnapshots(before, after) {
3892
4029
  columnAad,
3893
4030
  commitMemoryVersion,
3894
4031
  configureEvents,
4032
+ configureJailHold,
3895
4033
  containsEvasionCharacters,
3896
4034
  containsSecret,
3897
4035
  createFileLogger,
@@ -3920,12 +4058,14 @@ function diffMemorySnapshots(before, after) {
3920
4058
  getRollbackHistory,
3921
4059
  guardMemoryWrite,
3922
4060
  isEnvelope,
4061
+ isJailHeld,
3923
4062
  isValidPrivateKey,
3924
4063
  keyFingerprintOf,
3925
4064
  keyPathCandidates,
3926
4065
  loadAgent,
3927
4066
  loadAgentFromFile,
3928
4067
  loadUserConfig,
4068
+ memorySlotOf,
3929
4069
  normalizeActionForHash,
3930
4070
  normalizeActionType,
3931
4071
  normalizeForMatching,
@@ -3939,6 +4079,7 @@ function diffMemorySnapshots(before, after) {
3939
4079
  recordEvent,
3940
4080
  redactJsonStrings,
3941
4081
  redactSecrets,
4082
+ releaseJailHolds,
3942
4083
  resolve,
3943
4084
  resolveKeyPath,
3944
4085
  rollbackMemory,