@atbash/sdk 0.19.1-dev.0 → 0.20.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
@@ -42,6 +42,7 @@ __export(src_ts_exports, {
42
42
  EciesDomain: () => EciesDomain,
43
43
  HttpClient: () => HttpClient,
44
44
  HttpTransportError: () => HttpTransportError,
45
+ JudgeRefusalError: () => JudgeRefusalError,
45
46
  KEY_FILENAMES: () => KEY_FILENAMES,
46
47
  MemoryGuardManager: () => MemoryGuardManager,
47
48
  MemoryIntegrityError: () => MemoryIntegrityError,
@@ -60,6 +61,7 @@ __export(src_ts_exports, {
60
61
  columnAad: () => columnAad,
61
62
  commitMemoryVersion: () => commitMemoryVersion,
62
63
  configureEvents: () => configureEvents,
64
+ configureJailHold: () => configureJailHold,
63
65
  containsEvasionCharacters: () => containsEvasionCharacters,
64
66
  containsSecret: () => containsSecret,
65
67
  createFileLogger: () => createFileLogger,
@@ -88,12 +90,14 @@ __export(src_ts_exports, {
88
90
  getRollbackHistory: () => getRollbackHistory,
89
91
  guardMemoryWrite: () => guardMemoryWrite,
90
92
  isEnvelope: () => isEnvelope,
93
+ isJailHeld: () => isJailHeld,
91
94
  isValidPrivateKey: () => isValidPrivateKey,
92
95
  keyFingerprintOf: () => keyFingerprintOf,
93
96
  keyPathCandidates: () => keyPathCandidates,
94
97
  loadAgent: () => loadAgent,
95
98
  loadAgentFromFile: () => loadAgentFromFile,
96
99
  loadUserConfig: () => loadUserConfig,
100
+ memorySlotOf: () => memorySlotOf,
97
101
  normalizeActionForHash: () => normalizeActionForHash,
98
102
  normalizeActionType: () => normalizeActionType,
99
103
  normalizeForMatching: () => normalizeForMatching,
@@ -107,6 +111,7 @@ __export(src_ts_exports, {
107
111
  recordEvent: () => recordEvent,
108
112
  redactJsonStrings: () => redactJsonStrings,
109
113
  redactSecrets: () => redactSecrets,
114
+ releaseJailHolds: () => releaseJailHolds,
110
115
  resolve: () => resolve,
111
116
  resolveKeyPath: () => resolveKeyPath,
112
117
  rollbackMemory: () => rollbackMemory,
@@ -293,12 +298,12 @@ var AtbashAPIError = class extends Error {
293
298
  * ran does not carry. The path is the fact that settles it.
294
299
  */
295
300
  path;
296
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path5) {
297
- super(enrich(status, body, statusText, endpoint, path5));
301
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path4) {
302
+ super(enrich(status, body, statusText, endpoint, path4));
298
303
  this.name = "AtbashAPIError";
299
304
  this.status = status;
300
305
  this.body = body;
301
- this.path = path5;
306
+ this.path = path4;
302
307
  }
303
308
  };
304
309
  var SignatureVerificationError = class extends Error {
@@ -307,9 +312,18 @@ var SignatureVerificationError = class extends Error {
307
312
  this.name = "SignatureVerificationError";
308
313
  }
309
314
  };
310
- function enrich(status, body, statusText, endpoint, path5) {
315
+ var JudgeRefusalError = class extends Error {
316
+ constructor(message) {
317
+ super(message);
318
+ this.name = "JudgeRefusalError";
319
+ }
320
+ };
321
+ function isRefusalStatus(status) {
322
+ return status >= 400 && status < 500 && status !== 408;
323
+ }
324
+ function enrich(status, body, statusText, endpoint, path4) {
311
325
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
312
- let msg = `API error ${status}${path5 ? ` from ${path5}` : ""}: ${body || statusText}`;
326
+ let msg = `API error ${status}${path4 ? ` from ${path4}` : ""}: ${body || statusText}`;
313
327
  const lowered = body.toLowerCase();
314
328
  if (lowered.includes("agent not registered")) {
315
329
  msg += `
@@ -330,6 +344,61 @@ function enrich(status, body, statusText, endpoint, path5) {
330
344
  return msg;
331
345
  }
332
346
 
347
+ // src-ts/jail-hold.ts
348
+ var DEFAULTS = { ceilingMs: 3e4, pollMs: 1e3 };
349
+ var holds = /* @__PURE__ */ new Map();
350
+ var blockedAt = /* @__PURE__ */ new Map();
351
+ var BLOCK_MEMORY_MS = 10 * 6e4;
352
+ var config = { ...DEFAULTS };
353
+ function isJailHeld(pubkey) {
354
+ return holds.has(pubkey);
355
+ }
356
+ function recordBlock(pubkey) {
357
+ const now = Date.now();
358
+ for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
359
+ blockedAt.set(pubkey, now);
360
+ }
361
+ function blockedSince(pubkey, startedAt) {
362
+ const at = blockedAt.get(pubkey);
363
+ return at !== void 0 && startedAt <= at;
364
+ }
365
+ function holdJail(pubkey, confirmJailed, onGiveUp) {
366
+ if (holds.has(pubkey)) return;
367
+ const hold = { timer: null };
368
+ holds.set(pubkey, hold);
369
+ const deadline = Date.now() + config.ceilingMs;
370
+ const tick = async () => {
371
+ if (holds.get(pubkey) !== hold) return;
372
+ let jailed = false;
373
+ try {
374
+ jailed = await confirmJailed();
375
+ } catch {
376
+ }
377
+ if (holds.get(pubkey) !== hold) return;
378
+ if (jailed) {
379
+ holds.delete(pubkey);
380
+ return;
381
+ }
382
+ if (Date.now() >= deadline) {
383
+ holds.delete(pubkey);
384
+ onGiveUp?.();
385
+ return;
386
+ }
387
+ const timer = setTimeout(() => void tick(), config.pollMs);
388
+ timer.unref?.();
389
+ hold.timer = timer;
390
+ };
391
+ void tick();
392
+ }
393
+ function releaseJailHolds() {
394
+ for (const hold of holds.values()) if (hold.timer) clearTimeout(hold.timer);
395
+ holds.clear();
396
+ blockedAt.clear();
397
+ }
398
+ function configureJailHold(next) {
399
+ config = { ...DEFAULTS, ...next };
400
+ }
401
+
333
402
  // src-ts/http/client.ts
334
403
  var HttpClient = class {
335
404
  baseUrl;
@@ -338,8 +407,8 @@ var HttpClient = class {
338
407
  this.baseUrl = baseUrl.replace(/\/+$/, "");
339
408
  this.timeoutMs = timeoutMs;
340
409
  }
341
- buildUrl(path5, query) {
342
- const url = new URL(this.baseUrl + path5);
410
+ buildUrl(path4, query) {
411
+ const url = new URL(this.baseUrl + path4);
343
412
  if (query) {
344
413
  for (const [k, v] of Object.entries(query)) {
345
414
  if (v !== void 0 && v !== null && v !== "") {
@@ -349,14 +418,14 @@ var HttpClient = class {
349
418
  }
350
419
  return url.toString();
351
420
  }
352
- async get(path5, query, headers) {
353
- return this.fetch(this.buildUrl(path5, query), {
421
+ async get(path4, query, headers) {
422
+ return this.fetch(this.buildUrl(path4, query), {
354
423
  method: "GET",
355
424
  ...headers && { headers }
356
425
  });
357
426
  }
358
- async post(path5, body, headers) {
359
- return this.fetch(this.buildUrl(path5), {
427
+ async post(path4, body, headers) {
428
+ return this.fetch(this.buildUrl(path4), {
360
429
  method: "POST",
361
430
  headers: { "Content-Type": "application/json", ...headers },
362
431
  body: JSON.stringify(body)
@@ -622,11 +691,12 @@ function eventsOptedOut() {
622
691
  const flag = process.env.ATBASH_TELEMETRY_EVENTS_DISABLED?.trim().toLowerCase();
623
692
  return Boolean(flag && ["1", "true", "yes", "on"].includes(flag));
624
693
  }
694
+ var SOURCE_SHAPE = /^(?:sdk|cli|plugin:[a-z0-9][a-z0-9._-]{0,31})$/;
625
695
  function configureEvents(next, opts = {}) {
626
696
  logger = next;
627
697
  eventsEnabled = next !== null && !eventsOptedOut();
628
698
  if (opts.version) sdkVersion = opts.version;
629
- if (opts.source) eventSource = opts.source;
699
+ if (opts.source && SOURCE_SHAPE.test(opts.source)) eventSource = opts.source;
630
700
  hashAgentKey = opts.hashAgentKey ?? null;
631
701
  }
632
702
  function eventsAreOn() {
@@ -673,6 +743,20 @@ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
673
743
  var import_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
674
744
  var import_sdk_logs = require("@opentelemetry/sdk-logs");
675
745
  var import_node_crypto = require("crypto");
746
+ var SDK_VERSION = true ? "0.20.0-dev.0" : "";
747
+ var SDK_LANGUAGE = "node";
748
+ function sdkResourceAttributes() {
749
+ const attrs = {
750
+ "service.name": "atbash-sdk",
751
+ "sdk.language": SDK_LANGUAGE
752
+ };
753
+ if (SDK_VERSION) attrs["sdk.version"] = SDK_VERSION;
754
+ return attrs;
755
+ }
756
+ function sourceOrDefault(candidate) {
757
+ return typeof candidate === "string" && SOURCE_SHAPE.test(candidate) ? candidate : defaultSource;
758
+ }
759
+ var exitHookInstalled = false;
676
760
  var meterProvider = null;
677
761
  var loggerProvider = null;
678
762
  var callCounter = null;
@@ -690,8 +774,8 @@ function isTelemetryOptedOut() {
690
774
  const filePath = (0, import_node_path2.join)(home2, ".config", "atbash", "telemetry.json");
691
775
  const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8").trim();
692
776
  if (!raw) return false;
693
- const config = JSON.parse(raw);
694
- return config.enabled === false;
777
+ const config2 = JSON.parse(raw);
778
+ return config2.enabled === false;
695
779
  } catch {
696
780
  return false;
697
781
  }
@@ -736,26 +820,26 @@ function customCollector() {
736
820
  }
737
821
  return { url: url.replace(/\/+$/, ""), headers: () => headers };
738
822
  }
739
- function setupTelemetry(config) {
740
- if (!config.enabled) return;
823
+ function setupTelemetry(config2) {
824
+ if (!config2.enabled) return;
741
825
  if (isTelemetryOptedOut()) return;
742
- if (config.source && !sourceExplicitlySet) {
743
- defaultSource = config.source;
826
+ if (config2.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config2.source)) {
827
+ defaultSource = config2.source;
744
828
  sourceExplicitlySet = true;
745
829
  }
746
- if (config.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
747
- requestedExportIntervalMs = config.exportIntervalMs;
830
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
831
+ requestedExportIntervalMs = config2.exportIntervalMs;
748
832
  }
749
833
  if (meterProvider) return;
750
834
  const collector = customCollector();
751
- if (!collector && (!config.endpoint || !config.getAuthHeaders)) return;
835
+ if (!collector && (!config2.endpoint || !config2.getAuthHeaders)) return;
752
836
  if (!collector && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
753
- config.endpoint
837
+ config2.endpoint
754
838
  ))
755
839
  return;
756
840
  const destination = collector ?? {
757
- url: `${config.endpoint.replace(/\/+$/, "")}/api/telemetry`,
758
- headers: () => config.getAuthHeaders()
841
+ url: `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`,
842
+ headers: () => config2.getAuthHeaders()
759
843
  };
760
844
  const proxyUrl = destination.url;
761
845
  const exporter = new import_exporter_metrics_otlp_http.OTLPMetricExporter({
@@ -779,9 +863,7 @@ function setupTelemetry(config) {
779
863
  exportIntervalMillis: requestedExportIntervalMs ?? 6e4
780
864
  });
781
865
  meterProvider = new import_sdk_metrics.MeterProvider({
782
- resource: (0, import_resources.resourceFromAttributes)({
783
- "service.name": "atbash-sdk"
784
- }),
866
+ resource: (0, import_resources.resourceFromAttributes)(sdkResourceAttributes()),
785
867
  readers: [reader]
786
868
  });
787
869
  const meter = meterProvider.getMeter("atbash-sdk");
@@ -792,7 +874,7 @@ function setupTelemetry(config) {
792
874
  description: "SDK function execution duration",
793
875
  unit: "ms"
794
876
  });
795
- counterHasher = config.hashAgentKey ?? defaultAgentKeyHasher;
877
+ counterHasher = config2.hashAgentKey ?? defaultAgentKeyHasher;
796
878
  const logExporter = new import_exporter_logs_otlp_http.OTLPLogExporter({
797
879
  // A real collector takes OTLP at its own signal paths; the Atbash proxy
798
880
  // takes a query parameter, because it is one authenticated route.
@@ -800,12 +882,20 @@ function setupTelemetry(config) {
800
882
  headers: async () => destination.headers()
801
883
  });
802
884
  loggerProvider = new import_sdk_logs.LoggerProvider({
803
- resource: (0, import_resources.resourceFromAttributes)({ "service.name": "atbash-sdk" }),
885
+ resource: (0, import_resources.resourceFromAttributes)(sdkResourceAttributes()),
804
886
  processors: [new import_sdk_logs.BatchLogRecordProcessor({ exporter: logExporter })]
805
887
  });
888
+ if (!exitHookInstalled && typeof process?.once === "function") {
889
+ exitHookInstalled = true;
890
+ process.once("beforeExit", () => {
891
+ shutdownTelemetry().catch(() => {
892
+ });
893
+ });
894
+ }
806
895
  configureEvents(loggerProvider.getLogger("atbash-sdk"), {
807
- source: config.source ?? defaultSource,
808
- hashAgentKey: config.hashAgentKey ?? defaultAgentKeyHasher
896
+ source: sourceOrDefault(config2.source),
897
+ hashAgentKey: config2.hashAgentKey ?? defaultAgentKeyHasher,
898
+ version: SDK_VERSION
809
899
  });
810
900
  }
811
901
  function recordCall(functionName, source, agentPubkey) {
@@ -813,7 +903,7 @@ function recordCall(functionName, source, agentPubkey) {
813
903
  if (!callCounter) return;
814
904
  callCounter.add(1, {
815
905
  "function.name": functionName,
816
- source: source ?? defaultSource,
906
+ source: sourceOrDefault(source),
817
907
  // Hashed on this path too. The counters and the events export to the same
818
908
  // place in the same request, so a raw key here would make hashing it on
819
909
  // the event beside it pointless.
@@ -825,7 +915,7 @@ function recordDuration(functionName, durationMs, status, source) {
825
915
  durationHistogram.record(durationMs, {
826
916
  "function.name": functionName,
827
917
  status,
828
- source: source ?? defaultSource
918
+ source: sourceOrDefault(source)
829
919
  });
830
920
  }
831
921
  async function flushTelemetry() {
@@ -876,13 +966,13 @@ function loadUserConfig() {
876
966
  return {};
877
967
  }
878
968
  }
879
- function saveUserConfig(config) {
969
+ function saveUserConfig(config2) {
880
970
  const dir = getConfigDir();
881
971
  if (!(0, import_node_fs3.existsSync)(dir)) {
882
972
  (0, import_node_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
883
973
  }
884
974
  const filePath = getConfigPath();
885
- (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(config, null, 2) + "\n", {
975
+ (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(config2, null, 2) + "\n", {
886
976
  mode: 384
887
977
  });
888
978
  (0, import_node_fs3.chmodSync)(filePath, 384);
@@ -917,6 +1007,7 @@ function forcedChainNetwork(flagValue) {
917
1007
  }
918
1008
 
919
1009
  // src-ts/client.ts
1010
+ var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
920
1011
  function generateToolCallId() {
921
1012
  return `tc-${Date.now()}-${randomHex(4)}`;
922
1013
  }
@@ -1265,6 +1356,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1265
1356
  * return the signed tx hex. The server broadcasts to chain.
1266
1357
  */
1267
1358
  async logToolCall(action, context = "", options = {}) {
1359
+ return this._logToolCall(action, context, options, false);
1360
+ }
1361
+ /**
1362
+ * `raisePreflightErrors` (the judge path, like Python's
1363
+ * `raise_preflight_errors`): a failed registration check throws instead of
1364
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
1365
+ * (not registered, or a 4xx) from an outage.
1366
+ */
1367
+ async _logToolCall(action, context, options, raisePreflightErrors) {
1268
1368
  const start = performance.now();
1269
1369
  recordCall("logToolCall", void 0, this.auth.pubkey);
1270
1370
  let exists;
@@ -1274,18 +1374,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1274
1374
  });
1275
1375
  } catch (err) {
1276
1376
  recordDuration("logToolCall", performance.now() - start, "error");
1377
+ if (raisePreflightErrors) throw err;
1277
1378
  return { success: false, toolCallId: null, error: errorMessage(err) };
1278
1379
  }
1279
1380
  if (!exists) {
1280
1381
  recordDuration("logToolCall", performance.now() - start, "error");
1382
+ if (raisePreflightErrors) throw new JudgeRefusalError(AGENT_NOT_REGISTERED);
1281
1383
  return {
1282
1384
  success: false,
1283
1385
  toolCallId: null,
1284
- error: "Agent not registered. Onboard the agent at the dashboard before submitting actions."
1386
+ error: AGENT_NOT_REGISTERED
1285
1387
  };
1286
1388
  }
1287
1389
  const toolCallId = generateToolCallId();
1288
- const brid = options.chainOpts?.blockchainRid || options.chainOpts?.network ? this.bridFromChainOpts(options.chainOpts) : await this.defaultOrgBrid() ?? this.blockchainRid;
1390
+ const brid = await this.callBrid(options.chainOpts);
1289
1391
  const orgKey = options.orgEncryptionPubKey ?? this.orgEncryptionPubKey ?? this._orgKeyFromChain;
1290
1392
  try {
1291
1393
  const signedHex = orgKey ? signEncryptedToolCall(
@@ -1335,6 +1437,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1335
1437
  if (!action?.trim()) {
1336
1438
  throw new Error("action is required and cannot be empty.");
1337
1439
  }
1440
+ if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
1441
+ const startedAt = Date.now();
1338
1442
  let chainOpts = options.chainOpts;
1339
1443
  if (options.orgName && this._explicitChain) {
1340
1444
  chainOpts = options.chainOpts ?? { network: this._defaultChain.network };
@@ -1359,12 +1463,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1359
1463
  }
1360
1464
  }
1361
1465
  }
1362
- const brid = this.bridFromChainOpts(chainOpts);
1363
- const logResult = await this.logToolCall(action, context, {
1364
- toolName: options.toolName,
1365
- toolArgsJson: options.toolArgsJson,
1366
- chainOpts
1367
- });
1466
+ const brid = await this.callBrid(chainOpts);
1467
+ const logResult = await this._logToolCall(
1468
+ action,
1469
+ context,
1470
+ { toolName: options.toolName, toolArgsJson: options.toolArgsJson, chainOpts },
1471
+ true
1472
+ );
1368
1473
  if (!logResult.success || !logResult.toolCallId || !logResult.signedHex) {
1369
1474
  throw new Error(logResult.error || "Failed to sign log_tool_call");
1370
1475
  }
@@ -1401,10 +1506,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1401
1506
  if (verifyPubKey !== void 0) {
1402
1507
  verifyResponseSignature(bodyBytes, resp, verifyPubKey);
1403
1508
  }
1404
- const data = parseJson(bodyBytes);
1509
+ const parsed = tryParseJson(bodyBytes.toString("utf-8"));
1510
+ if (!isRecord2(parsed)) {
1511
+ throw new JudgeRefusalError(
1512
+ "The judge's reply could not be read, so the action was not approved."
1513
+ );
1514
+ }
1515
+ const data = parsed;
1405
1516
  const rawScore = data.score;
1406
1517
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
1407
- return {
1518
+ const result = {
1408
1519
  verdict: normalizeVerdict(data.verdict),
1409
1520
  allow: this.executable(data),
1410
1521
  actionType: normalizeActionType(data.action_type),
@@ -1420,6 +1531,45 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1420
1531
  score,
1421
1532
  refusal: explicitRefusal(data)
1422
1533
  };
1534
+ return this.applyJailHold(result, startedAt);
1535
+ }
1536
+ /**
1537
+ * Start a local hold on an enforced, on-chain BLOCK (the verdict that jails).
1538
+ * Refuse any other result that lands while the agent is held, or whose
1539
+ * request was already in flight when the block arrived: the server judged it
1540
+ * on pre-jail state.
1541
+ */
1542
+ applyJailHold(result, startedAt) {
1543
+ const pubkey = this.auth.pubkey;
1544
+ if (result.verdict === "BLOCK" && result.enforced && result.onChain) {
1545
+ recordBlock(pubkey);
1546
+ holdJail(
1547
+ pubkey,
1548
+ () => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
1549
+ () => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
1550
+ );
1551
+ return result;
1552
+ }
1553
+ return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
1554
+ }
1555
+ /** The server's jailed refusal, produced locally while the agent is held. */
1556
+ localJailBlock(toolCallId = generateToolCallId()) {
1557
+ return {
1558
+ verdict: "BLOCK",
1559
+ allow: false,
1560
+ actionType: "block",
1561
+ reason: "Agent is jailed. All actions are blocked until the organization owner unjails this agent.",
1562
+ confidence: 1,
1563
+ provider: "policy",
1564
+ latencyMs: 0,
1565
+ toolCallId,
1566
+ onChain: false,
1567
+ enforced: true,
1568
+ enforcementMode: "enforce",
1569
+ status: "",
1570
+ score: void 0,
1571
+ refusal: "BLOCK"
1572
+ };
1423
1573
  }
1424
1574
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
1425
1575
  /**
@@ -1471,6 +1621,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1471
1621
  reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
1472
1622
  };
1473
1623
  }
1624
+ if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
1625
+ return { ...this.failJudge(errorMessage(err), err), allow: false };
1626
+ }
1474
1627
  return this.failJudge(errorMessage(err), err);
1475
1628
  }
1476
1629
  }
@@ -1500,6 +1653,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1500
1653
  * never overrides a refusal.
1501
1654
  */
1502
1655
  decide(result) {
1656
+ const inner = this.decideInner(result);
1657
+ if (!result.enforcementMode) return inner;
1658
+ return {
1659
+ ...inner,
1660
+ enforced: result.enforced,
1661
+ enforcementMode: result.enforcementMode
1662
+ };
1663
+ }
1664
+ decideInner(result) {
1503
1665
  const toolCallId = result.toolCallId;
1504
1666
  const deny = (verdict, reason) => ({
1505
1667
  allow: false,
@@ -1613,17 +1775,17 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1613
1775
  /**
1614
1776
  * Return the current status of a previously submitted judgment.
1615
1777
  *
1616
- * `chainOpts` names which chain the judgment was signed against. The
1617
- * server's GET /api/v1/judge routes to that chain when the SDK sends
1618
- * a `brid` query param; without it, the server falls back to public.
1619
- * Callers on the private chain must pass a `chainOpts` (or configure
1620
- * the client on the private chain) — otherwise polling a POSTed
1621
- * judgment on the private chain 404s at the server.
1778
+ * `chainOpts` names which chain the judgment was signed against; the SDK
1779
+ * sends it as the `brid` query param the server routes by. Without it the
1780
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
1781
+ * client's org chain when it has an `orgName`, else the client default.
1782
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
1783
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
1622
1784
  */
1623
1785
  async getJudgmentStatus(judgmentId, agentPubkey, chainOpts) {
1624
1786
  const pk = agentPubkey ?? this.auth.pubkey;
1625
- const brid = this.bridFromChainOpts(chainOpts);
1626
1787
  return this.track("getJudgmentStatus", pk, async () => {
1788
+ const brid = await this.callBrid(chainOpts);
1627
1789
  const resp = await this.http.get(
1628
1790
  "/api/v1/judge",
1629
1791
  { tool_call_id: judgmentId, agent_pubkey: pk, brid },
@@ -1961,6 +2123,18 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1961
2123
  if (chainOpts?.network === "public") return PUBLIC_CHAIN.blockchainRid;
1962
2124
  return this.blockchainRid;
1963
2125
  }
2126
+ /**
2127
+ * The chain one call's transactions are signed for: the per-call override
2128
+ * when given, otherwise the client's org, otherwise the client default.
2129
+ * log_tool_call, judge_action and the judgment-status read all use this, so
2130
+ * a request never pairs transactions signed for two different chains.
2131
+ */
2132
+ async callBrid(chainOpts) {
2133
+ if (chainOpts?.blockchainRid || chainOpts?.network) {
2134
+ return this.bridFromChainOpts(chainOpts);
2135
+ }
2136
+ return await this.defaultOrgBrid() ?? this.blockchainRid;
2137
+ }
1964
2138
  /**
1965
2139
  * Resolve the dashboard chain used by agent metadata/policy reads.
1966
2140
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -2006,8 +2180,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
2006
2180
  async riskEnginePost(body, network, brid) {
2007
2181
  let resp;
2008
2182
  try {
2009
- const path5 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2010
- resp = await this.http.post(path5, body, this.authHeaders(brid));
2183
+ const path4 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2184
+ resp = await this.http.post(path4, body, this.authHeaders(brid));
2011
2185
  } catch (err) {
2012
2186
  throw this.transportError(err);
2013
2187
  }
@@ -2268,10 +2442,6 @@ function tryParseJson(text) {
2268
2442
  return void 0;
2269
2443
  }
2270
2444
  }
2271
- function parseJson(bytes) {
2272
- const parsed = tryParseJson(bytes.toString("utf-8"));
2273
- return parsed === void 0 ? {} : parsed;
2274
- }
2275
2445
  function requestPath(url) {
2276
2446
  if (!url) return void 0;
2277
2447
  try {
@@ -2550,10 +2720,10 @@ function resolveBlockchainRid(chainOpts) {
2550
2720
  '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"`.'
2551
2721
  );
2552
2722
  }
2553
- const config = chainForNetwork(
2723
+ const config2 = chainForNetwork(
2554
2724
  chainOpts?.network ?? forcedChainNetwork() ?? "private"
2555
2725
  );
2556
- return config.blockchainRid;
2726
+ return config2.blockchainRid;
2557
2727
  }
2558
2728
  async function commitMemoryVersion(plaintext, auth, opts) {
2559
2729
  const key = await deriveMemoryKey(auth.privkey);
@@ -2779,7 +2949,34 @@ function classifyMemoryWrite(event, ctx, opts = {}) {
2779
2949
  // src-ts/memory/guard.ts
2780
2950
  var import_node_fs4 = require("fs");
2781
2951
  var import_node_os4 = __toESM(require("os"));
2952
+
2953
+ // src-ts/memory/slot.ts
2782
2954
  var import_node_path4 = __toESM(require("path"));
2955
+ function asciiLower(s) {
2956
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) | 32));
2957
+ }
2958
+ function spelledAs(target, base, p = import_node_path4.default) {
2959
+ const resolved = p.resolve(base);
2960
+ if (p.sep !== "\\") return resolved;
2961
+ const prefix = resolved.replace(/[\\/]+$/, "");
2962
+ const t = asciiLower(target);
2963
+ const b = asciiLower(prefix);
2964
+ if (t === b || t.startsWith(b + "\\") || t.startsWith(b + "/")) {
2965
+ return target.slice(0, prefix.length);
2966
+ }
2967
+ return resolved;
2968
+ }
2969
+ function memorySlotOf(target, home2, root, extraHomes, p = import_node_path4.default) {
2970
+ const resolved = p.resolve(target);
2971
+ return native.memorySlotForPath(
2972
+ resolved,
2973
+ home2 ? spelledAs(resolved, home2, p) : "",
2974
+ root ? spelledAs(resolved, root, p) : void 0,
2975
+ extraHomes
2976
+ );
2977
+ }
2978
+
2979
+ // src-ts/memory/guard.ts
2783
2980
  function emitDebugProbe(event, ctx, memEntry, logger2) {
2784
2981
  if (!logger2?.info) return;
2785
2982
  const ev = event ?? {};
@@ -2805,7 +3002,7 @@ function emitDebugProbe(event, ctx, memEntry, logger2) {
2805
3002
  }
2806
3003
  function memorySlotFor(key, root, extraHomes) {
2807
3004
  const home2 = process.env.HOME || import_node_os4.default.homedir() || "";
2808
- return native.memorySlotForPath(import_node_path4.default.resolve(key), home2, root, extraHomes);
3005
+ return memorySlotOf(key, home2, root, extraHomes);
2809
3006
  }
2810
3007
  async function resultingDocument(memEntry, event, ctx) {
2811
3008
  const edits = native.extractMemoryEdits(event, ctx);
@@ -3558,12 +3755,7 @@ var MemoryGuardManager = class {
3558
3755
  */
3559
3756
  slotFor(target) {
3560
3757
  const home2 = process.env.HOME || import_node_os5.default.homedir() || "";
3561
- return native.memorySlotForPath(
3562
- import_node_path7.default.resolve(target),
3563
- home2,
3564
- this.opts.memoryRoot,
3565
- this.opts.agentHomeDirs
3566
- );
3758
+ return memorySlotOf(target, home2, this.opts.memoryRoot, this.opts.agentHomeDirs);
3567
3759
  }
3568
3760
  /**
3569
3761
  * Verify every memory file this call reads, each against its own chain slot.
@@ -3853,6 +4045,7 @@ function diffMemorySnapshots(before, after) {
3853
4045
  EciesDomain,
3854
4046
  HttpClient,
3855
4047
  HttpTransportError,
4048
+ JudgeRefusalError,
3856
4049
  KEY_FILENAMES,
3857
4050
  MemoryGuardManager,
3858
4051
  MemoryIntegrityError,
@@ -3871,6 +4064,7 @@ function diffMemorySnapshots(before, after) {
3871
4064
  columnAad,
3872
4065
  commitMemoryVersion,
3873
4066
  configureEvents,
4067
+ configureJailHold,
3874
4068
  containsEvasionCharacters,
3875
4069
  containsSecret,
3876
4070
  createFileLogger,
@@ -3899,12 +4093,14 @@ function diffMemorySnapshots(before, after) {
3899
4093
  getRollbackHistory,
3900
4094
  guardMemoryWrite,
3901
4095
  isEnvelope,
4096
+ isJailHeld,
3902
4097
  isValidPrivateKey,
3903
4098
  keyFingerprintOf,
3904
4099
  keyPathCandidates,
3905
4100
  loadAgent,
3906
4101
  loadAgentFromFile,
3907
4102
  loadUserConfig,
4103
+ memorySlotOf,
3908
4104
  normalizeActionForHash,
3909
4105
  normalizeActionType,
3910
4106
  normalizeForMatching,
@@ -3918,6 +4114,7 @@ function diffMemorySnapshots(before, after) {
3918
4114
  recordEvent,
3919
4115
  redactJsonStrings,
3920
4116
  redactSecrets,
4117
+ releaseJailHolds,
3921
4118
  resolve,
3922
4119
  resolveKeyPath,
3923
4120
  rollbackMemory,