@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.mjs CHANGED
@@ -166,12 +166,12 @@ var AtbashAPIError = class extends Error {
166
166
  * ran does not carry. The path is the fact that settles it.
167
167
  */
168
168
  path;
169
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path5) {
170
- super(enrich(status, body, statusText, endpoint, path5));
169
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path4) {
170
+ super(enrich(status, body, statusText, endpoint, path4));
171
171
  this.name = "AtbashAPIError";
172
172
  this.status = status;
173
173
  this.body = body;
174
- this.path = path5;
174
+ this.path = path4;
175
175
  }
176
176
  };
177
177
  var SignatureVerificationError = class extends Error {
@@ -180,9 +180,18 @@ var SignatureVerificationError = class extends Error {
180
180
  this.name = "SignatureVerificationError";
181
181
  }
182
182
  };
183
- function enrich(status, body, statusText, endpoint, path5) {
183
+ var JudgeRefusalError = class extends Error {
184
+ constructor(message) {
185
+ super(message);
186
+ this.name = "JudgeRefusalError";
187
+ }
188
+ };
189
+ function isRefusalStatus(status) {
190
+ return status >= 400 && status < 500 && status !== 408;
191
+ }
192
+ function enrich(status, body, statusText, endpoint, path4) {
184
193
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
185
- let msg = `API error ${status}${path5 ? ` from ${path5}` : ""}: ${body || statusText}`;
194
+ let msg = `API error ${status}${path4 ? ` from ${path4}` : ""}: ${body || statusText}`;
186
195
  const lowered = body.toLowerCase();
187
196
  if (lowered.includes("agent not registered")) {
188
197
  msg += `
@@ -203,6 +212,61 @@ function enrich(status, body, statusText, endpoint, path5) {
203
212
  return msg;
204
213
  }
205
214
 
215
+ // src-ts/jail-hold.ts
216
+ var DEFAULTS = { ceilingMs: 3e4, pollMs: 1e3 };
217
+ var holds = /* @__PURE__ */ new Map();
218
+ var blockedAt = /* @__PURE__ */ new Map();
219
+ var BLOCK_MEMORY_MS = 10 * 6e4;
220
+ var config = { ...DEFAULTS };
221
+ function isJailHeld(pubkey) {
222
+ return holds.has(pubkey);
223
+ }
224
+ function recordBlock(pubkey) {
225
+ const now = Date.now();
226
+ for (const [key, at] of blockedAt) if (now - at > BLOCK_MEMORY_MS) blockedAt.delete(key);
227
+ blockedAt.set(pubkey, now);
228
+ }
229
+ function blockedSince(pubkey, startedAt) {
230
+ const at = blockedAt.get(pubkey);
231
+ return at !== void 0 && startedAt <= at;
232
+ }
233
+ function holdJail(pubkey, confirmJailed, onGiveUp) {
234
+ if (holds.has(pubkey)) return;
235
+ const hold = { timer: null };
236
+ holds.set(pubkey, hold);
237
+ const deadline = Date.now() + config.ceilingMs;
238
+ const tick = async () => {
239
+ if (holds.get(pubkey) !== hold) return;
240
+ let jailed = false;
241
+ try {
242
+ jailed = await confirmJailed();
243
+ } catch {
244
+ }
245
+ if (holds.get(pubkey) !== hold) return;
246
+ if (jailed) {
247
+ holds.delete(pubkey);
248
+ return;
249
+ }
250
+ if (Date.now() >= deadline) {
251
+ holds.delete(pubkey);
252
+ onGiveUp?.();
253
+ return;
254
+ }
255
+ const timer = setTimeout(() => void tick(), config.pollMs);
256
+ timer.unref?.();
257
+ hold.timer = timer;
258
+ };
259
+ void tick();
260
+ }
261
+ function releaseJailHolds() {
262
+ for (const hold of holds.values()) if (hold.timer) clearTimeout(hold.timer);
263
+ holds.clear();
264
+ blockedAt.clear();
265
+ }
266
+ function configureJailHold(next) {
267
+ config = { ...DEFAULTS, ...next };
268
+ }
269
+
206
270
  // src-ts/http/client.ts
207
271
  var HttpClient = class {
208
272
  baseUrl;
@@ -211,8 +275,8 @@ var HttpClient = class {
211
275
  this.baseUrl = baseUrl.replace(/\/+$/, "");
212
276
  this.timeoutMs = timeoutMs;
213
277
  }
214
- buildUrl(path5, query) {
215
- const url = new URL(this.baseUrl + path5);
278
+ buildUrl(path4, query) {
279
+ const url = new URL(this.baseUrl + path4);
216
280
  if (query) {
217
281
  for (const [k, v] of Object.entries(query)) {
218
282
  if (v !== void 0 && v !== null && v !== "") {
@@ -222,14 +286,14 @@ var HttpClient = class {
222
286
  }
223
287
  return url.toString();
224
288
  }
225
- async get(path5, query, headers) {
226
- return this.fetch(this.buildUrl(path5, query), {
289
+ async get(path4, query, headers) {
290
+ return this.fetch(this.buildUrl(path4, query), {
227
291
  method: "GET",
228
292
  ...headers && { headers }
229
293
  });
230
294
  }
231
- async post(path5, body, headers) {
232
- return this.fetch(this.buildUrl(path5), {
295
+ async post(path4, body, headers) {
296
+ return this.fetch(this.buildUrl(path4), {
233
297
  method: "POST",
234
298
  headers: { "Content-Type": "application/json", ...headers },
235
299
  body: JSON.stringify(body)
@@ -495,11 +559,12 @@ function eventsOptedOut() {
495
559
  const flag = process.env.ATBASH_TELEMETRY_EVENTS_DISABLED?.trim().toLowerCase();
496
560
  return Boolean(flag && ["1", "true", "yes", "on"].includes(flag));
497
561
  }
562
+ var SOURCE_SHAPE = /^(?:sdk|cli|plugin:[a-z0-9][a-z0-9._-]{0,31})$/;
498
563
  function configureEvents(next, opts = {}) {
499
564
  logger = next;
500
565
  eventsEnabled = next !== null && !eventsOptedOut();
501
566
  if (opts.version) sdkVersion = opts.version;
502
- if (opts.source) eventSource = opts.source;
567
+ if (opts.source && SOURCE_SHAPE.test(opts.source)) eventSource = opts.source;
503
568
  hashAgentKey = opts.hashAgentKey ?? null;
504
569
  }
505
570
  function eventsAreOn() {
@@ -552,6 +617,20 @@ import {
552
617
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
553
618
  import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
554
619
  import { createHash } from "crypto";
620
+ var SDK_VERSION = true ? "0.20.0-dev.0" : "";
621
+ var SDK_LANGUAGE = "node";
622
+ function sdkResourceAttributes() {
623
+ const attrs = {
624
+ "service.name": "atbash-sdk",
625
+ "sdk.language": SDK_LANGUAGE
626
+ };
627
+ if (SDK_VERSION) attrs["sdk.version"] = SDK_VERSION;
628
+ return attrs;
629
+ }
630
+ function sourceOrDefault(candidate) {
631
+ return typeof candidate === "string" && SOURCE_SHAPE.test(candidate) ? candidate : defaultSource;
632
+ }
633
+ var exitHookInstalled = false;
555
634
  var meterProvider = null;
556
635
  var loggerProvider = null;
557
636
  var callCounter = null;
@@ -569,8 +648,8 @@ function isTelemetryOptedOut() {
569
648
  const filePath = join2(home2, ".config", "atbash", "telemetry.json");
570
649
  const raw = readFileSync2(filePath, "utf-8").trim();
571
650
  if (!raw) return false;
572
- const config = JSON.parse(raw);
573
- return config.enabled === false;
651
+ const config2 = JSON.parse(raw);
652
+ return config2.enabled === false;
574
653
  } catch {
575
654
  return false;
576
655
  }
@@ -615,26 +694,26 @@ function customCollector() {
615
694
  }
616
695
  return { url: url.replace(/\/+$/, ""), headers: () => headers };
617
696
  }
618
- function setupTelemetry(config) {
619
- if (!config.enabled) return;
697
+ function setupTelemetry(config2) {
698
+ if (!config2.enabled) return;
620
699
  if (isTelemetryOptedOut()) return;
621
- if (config.source && !sourceExplicitlySet) {
622
- defaultSource = config.source;
700
+ if (config2.source && !sourceExplicitlySet && SOURCE_SHAPE.test(config2.source)) {
701
+ defaultSource = config2.source;
623
702
  sourceExplicitlySet = true;
624
703
  }
625
- if (config.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
626
- requestedExportIntervalMs = config.exportIntervalMs;
704
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
705
+ requestedExportIntervalMs = config2.exportIntervalMs;
627
706
  }
628
707
  if (meterProvider) return;
629
708
  const collector = customCollector();
630
- if (!collector && (!config.endpoint || !config.getAuthHeaders)) return;
709
+ if (!collector && (!config2.endpoint || !config2.getAuthHeaders)) return;
631
710
  if (!collector && /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
632
- config.endpoint
711
+ config2.endpoint
633
712
  ))
634
713
  return;
635
714
  const destination = collector ?? {
636
- url: `${config.endpoint.replace(/\/+$/, "")}/api/telemetry`,
637
- headers: () => config.getAuthHeaders()
715
+ url: `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`,
716
+ headers: () => config2.getAuthHeaders()
638
717
  };
639
718
  const proxyUrl = destination.url;
640
719
  const exporter = new OTLPMetricExporter({
@@ -658,9 +737,7 @@ function setupTelemetry(config) {
658
737
  exportIntervalMillis: requestedExportIntervalMs ?? 6e4
659
738
  });
660
739
  meterProvider = new MeterProvider({
661
- resource: resourceFromAttributes({
662
- "service.name": "atbash-sdk"
663
- }),
740
+ resource: resourceFromAttributes(sdkResourceAttributes()),
664
741
  readers: [reader]
665
742
  });
666
743
  const meter = meterProvider.getMeter("atbash-sdk");
@@ -671,7 +748,7 @@ function setupTelemetry(config) {
671
748
  description: "SDK function execution duration",
672
749
  unit: "ms"
673
750
  });
674
- counterHasher = config.hashAgentKey ?? defaultAgentKeyHasher;
751
+ counterHasher = config2.hashAgentKey ?? defaultAgentKeyHasher;
675
752
  const logExporter = new OTLPLogExporter({
676
753
  // A real collector takes OTLP at its own signal paths; the Atbash proxy
677
754
  // takes a query parameter, because it is one authenticated route.
@@ -679,12 +756,20 @@ function setupTelemetry(config) {
679
756
  headers: async () => destination.headers()
680
757
  });
681
758
  loggerProvider = new LoggerProvider({
682
- resource: resourceFromAttributes({ "service.name": "atbash-sdk" }),
759
+ resource: resourceFromAttributes(sdkResourceAttributes()),
683
760
  processors: [new BatchLogRecordProcessor({ exporter: logExporter })]
684
761
  });
762
+ if (!exitHookInstalled && typeof process?.once === "function") {
763
+ exitHookInstalled = true;
764
+ process.once("beforeExit", () => {
765
+ shutdownTelemetry().catch(() => {
766
+ });
767
+ });
768
+ }
685
769
  configureEvents(loggerProvider.getLogger("atbash-sdk"), {
686
- source: config.source ?? defaultSource,
687
- hashAgentKey: config.hashAgentKey ?? defaultAgentKeyHasher
770
+ source: sourceOrDefault(config2.source),
771
+ hashAgentKey: config2.hashAgentKey ?? defaultAgentKeyHasher,
772
+ version: SDK_VERSION
688
773
  });
689
774
  }
690
775
  function recordCall(functionName, source, agentPubkey) {
@@ -692,7 +777,7 @@ function recordCall(functionName, source, agentPubkey) {
692
777
  if (!callCounter) return;
693
778
  callCounter.add(1, {
694
779
  "function.name": functionName,
695
- source: source ?? defaultSource,
780
+ source: sourceOrDefault(source),
696
781
  // Hashed on this path too. The counters and the events export to the same
697
782
  // place in the same request, so a raw key here would make hashing it on
698
783
  // the event beside it pointless.
@@ -704,7 +789,7 @@ function recordDuration(functionName, durationMs, status, source) {
704
789
  durationHistogram.record(durationMs, {
705
790
  "function.name": functionName,
706
791
  status,
707
- source: source ?? defaultSource
792
+ source: sourceOrDefault(source)
708
793
  });
709
794
  }
710
795
  async function flushTelemetry() {
@@ -761,13 +846,13 @@ function loadUserConfig() {
761
846
  return {};
762
847
  }
763
848
  }
764
- function saveUserConfig(config) {
849
+ function saveUserConfig(config2) {
765
850
  const dir = getConfigDir();
766
851
  if (!existsSync2(dir)) {
767
852
  mkdirSync(dir, { recursive: true, mode: 448 });
768
853
  }
769
854
  const filePath = getConfigPath();
770
- writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", {
855
+ writeFileSync(filePath, JSON.stringify(config2, null, 2) + "\n", {
771
856
  mode: 384
772
857
  });
773
858
  chmodSync(filePath, 384);
@@ -802,6 +887,7 @@ function forcedChainNetwork(flagValue) {
802
887
  }
803
888
 
804
889
  // src-ts/client.ts
890
+ var AGENT_NOT_REGISTERED = "Agent not registered. Onboard the agent at the dashboard before submitting actions.";
805
891
  function generateToolCallId() {
806
892
  return `tc-${Date.now()}-${randomHex(4)}`;
807
893
  }
@@ -1150,6 +1236,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1150
1236
  * return the signed tx hex. The server broadcasts to chain.
1151
1237
  */
1152
1238
  async logToolCall(action, context = "", options = {}) {
1239
+ return this._logToolCall(action, context, options, false);
1240
+ }
1241
+ /**
1242
+ * `raisePreflightErrors` (the judge path, like Python's
1243
+ * `raise_preflight_errors`): a failed registration check throws instead of
1244
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
1245
+ * (not registered, or a 4xx) from an outage.
1246
+ */
1247
+ async _logToolCall(action, context, options, raisePreflightErrors) {
1153
1248
  const start = performance.now();
1154
1249
  recordCall("logToolCall", void 0, this.auth.pubkey);
1155
1250
  let exists;
@@ -1159,18 +1254,20 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1159
1254
  });
1160
1255
  } catch (err) {
1161
1256
  recordDuration("logToolCall", performance.now() - start, "error");
1257
+ if (raisePreflightErrors) throw err;
1162
1258
  return { success: false, toolCallId: null, error: errorMessage(err) };
1163
1259
  }
1164
1260
  if (!exists) {
1165
1261
  recordDuration("logToolCall", performance.now() - start, "error");
1262
+ if (raisePreflightErrors) throw new JudgeRefusalError(AGENT_NOT_REGISTERED);
1166
1263
  return {
1167
1264
  success: false,
1168
1265
  toolCallId: null,
1169
- error: "Agent not registered. Onboard the agent at the dashboard before submitting actions."
1266
+ error: AGENT_NOT_REGISTERED
1170
1267
  };
1171
1268
  }
1172
1269
  const toolCallId = generateToolCallId();
1173
- const brid = options.chainOpts?.blockchainRid || options.chainOpts?.network ? this.bridFromChainOpts(options.chainOpts) : await this.defaultOrgBrid() ?? this.blockchainRid;
1270
+ const brid = await this.callBrid(options.chainOpts);
1174
1271
  const orgKey = options.orgEncryptionPubKey ?? this.orgEncryptionPubKey ?? this._orgKeyFromChain;
1175
1272
  try {
1176
1273
  const signedHex = orgKey ? signEncryptedToolCall(
@@ -1220,6 +1317,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1220
1317
  if (!action?.trim()) {
1221
1318
  throw new Error("action is required and cannot be empty.");
1222
1319
  }
1320
+ if (isJailHeld(this.auth.pubkey)) return this.localJailBlock();
1321
+ const startedAt = Date.now();
1223
1322
  let chainOpts = options.chainOpts;
1224
1323
  if (options.orgName && this._explicitChain) {
1225
1324
  chainOpts = options.chainOpts ?? { network: this._defaultChain.network };
@@ -1244,12 +1343,13 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1244
1343
  }
1245
1344
  }
1246
1345
  }
1247
- const brid = this.bridFromChainOpts(chainOpts);
1248
- const logResult = await this.logToolCall(action, context, {
1249
- toolName: options.toolName,
1250
- toolArgsJson: options.toolArgsJson,
1251
- chainOpts
1252
- });
1346
+ const brid = await this.callBrid(chainOpts);
1347
+ const logResult = await this._logToolCall(
1348
+ action,
1349
+ context,
1350
+ { toolName: options.toolName, toolArgsJson: options.toolArgsJson, chainOpts },
1351
+ true
1352
+ );
1253
1353
  if (!logResult.success || !logResult.toolCallId || !logResult.signedHex) {
1254
1354
  throw new Error(logResult.error || "Failed to sign log_tool_call");
1255
1355
  }
@@ -1286,10 +1386,16 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1286
1386
  if (verifyPubKey !== void 0) {
1287
1387
  verifyResponseSignature(bodyBytes, resp, verifyPubKey);
1288
1388
  }
1289
- const data = parseJson(bodyBytes);
1389
+ const parsed = tryParseJson(bodyBytes.toString("utf-8"));
1390
+ if (!isRecord2(parsed)) {
1391
+ throw new JudgeRefusalError(
1392
+ "The judge's reply could not be read, so the action was not approved."
1393
+ );
1394
+ }
1395
+ const data = parsed;
1290
1396
  const rawScore = data.score;
1291
1397
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
1292
- return {
1398
+ const result = {
1293
1399
  verdict: normalizeVerdict(data.verdict),
1294
1400
  allow: this.executable(data),
1295
1401
  actionType: normalizeActionType(data.action_type),
@@ -1305,6 +1411,45 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1305
1411
  score,
1306
1412
  refusal: explicitRefusal(data)
1307
1413
  };
1414
+ return this.applyJailHold(result, startedAt);
1415
+ }
1416
+ /**
1417
+ * Start a local hold on an enforced, on-chain BLOCK (the verdict that jails).
1418
+ * Refuse any other result that lands while the agent is held, or whose
1419
+ * request was already in flight when the block arrived: the server judged it
1420
+ * on pre-jail state.
1421
+ */
1422
+ applyJailHold(result, startedAt) {
1423
+ const pubkey = this.auth.pubkey;
1424
+ if (result.verdict === "BLOCK" && result.enforced && result.onChain) {
1425
+ recordBlock(pubkey);
1426
+ holdJail(
1427
+ pubkey,
1428
+ () => this.getAgentPolicy(pubkey).then((p) => p.isJailed),
1429
+ () => this.logger.warn?.("[atbash] local jail hold released: the chain did not confirm the jail in time")
1430
+ );
1431
+ return result;
1432
+ }
1433
+ return isJailHeld(pubkey) || blockedSince(pubkey, startedAt) ? this.localJailBlock(result.toolCallId) : result;
1434
+ }
1435
+ /** The server's jailed refusal, produced locally while the agent is held. */
1436
+ localJailBlock(toolCallId = generateToolCallId()) {
1437
+ return {
1438
+ verdict: "BLOCK",
1439
+ allow: false,
1440
+ actionType: "block",
1441
+ reason: "Agent is jailed. All actions are blocked until the organization owner unjails this agent.",
1442
+ confidence: 1,
1443
+ provider: "policy",
1444
+ latencyMs: 0,
1445
+ toolCallId,
1446
+ onChain: false,
1447
+ enforced: true,
1448
+ enforcementMode: "enforce",
1449
+ status: "",
1450
+ score: void 0,
1451
+ refusal: "BLOCK"
1452
+ };
1308
1453
  }
1309
1454
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
1310
1455
  /**
@@ -1356,6 +1501,9 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1356
1501
  reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
1357
1502
  };
1358
1503
  }
1504
+ if (err instanceof JudgeRefusalError || err instanceof AtbashAPIError && isRefusalStatus(err.status)) {
1505
+ return { ...this.failJudge(errorMessage(err), err), allow: false };
1506
+ }
1359
1507
  return this.failJudge(errorMessage(err), err);
1360
1508
  }
1361
1509
  }
@@ -1385,6 +1533,15 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1385
1533
  * never overrides a refusal.
1386
1534
  */
1387
1535
  decide(result) {
1536
+ const inner = this.decideInner(result);
1537
+ if (!result.enforcementMode) return inner;
1538
+ return {
1539
+ ...inner,
1540
+ enforced: result.enforced,
1541
+ enforcementMode: result.enforcementMode
1542
+ };
1543
+ }
1544
+ decideInner(result) {
1388
1545
  const toolCallId = result.toolCallId;
1389
1546
  const deny = (verdict, reason) => ({
1390
1547
  allow: false,
@@ -1498,17 +1655,17 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1498
1655
  /**
1499
1656
  * Return the current status of a previously submitted judgment.
1500
1657
  *
1501
- * `chainOpts` names which chain the judgment was signed against. The
1502
- * server's GET /api/v1/judge routes to that chain when the SDK sends
1503
- * a `brid` query param; without it, the server falls back to public.
1504
- * Callers on the private chain must pass a `chainOpts` (or configure
1505
- * the client on the private chain) — otherwise polling a POSTed
1506
- * judgment on the private chain 404s at the server.
1658
+ * `chainOpts` names which chain the judgment was signed against; the SDK
1659
+ * sends it as the `brid` query param the server routes by. Without it the
1660
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
1661
+ * client's org chain when it has an `orgName`, else the client default.
1662
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
1663
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
1507
1664
  */
1508
1665
  async getJudgmentStatus(judgmentId, agentPubkey, chainOpts) {
1509
1666
  const pk = agentPubkey ?? this.auth.pubkey;
1510
- const brid = this.bridFromChainOpts(chainOpts);
1511
1667
  return this.track("getJudgmentStatus", pk, async () => {
1668
+ const brid = await this.callBrid(chainOpts);
1512
1669
  const resp = await this.http.get(
1513
1670
  "/api/v1/judge",
1514
1671
  { tool_call_id: judgmentId, agent_pubkey: pk, brid },
@@ -1846,6 +2003,18 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1846
2003
  if (chainOpts?.network === "public") return PUBLIC_CHAIN.blockchainRid;
1847
2004
  return this.blockchainRid;
1848
2005
  }
2006
+ /**
2007
+ * The chain one call's transactions are signed for: the per-call override
2008
+ * when given, otherwise the client's org, otherwise the client default.
2009
+ * log_tool_call, judge_action and the judgment-status read all use this, so
2010
+ * a request never pairs transactions signed for two different chains.
2011
+ */
2012
+ async callBrid(chainOpts) {
2013
+ if (chainOpts?.blockchainRid || chainOpts?.network) {
2014
+ return this.bridFromChainOpts(chainOpts);
2015
+ }
2016
+ return await this.defaultOrgBrid() ?? this.blockchainRid;
2017
+ }
1849
2018
  /**
1850
2019
  * Resolve the dashboard chain used by agent metadata/policy reads.
1851
2020
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -1891,8 +2060,8 @@ config.json still holds an "agentKey" (${identity}), but a settings file no long
1891
2060
  async riskEnginePost(body, network, brid) {
1892
2061
  let resp;
1893
2062
  try {
1894
- const path5 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
1895
- resp = await this.http.post(path5, body, this.authHeaders(brid));
2063
+ const path4 = network ? `/api/risk-engine?network=${encodeURIComponent(network)}` : "/api/risk-engine";
2064
+ resp = await this.http.post(path4, body, this.authHeaders(brid));
1896
2065
  } catch (err) {
1897
2066
  throw this.transportError(err);
1898
2067
  }
@@ -2153,10 +2322,6 @@ function tryParseJson(text) {
2153
2322
  return void 0;
2154
2323
  }
2155
2324
  }
2156
- function parseJson(bytes) {
2157
- const parsed = tryParseJson(bytes.toString("utf-8"));
2158
- return parsed === void 0 ? {} : parsed;
2159
- }
2160
2325
  function requestPath(url) {
2161
2326
  if (!url) return void 0;
2162
2327
  try {
@@ -2435,10 +2600,10 @@ function resolveBlockchainRid(chainOpts) {
2435
2600
  '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"`.'
2436
2601
  );
2437
2602
  }
2438
- const config = chainForNetwork(
2603
+ const config2 = chainForNetwork(
2439
2604
  chainOpts?.network ?? forcedChainNetwork() ?? "private"
2440
2605
  );
2441
- return config.blockchainRid;
2606
+ return config2.blockchainRid;
2442
2607
  }
2443
2608
  async function commitMemoryVersion(plaintext, auth, opts) {
2444
2609
  const key = await deriveMemoryKey(auth.privkey);
@@ -2664,7 +2829,34 @@ function classifyMemoryWrite(event, ctx, opts = {}) {
2664
2829
  // src-ts/memory/guard.ts
2665
2830
  import { promises as fs } from "fs";
2666
2831
  import os from "os";
2667
- import path from "path";
2832
+
2833
+ // src-ts/memory/slot.ts
2834
+ import nodePath from "path";
2835
+ function asciiLower(s) {
2836
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) | 32));
2837
+ }
2838
+ function spelledAs(target, base, p = nodePath) {
2839
+ const resolved = p.resolve(base);
2840
+ if (p.sep !== "\\") return resolved;
2841
+ const prefix = resolved.replace(/[\\/]+$/, "");
2842
+ const t = asciiLower(target);
2843
+ const b = asciiLower(prefix);
2844
+ if (t === b || t.startsWith(b + "\\") || t.startsWith(b + "/")) {
2845
+ return target.slice(0, prefix.length);
2846
+ }
2847
+ return resolved;
2848
+ }
2849
+ function memorySlotOf(target, home2, root, extraHomes, p = nodePath) {
2850
+ const resolved = p.resolve(target);
2851
+ return native.memorySlotForPath(
2852
+ resolved,
2853
+ home2 ? spelledAs(resolved, home2, p) : "",
2854
+ root ? spelledAs(resolved, root, p) : void 0,
2855
+ extraHomes
2856
+ );
2857
+ }
2858
+
2859
+ // src-ts/memory/guard.ts
2668
2860
  function emitDebugProbe(event, ctx, memEntry, logger2) {
2669
2861
  if (!logger2?.info) return;
2670
2862
  const ev = event ?? {};
@@ -2690,7 +2882,7 @@ function emitDebugProbe(event, ctx, memEntry, logger2) {
2690
2882
  }
2691
2883
  function memorySlotFor(key, root, extraHomes) {
2692
2884
  const home2 = process.env.HOME || os.homedir() || "";
2693
- return native.memorySlotForPath(path.resolve(key), home2, root, extraHomes);
2885
+ return memorySlotOf(key, home2, root, extraHomes);
2694
2886
  }
2695
2887
  async function resultingDocument(memEntry, event, ctx) {
2696
2888
  const edits = native.extractMemoryEdits(event, ctx);
@@ -2893,7 +3085,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
2893
3085
 
2894
3086
  // src-ts/memory/pointer-store.ts
2895
3087
  import { promises as fs2 } from "fs";
2896
- import path2 from "path";
3088
+ import path from "path";
2897
3089
  var VERSION = 2;
2898
3090
  var ZERO = { activeId: null, checkedAt: 0 };
2899
3091
  var PointerStore = class {
@@ -2946,19 +3138,19 @@ var PointerStore = class {
2946
3138
  this.cache = { version: VERSION, agents: {} };
2947
3139
  }
2948
3140
  async persist(file) {
2949
- await fs2.mkdir(path2.dirname(this.filePath), { recursive: true });
3141
+ await fs2.mkdir(path.dirname(this.filePath), { recursive: true });
2950
3142
  const tmp = `${this.filePath}.${process.pid}.tmp`;
2951
3143
  await fs2.writeFile(tmp, JSON.stringify(file, null, 2), "utf8");
2952
3144
  await fs2.rename(tmp, this.filePath);
2953
3145
  }
2954
3146
  };
2955
3147
  function defaultPointerPath(workspaceDir = process.cwd()) {
2956
- return path2.join(workspaceDir, ".atbash", "memory-pointer.json");
3148
+ return path.join(workspaceDir, ".atbash", "memory-pointer.json");
2957
3149
  }
2958
3150
 
2959
3151
  // src-ts/memory/file-logger.ts
2960
3152
  import { promises as fs3 } from "fs";
2961
- import path3 from "path";
3153
+ import path2 from "path";
2962
3154
  function formatMeta(meta) {
2963
3155
  if (!meta || Object.keys(meta).length === 0) return "";
2964
3156
  try {
@@ -2971,7 +3163,7 @@ function createFileLogger(filePath, upstream) {
2971
3163
  let queue = Promise.resolve();
2972
3164
  let writeError;
2973
3165
  async function ensureDir() {
2974
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
3166
+ await fs3.mkdir(path2.dirname(filePath), { recursive: true });
2975
3167
  }
2976
3168
  function append(level, message, meta) {
2977
3169
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
@@ -2997,7 +3189,7 @@ function createFileLogger(filePath, upstream) {
2997
3189
  };
2998
3190
  }
2999
3191
  function defaultPluginLogPath(workspaceDir = process.cwd()) {
3000
- return path3.join(workspaceDir, ".atbash", "plugin.log");
3192
+ return path2.join(workspaceDir, ".atbash", "plugin.log");
3001
3193
  }
3002
3194
 
3003
3195
  // src-ts/memory/read-classifier.ts
@@ -3013,14 +3205,14 @@ function classifyMemoryRead(event, ctx, opts = {}) {
3013
3205
  // src-ts/memory/guard-manager.ts
3014
3206
  import { promises as fs4 } from "fs";
3015
3207
  import os2 from "os";
3016
- import path4 from "path";
3208
+ import path3 from "path";
3017
3209
  var DEFAULT_SYNC_TTL_MS = 3e4;
3018
3210
  var MemoryGuardManager = class {
3019
3211
  constructor(opts) {
3020
3212
  this.opts = opts;
3021
3213
  const workspaceDir = opts.workspaceDir;
3022
3214
  this.workspaceDir = workspaceDir;
3023
- this.memoryFilePath = opts.memoryFilePath ?? path4.join(workspaceDir, "MEMORY.md");
3215
+ this.memoryFilePath = opts.memoryFilePath ?? path3.join(workspaceDir, "MEMORY.md");
3024
3216
  this.pointerStore = new PointerStore(opts.pointerFilePath ?? defaultPointerPath(workspaceDir));
3025
3217
  this.logger = createFileLogger(
3026
3218
  opts.logFilePath ?? defaultPluginLogPath(workspaceDir),
@@ -3235,7 +3427,7 @@ var MemoryGuardManager = class {
3235
3427
  */
3236
3428
  async locate(slot, roots) {
3237
3429
  for (const root of roots) {
3238
- const candidate = path4.join(root, slot);
3430
+ const candidate = path3.join(root, slot);
3239
3431
  try {
3240
3432
  await fs4.access(candidate);
3241
3433
  return candidate;
@@ -3250,7 +3442,7 @@ var MemoryGuardManager = class {
3250
3442
  /** Roots a chain slot may be joined to, most specific first. */
3251
3443
  async memoryRoots() {
3252
3444
  const roots = [];
3253
- if (this.opts.memoryRoot) roots.push(path4.resolve(this.opts.memoryRoot));
3445
+ if (this.opts.memoryRoot) roots.push(path3.resolve(this.opts.memoryRoot));
3254
3446
  const home2 = process.env.HOME || os2.homedir() || "";
3255
3447
  if (home2) {
3256
3448
  const names = [
@@ -3258,7 +3450,7 @@ var MemoryGuardManager = class {
3258
3450
  ...this.opts.agentHomeDirs ?? []
3259
3451
  ];
3260
3452
  for (const name of names) {
3261
- const candidate = path4.join(home2, name);
3453
+ const candidate = path3.join(home2, name);
3262
3454
  try {
3263
3455
  if ((await fs4.stat(candidate)).isDirectory()) roots.push(candidate);
3264
3456
  } catch {
@@ -3396,7 +3588,7 @@ var MemoryGuardManager = class {
3396
3588
  const rec = bag;
3397
3589
  for (const k of KEYS) {
3398
3590
  const v = rec[k];
3399
- if (typeof v === "string" && v) found.add(path4.resolve(this.workspaceDir, v));
3591
+ if (typeof v === "string" && v) found.add(path3.resolve(this.workspaceDir, v));
3400
3592
  }
3401
3593
  }
3402
3594
  }
@@ -3443,12 +3635,7 @@ var MemoryGuardManager = class {
3443
3635
  */
3444
3636
  slotFor(target) {
3445
3637
  const home2 = process.env.HOME || os2.homedir() || "";
3446
- return native.memorySlotForPath(
3447
- path4.resolve(target),
3448
- home2,
3449
- this.opts.memoryRoot,
3450
- this.opts.agentHomeDirs
3451
- );
3638
+ return memorySlotOf(target, home2, this.opts.memoryRoot, this.opts.agentHomeDirs);
3452
3639
  }
3453
3640
  /**
3454
3641
  * Verify every memory file this call reads, each against its own chain slot.
@@ -3597,7 +3784,7 @@ var MemoryGuardManager = class {
3597
3784
  }
3598
3785
  /** Replace `target` in one step, so a reader never sees a half-written file. */
3599
3786
  async writeFileAtomic(target, content) {
3600
- await fs4.mkdir(path4.dirname(target), { recursive: true });
3787
+ await fs4.mkdir(path3.dirname(target), { recursive: true });
3601
3788
  const tmp = `${target}.${process.pid}.tmp`;
3602
3789
  await fs4.writeFile(tmp, content, "utf8");
3603
3790
  await fs4.rename(tmp, target);
@@ -3737,6 +3924,7 @@ export {
3737
3924
  EciesDomain,
3738
3925
  HttpClient,
3739
3926
  HttpTransportError,
3927
+ JudgeRefusalError,
3740
3928
  KEY_FILENAMES,
3741
3929
  MemoryGuardManager,
3742
3930
  MemoryIntegrityError,
@@ -3755,6 +3943,7 @@ export {
3755
3943
  columnAad,
3756
3944
  commitMemoryVersion,
3757
3945
  configureEvents,
3946
+ configureJailHold,
3758
3947
  containsEvasionCharacters,
3759
3948
  containsSecret,
3760
3949
  createFileLogger,
@@ -3783,12 +3972,14 @@ export {
3783
3972
  getRollbackHistory,
3784
3973
  guardMemoryWrite,
3785
3974
  isEnvelope,
3975
+ isJailHeld,
3786
3976
  isValidPrivateKey,
3787
3977
  keyFingerprintOf,
3788
3978
  keyPathCandidates,
3789
3979
  loadAgent,
3790
3980
  loadAgentFromFile,
3791
3981
  loadUserConfig,
3982
+ memorySlotOf,
3792
3983
  normalizeActionForHash,
3793
3984
  normalizeActionType,
3794
3985
  normalizeForMatching,
@@ -3802,6 +3993,7 @@ export {
3802
3993
  recordEvent,
3803
3994
  redactJsonStrings,
3804
3995
  redactSecrets,
3996
+ releaseJailHolds,
3805
3997
  resolve,
3806
3998
  resolveKeyPath,
3807
3999
  rollbackMemory,