@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.mjs CHANGED
@@ -43,32 +43,28 @@ function agentAuthHeaders(auth, blockchainRid) {
43
43
  return { Authorization: `Bearer ${agentAuthBearer(auth, blockchainRid)}` };
44
44
  }
45
45
 
46
- // src-ts/constants.ts
47
- var DEFAULT_ENDPOINT = native.DEFAULT_ENDPOINT;
48
- var DEFAULT_CHROMIA_NODE_URLS = Object.freeze(
49
- native.defaultChromiaNodeUrls()
50
- );
51
- var DEFAULT_BLOCKCHAIN_RID = native.DEFAULT_BLOCKCHAIN_RID;
52
-
53
46
  // src-ts/chain-config.ts
54
47
  var DEFAULT_PRIVATE_NODE_URLS = Object.freeze(
55
48
  native.defaultPrivateNodeUrls()
56
49
  );
57
50
  var DEFAULT_PRIVATE_BLOCKCHAIN_RID = native.DEFAULT_PRIVATE_BLOCKCHAIN_RID;
58
- var PUBLIC_CHAIN = {
59
- network: "public",
60
- blockchainRid: DEFAULT_BLOCKCHAIN_RID,
61
- nodeUrls: DEFAULT_CHROMIA_NODE_URLS
62
- };
63
51
  var PRIVATE_CHAIN = {
64
52
  network: "private",
65
53
  blockchainRid: DEFAULT_PRIVATE_BLOCKCHAIN_RID,
66
54
  nodeUrls: DEFAULT_PRIVATE_NODE_URLS
67
55
  };
68
56
  function chainForNetwork(network) {
69
- return network === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
57
+ if (network !== "private") {
58
+ throw new Error(
59
+ `network must be "private" (the public chain is retired), got ${JSON.stringify(network)}`
60
+ );
61
+ }
62
+ return PRIVATE_CHAIN;
70
63
  }
71
64
 
65
+ // src-ts/constants.ts
66
+ var DEFAULT_ENDPOINT = native.DEFAULT_ENDPOINT;
67
+
72
68
  // src-ts/encrypted-toolcall.ts
73
69
  function columnAad(toolCallId, column) {
74
70
  return `${toolCallId}:${column}`;
@@ -166,12 +162,12 @@ var AtbashAPIError = class extends Error {
166
162
  * ran does not carry. The path is the fact that settles it.
167
163
  */
168
164
  path;
169
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path5) {
170
- super(enrich(status, body, statusText, endpoint, path5));
165
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path4) {
166
+ super(enrich(status, body, statusText, endpoint, path4));
171
167
  this.name = "AtbashAPIError";
172
168
  this.status = status;
173
169
  this.body = body;
174
- this.path = path5;
170
+ this.path = path4;
175
171
  }
176
172
  };
177
173
  var SignatureVerificationError = class extends Error {
@@ -180,9 +176,18 @@ var SignatureVerificationError = class extends Error {
180
176
  this.name = "SignatureVerificationError";
181
177
  }
182
178
  };
183
- function enrich(status, body, statusText, endpoint, path5) {
179
+ var JudgeRefusalError = class extends Error {
180
+ constructor(message) {
181
+ super(message);
182
+ this.name = "JudgeRefusalError";
183
+ }
184
+ };
185
+ function isRefusalStatus(status) {
186
+ return status >= 400 && status < 500 && status !== 408;
187
+ }
188
+ function enrich(status, body, statusText, endpoint, path4) {
184
189
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
185
- let msg = `API error ${status}${path5 ? ` from ${path5}` : ""}: ${body || statusText}`;
190
+ let msg = `API error ${status}${path4 ? ` from ${path4}` : ""}: ${body || statusText}`;
186
191
  const lowered = body.toLowerCase();
187
192
  if (lowered.includes("agent not registered")) {
188
193
  msg += `
@@ -203,6 +208,61 @@ function enrich(status, body, statusText, endpoint, path5) {
203
208
  return msg;
204
209
  }
205
210
 
211
+ // src-ts/jail-hold.ts
212
+ var DEFAULTS = { ceilingMs: 3e4, pollMs: 1e3 };
213
+ var holds = /* @__PURE__ */ new Map();
214
+ var blockedAt = /* @__PURE__ */ new Map();
215
+ var BLOCK_MEMORY_MS = 10 * 6e4;
216
+ var config = { ...DEFAULTS };
217
+ function isJailHeld(pubkey) {
218
+ return holds.has(pubkey);
219
+ }
220
+ function recordBlock(pubkey) {
221
+ const now = Date.now();
222
+ for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
223
+ blockedAt.set(pubkey, now);
224
+ }
225
+ function blockedSince(pubkey, startedAt) {
226
+ const at = blockedAt.get(pubkey);
227
+ return at !== void 0 && startedAt <= at;
228
+ }
229
+ function holdJail(pubkey, confirmJailed, onGiveUp) {
230
+ if (holds.has(pubkey)) return;
231
+ const hold = { timer: null };
232
+ holds.set(pubkey, hold);
233
+ const deadline = Date.now() + config.ceilingMs;
234
+ const tick = async () => {
235
+ if (holds.get(pubkey) !== hold) return;
236
+ let jailed = false;
237
+ try {
238
+ jailed = await confirmJailed();
239
+ } catch {
240
+ }
241
+ if (holds.get(pubkey) !== hold) return;
242
+ if (jailed) {
243
+ holds.delete(pubkey);
244
+ return;
245
+ }
246
+ if (Date.now() >= deadline) {
247
+ holds.delete(pubkey);
248
+ onGiveUp?.();
249
+ return;
250
+ }
251
+ const timer = setTimeout(() => void tick(), config.pollMs);
252
+ timer.unref?.();
253
+ hold.timer = timer;
254
+ };
255
+ void tick();
256
+ }
257
+ function releaseJailHolds() {
258
+ for (const hold of holds.values()) if (hold.timer) clearTimeout(hold.timer);
259
+ holds.clear();
260
+ blockedAt.clear();
261
+ }
262
+ function configureJailHold(next) {
263
+ config = { ...DEFAULTS, ...next };
264
+ }
265
+
206
266
  // src-ts/http/client.ts
207
267
  var HttpClient = class {
208
268
  baseUrl;
@@ -211,8 +271,8 @@ var HttpClient = class {
211
271
  this.baseUrl = baseUrl.replace(/\/+$/, "");
212
272
  this.timeoutMs = timeoutMs;
213
273
  }
214
- buildUrl(path5, query) {
215
- const url = new URL(this.baseUrl + path5);
274
+ buildUrl(path4, query) {
275
+ const url = new URL(this.baseUrl + path4);
216
276
  if (query) {
217
277
  for (const [k, v] of Object.entries(query)) {
218
278
  if (v !== void 0 && v !== null && v !== "") {
@@ -222,14 +282,14 @@ var HttpClient = class {
222
282
  }
223
283
  return url.toString();
224
284
  }
225
- async get(path5, query, headers) {
226
- return this.fetch(this.buildUrl(path5, query), {
285
+ async get(path4, query, headers) {
286
+ return this.fetch(this.buildUrl(path4, query), {
227
287
  method: "GET",
228
288
  ...headers && { headers }
229
289
  });
230
290
  }
231
- async post(path5, body, headers) {
232
- return this.fetch(this.buildUrl(path5), {
291
+ async post(path4, body, headers) {
292
+ return this.fetch(this.buildUrl(path4), {
233
293
  method: "POST",
234
294
  headers: { "Content-Type": "application/json", ...headers },
235
295
  body: JSON.stringify(body)
@@ -553,7 +613,7 @@ import {
553
613
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
554
614
  import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
555
615
  import { createHash } from "crypto";
556
- var SDK_VERSION = true ? "0.19.2-dev.0" : "";
616
+ var SDK_VERSION = true ? "0.21.0-dev.0" : "";
557
617
  var SDK_LANGUAGE = "node";
558
618
  function sdkResourceAttributes() {
559
619
  const attrs = {
@@ -584,8 +644,8 @@ function isTelemetryOptedOut() {
584
644
  const filePath = join2(home2, ".config", "atbash", "telemetry.json");
585
645
  const raw = readFileSync2(filePath, "utf-8").trim();
586
646
  if (!raw) return false;
587
- const config = JSON.parse(raw);
588
- return config.enabled === false;
647
+ const config2 = JSON.parse(raw);
648
+ return config2.enabled === false;
589
649
  } catch {
590
650
  return false;
591
651
  }
@@ -630,26 +690,26 @@ function customCollector() {
630
690
  }
631
691
  return { url: url.replace(/\/+$/, ""), headers: () => headers };
632
692
  }
633
- function setupTelemetry(config) {
634
- if (!config.enabled) return;
693
+ function setupTelemetry(config2) {
694
+ if (!config2.enabled) return;
635
695
  if (isTelemetryOptedOut()) return;
636
- if (config.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config.source)) {
637
- defaultSource = config.source;
696
+ if (config2.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config2.source)) {
697
+ defaultSource = config2.source;
638
698
  sourceExplicitlySet = true;
639
699
  }
640
- if (config.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
641
- requestedExportIntervalMs = config.exportIntervalMs;
700
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
701
+ requestedExportIntervalMs = config2.exportIntervalMs;
642
702
  }
643
703
  if (meterProvider) return;
644
704
  const collector = customCollector();
645
- if (!collector && (!config.endpoint || !config.getAuthHeaders)) return;
705
+ if (!collector && (!config2.endpoint || !config2.getAuthHeaders)) return;
646
706
  if (!collector && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
647
- config.endpoint
707
+ config2.endpoint
648
708
  ))
649
709
  return;
650
710
  const destination = collector ?? {
651
- url: `${config.endpoint.replace(/\/+$/, "")}/api/telemetry`,
652
- headers: () => config.getAuthHeaders()
711
+ url: `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`,
712
+ headers: () => config2.getAuthHeaders()
653
713
  };
654
714
  const proxyUrl = destination.url;
655
715
  const exporter = new OTLPMetricExporter({
@@ -684,7 +744,7 @@ function setupTelemetry(config) {
684
744
  description: "SDK function execution duration",
685
745
  unit: "ms"
686
746
  });
687
- counterHasher = config.hashAgentKey ?? defaultAgentKeyHasher;
747
+ counterHasher = config2.hashAgentKey ?? defaultAgentKeyHasher;
688
748
  const logExporter = new OTLPLogExporter({
689
749
  // A real collector takes OTLP at its own signal paths; the Atbash proxy
690
750
  // takes a query parameter, because it is one authenticated route.
@@ -703,8 +763,8 @@ function setupTelemetry(config) {
703
763
  });
704
764
  }
705
765
  configureEvents(loggerProvider.getLogger("atbash-sdk"), {
706
- source: sourceOrDefault(config.source),
707
- hashAgentKey: config.hashAgentKey ?? defaultAgentKeyHasher,
766
+ source: sourceOrDefault(config2.source),
767
+ hashAgentKey: config2.hashAgentKey ?? defaultAgentKeyHasher,
708
768
  version: SDK_VERSION
709
769
  });
710
770
  }
@@ -782,13 +842,13 @@ function loadUserConfig() {
782
842
  return {};
783
843
  }
784
844
  }
785
- function saveUserConfig(config) {
845
+ function saveUserConfig(config2) {
786
846
  const dir = getConfigDir();
787
847
  if (!existsSync2(dir)) {
788
848
  mkdirSync(dir, { recursive: true, mode: 448 });
789
849
  }
790
850
  const filePath = getConfigPath();
791
- writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", {
851
+ writeFileSync(filePath, JSON.stringify(config2, null, 2) + "\n", {
792
852
  mode: 384
793
853
  });
794
854
  chmodSync(filePath, 384);
@@ -814,15 +874,16 @@ function resolve(key, flagValue) {
814
874
  function forcedChainNetwork(flagValue) {
815
875
  const raw = resolve("defaultChainNetwork", flagValue);
816
876
  if (!raw) return void 0;
817
- if (raw !== "public" && raw !== "private") {
877
+ if (raw !== "private") {
818
878
  throw new Error(
819
- `ATBASH_DEFAULT_CHAIN_NETWORK / defaultChainNetwork must be "public" or "private", got ${JSON.stringify(raw)} \u2014 unset it to let each org's chain decide.`
879
+ `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.`
820
880
  );
821
881
  }
822
882
  return raw;
823
883
  }
824
884
 
825
885
  // src-ts/client.ts
886
+ var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
826
887
  function generateToolCallId() {
827
888
  return `tc-${Date.now()}-${randomHex(4)}`;
828
889
  }
@@ -832,15 +893,13 @@ function resolveConstructorChain(options) {
832
893
  const hasBrid = options.blockchainRid !== void 0;
833
894
  if (hasNodeUrls !== hasBrid) {
834
895
  throw new Error(
835
- '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"`.'
896
+ 'nodeUrls and blockchainRid must be provided together \u2014 passing one without the other 404s every chain request. Prefer `chain: PRIVATE_CHAIN` or `network: "private"`.'
836
897
  );
837
898
  }
838
899
  if (hasNodeUrls && hasBrid) {
839
- const brid = options.blockchainRid;
840
- const derivedNetwork = options.network ?? (brid === PUBLIC_CHAIN.blockchainRid ? "public" : brid === PRIVATE_CHAIN.blockchainRid ? "private" : "private");
841
900
  return {
842
- network: derivedNetwork,
843
- blockchainRid: brid,
901
+ network: "private",
902
+ blockchainRid: options.blockchainRid,
844
903
  nodeUrls: options.nodeUrls
845
904
  };
846
905
  }
@@ -980,10 +1039,9 @@ var Atbash = class _Atbash {
980
1039
  _Atbash.environmentLogged = true;
981
1040
  const brief = (rid) => rid ? `${rid.slice(0, 8)}\u2026` : "(unset)";
982
1041
  this.logger.info?.(
983
- `[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"}`,
1042
+ `[atbash] environment \u2014 judge=${this.endpoint} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyPubKey ? "on" : "off"}`,
984
1043
  {
985
1044
  judgeEndpoint: this.endpoint,
986
- publicBlockchainRid: native.DEFAULT_BLOCKCHAIN_RID,
987
1045
  privateBlockchainRid: native.DEFAULT_PRIVATE_BLOCKCHAIN_RID,
988
1046
  activeBlockchainRid: this.blockchainRid,
989
1047
  responseSignatureCheck: Boolean(this.verifyPubKey)
@@ -1171,6 +1229,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1171
1229
  * return the signed tx hex. The server broadcasts to chain.
1172
1230
  */
1173
1231
  async logToolCall(action, context = "", options = {}) {
1232
+ return this._logToolCall(action, context, options, false);
1233
+ }
1234
+ /**
1235
+ * `raisePreflightErrors` (the judge path, like Python's
1236
+ * `raise_preflight_errors`): a failed registration check throws instead of
1237
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
1238
+ * (not registered, or a 4xx) from an outage.
1239
+ */
1240
+ async _logToolCall(action, context, options, raisePreflightErrors) {
1174
1241
  const start = performance.now();
1175
1242
  recordCall("logToolCall", void 0, this.auth.pubkey);
1176
1243
  let exists;
@@ -1180,18 +1247,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1180
1247
  });
1181
1248
  } catch (err) {
1182
1249
  recordDuration("logToolCall", performance.now() - start, "error");
1250
+ if (raisePreflightErrors) throw err;
1183
1251
  return { success: false, toolCallId: null, error: errorMessage(err) };
1184
1252
  }
1185
1253
  if (!exists) {
1186
1254
  recordDuration("logToolCall", performance.now() - start, "error");
1255
+ if (raisePreflightErrors) throw new JudgeRefusalError(AGENT_NOT_REGISTERED);
1187
1256
  return {
1188
1257
  success: false,
1189
1258
  toolCallId: null,
1190
- error: "Agent not registered. Onboard the agent at the dashboard before submitting actions."
1259
+ error: AGENT_NOT_REGISTERED
1191
1260
  };
1192
1261
  }
1193
1262
  const toolCallId = generateToolCallId();
1194
- const brid = options.chainOpts?.blockchainRid || options.chainOpts?.network ? this.bridFromChainOpts(options.chainOpts) : await this.defaultOrgBrid() ?? this.blockchainRid;
1263
+ const brid = await this.callBrid(options.chainOpts);
1195
1264
  const orgKey = options.orgEncryptionPubKey ?? this.orgEncryptionPubKey ?? this._orgKeyFromChain;
1196
1265
  try {
1197
1266
  const signedHex = orgKey ? signEncryptedToolCall(
@@ -1241,9 +1310,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1241
1310
  if (!action?.trim()) {
1242
1311
  throw new Error("action is required and cannot be empty.");
1243
1312
  }
1313
+ if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
1314
+ const startedAt = Date.now();
1244
1315
  let chainOpts = options.chainOpts;
1245
1316
  if (options.orgName && this._explicitChain) {
1246
- chainOpts = options.chainOpts ?? { network: this._defaultChain.network };
1317
+ chainOpts = options.chainOpts ?? {
1318
+ blockchainRid: this._defaultChain.blockchainRid
1319
+ };
1247
1320
  } else if (options.orgName && this.forcedNetwork()) {
1248
1321
  chainOpts = { network: this.forcedNetwork() };
1249
1322
  } else if (options.orgName) {
@@ -1253,8 +1326,7 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1253
1326
  } else {
1254
1327
  const mapNetwork = await this.getActiveNetworkForOrg(options.orgName);
1255
1328
  if (mapNetwork) {
1256
- const chain = mapNetwork === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
1257
- this._chainCache.set(options.orgName, chain);
1329
+ this._chainCache.set(options.orgName, chainForNetwork(mapNetwork));
1258
1330
  chainOpts = { network: mapNetwork };
1259
1331
  } else if (!chainOpts?.blockchainRid) {
1260
1332
  const resolved = await this.resolveChainFromMap(
@@ -1265,12 +1337,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1265
1337
  }
1266
1338
  }
1267
1339
  }
1268
- const brid = this.bridFromChainOpts(chainOpts);
1269
- const logResult = await this.logToolCall(action, context, {
1270
- toolName: options.toolName,
1271
- toolArgsJson: options.toolArgsJson,
1272
- chainOpts
1273
- });
1340
+ const brid = await this.callBrid(chainOpts);
1341
+ const logResult = await this._logToolCall(
1342
+ action,
1343
+ context,
1344
+ { toolName: options.toolName, toolArgsJson: options.toolArgsJson, chainOpts },
1345
+ true
1346
+ );
1274
1347
  if (!logResult.success || !logResult.toolCallId || !logResult.signedHex) {
1275
1348
  throw new Error(logResult.error || "Failed to sign log_tool_call");
1276
1349
  }
@@ -1307,10 +1380,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1307
1380
  if (verifyPubKey !== void 0) {
1308
1381
  verifyResponseSignature(bodyBytes, resp, verifyPubKey);
1309
1382
  }
1310
- const data = parseJson(bodyBytes);
1383
+ const parsed = tryParseJson(bodyBytes.toString("utf-8"));
1384
+ if (!isRecord2(parsed)) {
1385
+ throw new JudgeRefusalError(
1386
+ "The judge's reply could not be read, so the action was not approved."
1387
+ );
1388
+ }
1389
+ const data = parsed;
1311
1390
  const rawScore = data.score;
1312
1391
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
1313
- return {
1392
+ const result = {
1314
1393
  verdict: normalizeVerdict(data.verdict),
1315
1394
  allow: this.executable(data),
1316
1395
  actionType: normalizeActionType(data.action_type),
@@ -1326,6 +1405,45 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1326
1405
  score,
1327
1406
  refusal: explicitRefusal(data)
1328
1407
  };
1408
+ return this.applyJailHold(result, startedAt);
1409
+ }
1410
+ /**
1411
+ * Start a local hold on an enforced, on-chain BLOCK (the verdict that jails).
1412
+ * Refuse any other result that lands while the agent is held, or whose
1413
+ * request was already in flight when the block arrived: the server judged it
1414
+ * on pre-jail state.
1415
+ */
1416
+ applyJailHold(result, startedAt) {
1417
+ const pubkey = this.auth.pubkey;
1418
+ if (result.verdict === "BLOCK" && result.enforced && result.onChain) {
1419
+ recordBlock(pubkey);
1420
+ holdJail(
1421
+ pubkey,
1422
+ () => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
1423
+ () => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
1424
+ );
1425
+ return result;
1426
+ }
1427
+ return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
1428
+ }
1429
+ /** The server's jailed refusal, produced locally while the agent is held. */
1430
+ localJailBlock(toolCallId = generateToolCallId()) {
1431
+ return {
1432
+ verdict: "BLOCK",
1433
+ allow: false,
1434
+ actionType: "block",
1435
+ reason: "Agent is jailed. All actions are blocked until the organization owner unjails this agent.",
1436
+ confidence: 1,
1437
+ provider: "policy",
1438
+ latencyMs: 0,
1439
+ toolCallId,
1440
+ onChain: false,
1441
+ enforced: true,
1442
+ enforcementMode: "enforce",
1443
+ status: "",
1444
+ score: void 0,
1445
+ refusal: "BLOCK"
1446
+ };
1329
1447
  }
1330
1448
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
1331
1449
  /**
@@ -1377,6 +1495,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1377
1495
  reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
1378
1496
  };
1379
1497
  }
1498
+ if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
1499
+ return { ...this.failJudge(errorMessage(err), err), allow: false };
1500
+ }
1380
1501
  return this.failJudge(errorMessage(err), err);
1381
1502
  }
1382
1503
  }
@@ -1406,6 +1527,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1406
1527
  * never overrides a refusal.
1407
1528
  */
1408
1529
  decide(result) {
1530
+ const inner = this.decideInner(result);
1531
+ if (!result.enforcementMode) return inner;
1532
+ return {
1533
+ ...inner,
1534
+ enforced: result.enforced,
1535
+ enforcementMode: result.enforcementMode
1536
+ };
1537
+ }
1538
+ decideInner(result) {
1409
1539
  const toolCallId = result.toolCallId;
1410
1540
  const deny = (verdict, reason) => ({
1411
1541
  allow: false,
@@ -1519,17 +1649,17 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1519
1649
  /**
1520
1650
  * Return the current status of a previously submitted judgment.
1521
1651
  *
1522
- * `chainOpts` names which chain the judgment was signed against. The
1523
- * server's GET /api/v1/judge routes to that chain when the SDK sends
1524
- * a `brid` query param; without it, the server falls back to public.
1525
- * Callers on the private chain must pass a `chainOpts` (or configure
1526
- * the client on the private chain) — otherwise polling a POSTed
1527
- * judgment on the private chain 404s at the server.
1652
+ * `chainOpts` names which chain the judgment was signed against; the SDK
1653
+ * sends it as the `brid` query param the server routes by. Without it the
1654
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
1655
+ * client's org chain when it has an `orgName`, else the client default.
1656
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
1657
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
1528
1658
  */
1529
1659
  async getJudgmentStatus(judgmentId, agentPubkey, chainOpts) {
1530
1660
  const pk = agentPubkey ?? this.auth.pubkey;
1531
- const brid = this.bridFromChainOpts(chainOpts);
1532
1661
  return this.track("getJudgmentStatus", pk, async () => {
1662
+ const brid = await this.callBrid(chainOpts);
1533
1663
  const resp = await this.http.get(
1534
1664
  "/api/v1/judge",
1535
1665
  { tool_call_id: judgmentId, agent_pubkey: pk, brid },
@@ -1734,21 +1864,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1734
1864
  }
1735
1865
  if (resp.status !== 200) throw await this.httpError(resp);
1736
1866
  const data = await this.json(resp);
1737
- if (data?.network === "public" || data?.network === "private") {
1738
- return data.network;
1739
- }
1867
+ if (data?.network === "private") return data.network;
1740
1868
  return null;
1741
1869
  }
1742
1870
  /**
1743
1871
  * Resolve which chain an org's actions should run against. Cached
1744
1872
  * per-client by orgName. Resolution order:
1745
1873
  * 1. `org_networks` map (authoritative).
1746
- * 2. Per-chain subscription fallback — public + private records
1747
- * are fetched in parallel, with `is_private_blockchain` and
1748
- * `assigned_at` reconciling mixed states.
1749
- * A lookup that names exactly one chain wins outright. Where it names
1750
- * neither (a brand-new org) or cannot choose between them, the client's
1751
- * configured default decides.
1874
+ * 2. Subscription fallback — the org's subscription row on the chain.
1875
+ * A lookup that finds the org wins outright. Where it finds nothing (a
1876
+ * brand-new org), the client's configured default decides.
1752
1877
  */
1753
1878
  async resolveChainForOrg(orgName) {
1754
1879
  const name = orgName.trim();
@@ -1770,35 +1895,14 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1770
1895
  const cached = this._chainCache.get(orgName);
1771
1896
  if (cached) return cached;
1772
1897
  if (mapNetwork) {
1773
- const chain = mapNetwork === "private" ? PRIVATE_CHAIN : PUBLIC_CHAIN;
1898
+ const chain = chainForNetwork(mapNetwork);
1774
1899
  this._chainCache.set(orgName, chain);
1775
1900
  return chain;
1776
1901
  }
1777
- const [pubRes, privRes] = await Promise.allSettled([
1778
- this.getOrgSubscription(orgName, "public"),
1779
- this.getOrgSubscription(orgName, "private")
1780
- ]);
1781
- const pubSub = pubRes.status === "fulfilled" ? pubRes.value : null;
1782
- const privSub = privRes.status === "fulfilled" ? privRes.value : null;
1783
- const failure2 = pubRes.status === "rejected" ? pubRes.reason : privRes.status === "rejected" ? privRes.reason : null;
1784
- if (pubSub?.is_private_blockchain) {
1785
- this._chainCache.set(orgName, PRIVATE_CHAIN);
1786
- return PRIVATE_CHAIN;
1787
- }
1788
- if (pubSub && privSub) {
1789
- const chain = privSub.assigned_at === pubSub.assigned_at ? this._defaultChain : privSub.assigned_at > pubSub.assigned_at ? PRIVATE_CHAIN : PUBLIC_CHAIN;
1790
- this._chainCache.set(orgName, chain);
1791
- return chain;
1792
- }
1793
- if (pubSub) {
1794
- this._chainCache.set(orgName, PUBLIC_CHAIN);
1795
- return PUBLIC_CHAIN;
1796
- }
1797
- if (privSub?.is_private_blockchain) {
1798
- this._chainCache.set(orgName, PRIVATE_CHAIN);
1799
- return PRIVATE_CHAIN;
1800
- }
1801
- if (failure2) {
1902
+ let privSub;
1903
+ try {
1904
+ privSub = await this.getOrgSubscription(orgName, "private");
1905
+ } catch (failure2) {
1802
1906
  const detail = failure2 instanceof Error ? failure2.message : String(failure2);
1803
1907
  throw new AtbashAPIError(
1804
1908
  failure2 instanceof AtbashAPIError ? failure2.status : 0,
@@ -1807,6 +1911,10 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1807
1911
  this.endpoint
1808
1912
  );
1809
1913
  }
1914
+ if (privSub) {
1915
+ this._chainCache.set(orgName, PRIVATE_CHAIN);
1916
+ return PRIVATE_CHAIN;
1917
+ }
1810
1918
  this._chainCache.set(orgName, this._defaultChain);
1811
1919
  return this._defaultChain;
1812
1920
  }
@@ -1863,10 +1971,21 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1863
1971
  */
1864
1972
  bridFromChainOpts(chainOpts) {
1865
1973
  if (chainOpts?.blockchainRid) return chainOpts.blockchainRid;
1866
- if (chainOpts?.network === "private") return PRIVATE_CHAIN.blockchainRid;
1867
- if (chainOpts?.network === "public") return PUBLIC_CHAIN.blockchainRid;
1974
+ if (chainOpts?.network) return chainForNetwork(chainOpts.network).blockchainRid;
1868
1975
  return this.blockchainRid;
1869
1976
  }
1977
+ /**
1978
+ * The chain one call's transactions are signed for: the per-call override
1979
+ * when given, otherwise the client's org, otherwise the client default.
1980
+ * log_tool_call, judge_action and the judgment-status read all use this, so
1981
+ * a request never pairs transactions signed for two different chains.
1982
+ */
1983
+ async callBrid(chainOpts) {
1984
+ if (chainOpts?.blockchainRid || chainOpts?.network) {
1985
+ return this.bridFromChainOpts(chainOpts);
1986
+ }
1987
+ return await this.defaultOrgBrid() ?? this.blockchainRid;
1988
+ }
1870
1989
  /**
1871
1990
  * Resolve the dashboard chain used by agent metadata/policy reads.
1872
1991
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -1912,8 +2031,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1912
2031
  async riskEnginePost(body, network, brid) {
1913
2032
  let resp;
1914
2033
  try {
1915
- const path5 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
1916
- resp = await this.http.post(path5, body, this.authHeaders(brid));
2034
+ const path4 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2035
+ resp = await this.http.post(path4, body, this.authHeaders(brid));
1917
2036
  } catch (err) {
1918
2037
  throw this.transportError(err);
1919
2038
  }
@@ -2174,10 +2293,6 @@ function tryParseJson(text) {
2174
2293
  return void 0;
2175
2294
  }
2176
2295
  }
2177
- function parseJson(bytes) {
2178
- const parsed = tryParseJson(bytes.toString("utf-8"));
2179
- return parsed === void 0 ? {} : parsed;
2180
- }
2181
2296
  function requestPath(url) {
2182
2297
  if (!url) return void 0;
2183
2298
  try {
@@ -2453,13 +2568,13 @@ function resolveBlockchainRid(chainOpts) {
2453
2568
  if (chainOpts?.blockchainRid !== void 0) return chainOpts.blockchainRid;
2454
2569
  if (chainOpts?.nodeUrls !== void 0) {
2455
2570
  throw new Error(
2456
- '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"`.'
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: "private"`.'
2457
2572
  );
2458
2573
  }
2459
- const config = chainForNetwork(
2574
+ const config2 = chainForNetwork(
2460
2575
  chainOpts?.network ?? forcedChainNetwork() ?? "private"
2461
2576
  );
2462
- return config.blockchainRid;
2577
+ return config2.blockchainRid;
2463
2578
  }
2464
2579
  async function commitMemoryVersion(plaintext, auth, opts) {
2465
2580
  const key = await deriveMemoryKey(auth.privkey);
@@ -2685,7 +2800,34 @@ function classifyMemoryWrite(event, ctx, opts = {}) {
2685
2800
  // src-ts/memory/guard.ts
2686
2801
  import { promises as fs } from "fs";
2687
2802
  import os from "os";
2688
- import path from "path";
2803
+
2804
+ // src-ts/memory/slot.ts
2805
+ import nodePath from "path";
2806
+ function asciiLower(s) {
2807
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) | 32));
2808
+ }
2809
+ function spelledAs(target, base, p = nodePath) {
2810
+ const resolved = p.resolve(base);
2811
+ if (p.sep !== "\\") return resolved;
2812
+ const prefix = resolved.replace(/[\\/]+$/, "");
2813
+ const t = asciiLower(target);
2814
+ const b = asciiLower(prefix);
2815
+ if (t === b || t.startsWith(b + "\\") || t.startsWith(b + "/")) {
2816
+ return target.slice(0, prefix.length);
2817
+ }
2818
+ return resolved;
2819
+ }
2820
+ function memorySlotOf(target, home2, root, extraHomes, p = nodePath) {
2821
+ const resolved = p.resolve(target);
2822
+ return native.memorySlotForPath(
2823
+ resolved,
2824
+ home2 ? spelledAs(resolved, home2, p) : "",
2825
+ root ? spelledAs(resolved, root, p) : void 0,
2826
+ extraHomes
2827
+ );
2828
+ }
2829
+
2830
+ // src-ts/memory/guard.ts
2689
2831
  function emitDebugProbe(event, ctx, memEntry, logger2) {
2690
2832
  if (!logger2?.info) return;
2691
2833
  const ev = event ?? {};
@@ -2711,7 +2853,7 @@ function emitDebugProbe(event, ctx, memEntry, logger2) {
2711
2853
  }
2712
2854
  function memorySlotFor(key, root, extraHomes) {
2713
2855
  const home2 = process.env.HOME || os.homedir() || "";
2714
- return native.memorySlotForPath(path.resolve(key), home2, root, extraHomes);
2856
+ return memorySlotOf(key, home2, root, extraHomes);
2715
2857
  }
2716
2858
  async function resultingDocument(memEntry, event, ctx) {
2717
2859
  const edits = native.extractMemoryEdits(event, ctx);
@@ -2914,7 +3056,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
2914
3056
 
2915
3057
  // src-ts/memory/pointer-store.ts
2916
3058
  import { promises as fs2 } from "fs";
2917
- import path2 from "path";
3059
+ import path from "path";
2918
3060
  var VERSION = 2;
2919
3061
  var ZERO = { activeId: null, checkedAt: 0 };
2920
3062
  var PointerStore = class {
@@ -2967,19 +3109,19 @@ var PointerStore = class {
2967
3109
  this.cache = { version: VERSION, agents: {} };
2968
3110
  }
2969
3111
  async persist(file) {
2970
- await fs2.mkdir(path2.dirname(this.filePath), { recursive: true });
3112
+ await fs2.mkdir(path.dirname(this.filePath), { recursive: true });
2971
3113
  const tmp = `${this.filePath}.${process.pid}.tmp`;
2972
3114
  await fs2.writeFile(tmp, JSON.stringify(file, null, 2), "utf8");
2973
3115
  await fs2.rename(tmp, this.filePath);
2974
3116
  }
2975
3117
  };
2976
3118
  function defaultPointerPath(workspaceDir = process.cwd()) {
2977
- return path2.join(workspaceDir, ".atbash", "memory-pointer.json");
3119
+ return path.join(workspaceDir, ".atbash", "memory-pointer.json");
2978
3120
  }
2979
3121
 
2980
3122
  // src-ts/memory/file-logger.ts
2981
3123
  import { promises as fs3 } from "fs";
2982
- import path3 from "path";
3124
+ import path2 from "path";
2983
3125
  function formatMeta(meta) {
2984
3126
  if (!meta || Object.keys(meta).length === 0) return "";
2985
3127
  try {
@@ -2992,7 +3134,7 @@ function createFileLogger(filePath, upstream) {
2992
3134
  let queue = Promise.resolve();
2993
3135
  let writeError;
2994
3136
  async function ensureDir() {
2995
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
3137
+ await fs3.mkdir(path2.dirname(filePath), { recursive: true });
2996
3138
  }
2997
3139
  function append(level, message, meta) {
2998
3140
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
@@ -3018,7 +3160,7 @@ function createFileLogger(filePath, upstream) {
3018
3160
  };
3019
3161
  }
3020
3162
  function defaultPluginLogPath(workspaceDir = process.cwd()) {
3021
- return path3.join(workspaceDir, ".atbash", "plugin.log");
3163
+ return path2.join(workspaceDir, ".atbash", "plugin.log");
3022
3164
  }
3023
3165
 
3024
3166
  // src-ts/memory/read-classifier.ts
@@ -3034,14 +3176,14 @@ function classifyMemoryRead(event, ctx, opts = {}) {
3034
3176
  // src-ts/memory/guard-manager.ts
3035
3177
  import { promises as fs4 } from "fs";
3036
3178
  import os2 from "os";
3037
- import path4 from "path";
3179
+ import path3 from "path";
3038
3180
  var DEFAULT_SYNC_TTL_MS = 3e4;
3039
3181
  var MemoryGuardManager = class {
3040
3182
  constructor(opts) {
3041
3183
  this.opts = opts;
3042
3184
  const workspaceDir = opts.workspaceDir;
3043
3185
  this.workspaceDir = workspaceDir;
3044
- this.memoryFilePath = opts.memoryFilePath ?? path4.join(workspaceDir, "MEMORY.md");
3186
+ this.memoryFilePath = opts.memoryFilePath ?? path3.join(workspaceDir, "MEMORY.md");
3045
3187
  this.pointerStore = new PointerStore(opts.pointerFilePath ?? defaultPointerPath(workspaceDir));
3046
3188
  this.logger = createFileLogger(
3047
3189
  opts.logFilePath ?? defaultPluginLogPath(workspaceDir),
@@ -3256,7 +3398,7 @@ var MemoryGuardManager = class {
3256
3398
  */
3257
3399
  async locate(slot, roots) {
3258
3400
  for (const root of roots) {
3259
- const candidate = path4.join(root, slot);
3401
+ const candidate = path3.join(root, slot);
3260
3402
  try {
3261
3403
  await fs4.access(candidate);
3262
3404
  return candidate;
@@ -3271,7 +3413,7 @@ var MemoryGuardManager = class {
3271
3413
  /** Roots a chain slot may be joined to, most specific first. */
3272
3414
  async memoryRoots() {
3273
3415
  const roots = [];
3274
- if (this.opts.memoryRoot) roots.push(path4.resolve(this.opts.memoryRoot));
3416
+ if (this.opts.memoryRoot) roots.push(path3.resolve(this.opts.memoryRoot));
3275
3417
  const home2 = process.env.HOME || os2.homedir() || "";
3276
3418
  if (home2) {
3277
3419
  const names = [
@@ -3279,7 +3421,7 @@ var MemoryGuardManager = class {
3279
3421
  ...this.opts.agentHomeDirs ?? []
3280
3422
  ];
3281
3423
  for (const name of names) {
3282
- const candidate = path4.join(home2, name);
3424
+ const candidate = path3.join(home2, name);
3283
3425
  try {
3284
3426
  if ((await fs4.stat(candidate)).isDirectory()) roots.push(candidate);
3285
3427
  } catch {
@@ -3417,7 +3559,7 @@ var MemoryGuardManager = class {
3417
3559
  const rec = bag;
3418
3560
  for (const k of KEYS) {
3419
3561
  const v = rec[k];
3420
- if (typeof v === "string" && v) found.add(path4.resolve(this.workspaceDir, v));
3562
+ if (typeof v === "string" && v) found.add(path3.resolve(this.workspaceDir, v));
3421
3563
  }
3422
3564
  }
3423
3565
  }
@@ -3464,12 +3606,7 @@ var MemoryGuardManager = class {
3464
3606
  */
3465
3607
  slotFor(target) {
3466
3608
  const home2 = process.env.HOME || os2.homedir() || "";
3467
- return native.memorySlotForPath(
3468
- path4.resolve(target),
3469
- home2,
3470
- this.opts.memoryRoot,
3471
- this.opts.agentHomeDirs
3472
- );
3609
+ return memorySlotOf(target, home2, this.opts.memoryRoot, this.opts.agentHomeDirs);
3473
3610
  }
3474
3611
  /**
3475
3612
  * Verify every memory file this call reads, each against its own chain slot.
@@ -3618,7 +3755,7 @@ var MemoryGuardManager = class {
3618
3755
  }
3619
3756
  /** Replace `target` in one step, so a reader never sees a half-written file. */
3620
3757
  async writeFileAtomic(target, content) {
3621
- await fs4.mkdir(path4.dirname(target), { recursive: true });
3758
+ await fs4.mkdir(path3.dirname(target), { recursive: true });
3622
3759
  const tmp = `${target}.${process.pid}.tmp`;
3623
3760
  await fs4.writeFile(tmp, content, "utf8");
3624
3761
  await fs4.rename(tmp, target);
@@ -3749,8 +3886,6 @@ export {
3749
3886
  Atbash,
3750
3887
  AtbashAPIError,
3751
3888
  BOOT_SYNC_HINT,
3752
- DEFAULT_BLOCKCHAIN_RID,
3753
- DEFAULT_CHROMIA_NODE_URLS,
3754
3889
  DEFAULT_ENDPOINT,
3755
3890
  DEFAULT_MEMORY_PATH_PATTERNS,
3756
3891
  DEFAULT_MEMORY_READ_TOOL_NAMES,
@@ -3758,11 +3893,11 @@ export {
3758
3893
  EciesDomain,
3759
3894
  HttpClient,
3760
3895
  HttpTransportError,
3896
+ JudgeRefusalError,
3761
3897
  KEY_FILENAMES,
3762
3898
  MemoryGuardManager,
3763
3899
  MemoryIntegrityError,
3764
3900
  PRIVATE_CHAIN,
3765
- PUBLIC_CHAIN,
3766
3901
  PointerStore,
3767
3902
  SignatureVerificationError,
3768
3903
  bootSyncFailureLine,
@@ -3776,6 +3911,7 @@ export {
3776
3911
  columnAad,
3777
3912
  commitMemoryVersion,
3778
3913
  configureEvents,
3914
+ configureJailHold,
3779
3915
  containsEvasionCharacters,
3780
3916
  containsSecret,
3781
3917
  createFileLogger,
@@ -3804,12 +3940,14 @@ export {
3804
3940
  getRollbackHistory,
3805
3941
  guardMemoryWrite,
3806
3942
  isEnvelope,
3943
+ isJailHeld,
3807
3944
  isValidPrivateKey,
3808
3945
  keyFingerprintOf,
3809
3946
  keyPathCandidates,
3810
3947
  loadAgent,
3811
3948
  loadAgentFromFile,
3812
3949
  loadUserConfig,
3950
+ memorySlotOf,
3813
3951
  normalizeActionForHash,
3814
3952
  normalizeActionType,
3815
3953
  normalizeForMatching,
@@ -3823,6 +3961,7 @@ export {
3823
3961
  recordEvent,
3824
3962
  redactJsonStrings,
3825
3963
  redactSecrets,
3964
+ releaseJailHolds,
3826
3965
  resolve,
3827
3966
  resolveKeyPath,
3828
3967
  rollbackMemory,