@atbash/sdk 0.9.0 → 0.9.2

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
@@ -3026,6 +3026,13 @@ function buildAllowedJudgeHosts() {
3026
3026
  ]);
3027
3027
  }
3028
3028
  var ALLOWED_JUDGE_HOSTS = buildAllowedJudgeHosts();
3029
+ function isAllowlistedJudgeHost(endpoint) {
3030
+ try {
3031
+ return ALLOWED_JUDGE_HOSTS.has(new URL(endpoint).hostname.toLowerCase());
3032
+ } catch {
3033
+ return false;
3034
+ }
3035
+ }
3029
3036
  function validateJudgeEndpoint(judge) {
3030
3037
  const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
3031
3038
  const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
@@ -3073,11 +3080,23 @@ var AtbashAPIError = class extends Error {
3073
3080
  status;
3074
3081
  /** Raw response body text (may be empty). */
3075
3082
  body;
3076
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT) {
3077
- super(enrich(status, body, statusText, endpoint));
3083
+ /**
3084
+ * Request path this status came from, e.g. `/api/v1/judge` — absent when the
3085
+ * request never reached a response (transport failures carry status 0).
3086
+ *
3087
+ * A status alone does not say what failed. Several routes answer one call, and
3088
+ * more than one of them can 404: a caller reading only `404` cannot tell "this
3089
+ * agent is not registered" from "the chain-routing lookup did not resolve", and
3090
+ * has to guess from the body's prose — which a 404 raised before any handler
3091
+ * ran does not carry. The path is the fact that settles it.
3092
+ */
3093
+ path;
3094
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path7) {
3095
+ super(enrich(status, body, statusText, endpoint, path7));
3078
3096
  this.name = "AtbashAPIError";
3079
3097
  this.status = status;
3080
3098
  this.body = body;
3099
+ this.path = path7;
3081
3100
  }
3082
3101
  };
3083
3102
  var SignatureVerificationError = class extends Error {
@@ -3086,9 +3105,9 @@ var SignatureVerificationError = class extends Error {
3086
3105
  this.name = "SignatureVerificationError";
3087
3106
  }
3088
3107
  };
3089
- function enrich(status, body, statusText, endpoint) {
3108
+ function enrich(status, body, statusText, endpoint, path7) {
3090
3109
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
3091
- let msg = `API error ${status}: ${body || statusText}`;
3110
+ let msg = `API error ${status}${path7 ? ` from ${path7}` : ""}: ${body || statusText}`;
3092
3111
  const lowered = body.toLowerCase();
3093
3112
  if (lowered.includes("agent not registered")) {
3094
3113
  msg += `
@@ -3178,6 +3197,13 @@ function classifyTransportError(err, url, method, timeoutMs) {
3178
3197
  { cause: err }
3179
3198
  );
3180
3199
  }
3200
+ if (code2 === "UND_ERR_CONNECT_TIMEOUT") {
3201
+ return new HttpTransportError(
3202
+ "timeout",
3203
+ `${method} ${url} could not establish a connection before the transport deadline \u2014 check the endpoint and network`,
3204
+ { cause: err }
3205
+ );
3206
+ }
3181
3207
  if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") {
3182
3208
  return new HttpTransportError(
3183
3209
  "dns",
@@ -3342,6 +3368,8 @@ var meterProvider = null;
3342
3368
  var callCounter = null;
3343
3369
  var durationHistogram = null;
3344
3370
  var defaultSource = "sdk";
3371
+ var sourceExplicitlySet = false;
3372
+ var requestedExportIntervalMs;
3345
3373
  function isTelemetryOptedOut() {
3346
3374
  const disabled = process.env.ATBASH_TELEMETRY_DISABLED?.trim().toLowerCase();
3347
3375
  if (disabled && ["1", "true", "yes", "on"].includes(disabled)) {
@@ -3365,20 +3393,41 @@ function autoInit() {
3365
3393
  }
3366
3394
  function setupTelemetry(config2) {
3367
3395
  if (!config2.enabled) return;
3368
- if (meterProvider) return;
3369
3396
  if (isTelemetryOptedOut()) return;
3397
+ if (config2.source && !sourceExplicitlySet) {
3398
+ defaultSource = config2.source;
3399
+ sourceExplicitlySet = true;
3400
+ }
3401
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
3402
+ requestedExportIntervalMs = config2.exportIntervalMs;
3403
+ }
3404
+ if (meterProvider) return;
3370
3405
  if (!config2.endpoint || !config2.getAuthHeaders) return;
3371
- if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(config2.endpoint)) return;
3372
- defaultSource = config2.source ?? "sdk";
3406
+ if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
3407
+ config2.endpoint
3408
+ ))
3409
+ return;
3373
3410
  const proxyUrl = `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`;
3374
3411
  const getAuthHeaders = config2.getAuthHeaders;
3375
3412
  const exporter = new import_exporter_metrics_otlp_http.OTLPMetricExporter({
3376
3413
  url: proxyUrl,
3377
- headers: async () => getAuthHeaders()
3414
+ headers: async () => getAuthHeaders(),
3415
+ // DELTA, not the OTel default of CUMULATIVE.
3416
+ //
3417
+ // A cumulative counter reports a running total, and the backend recovers
3418
+ // the increment by differencing consecutive points of a series. That needs
3419
+ // at least two points. Most Atbash callers are short-lived — a CLI run
3420
+ // exports once and exits — so their series has exactly one point, nothing
3421
+ // to difference against, and the usage board reads them as zero: the calls
3422
+ // arrive, carry the right `source`, and still count for nothing.
3423
+ //
3424
+ // A delta counter reports the increment itself, so a single export is a
3425
+ // whole truthful datapoint and SUM over any window is the real call count.
3426
+ temporalityPreference: import_exporter_metrics_otlp_http.AggregationTemporalityPreference.DELTA
3378
3427
  });
3379
3428
  const reader = new import_sdk_metrics.PeriodicExportingMetricReader({
3380
3429
  exporter,
3381
- exportIntervalMillis: config2.exportIntervalMs ?? 6e4
3430
+ exportIntervalMillis: requestedExportIntervalMs ?? 6e4
3382
3431
  });
3383
3432
  meterProvider = new import_sdk_metrics.MeterProvider({
3384
3433
  resource: (0, import_resources.resourceFromAttributes)({
@@ -3422,6 +3471,9 @@ async function shutdownTelemetry() {
3422
3471
  meterProvider = null;
3423
3472
  callCounter = null;
3424
3473
  durationHistogram = null;
3474
+ defaultSource = "sdk";
3475
+ sourceExplicitlySet = false;
3476
+ requestedExportIntervalMs = void 0;
3425
3477
  }
3426
3478
 
3427
3479
  // src-ts/userConfig.ts
@@ -3609,17 +3661,23 @@ var Atbash = class _Atbash {
3609
3661
  verifying: this.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
3610
3662
  });
3611
3663
  }
3612
- try {
3613
- setupTelemetry({
3614
- enabled: true,
3615
- source: "sdk",
3616
- endpoint: this.endpoint,
3617
- getAuthHeaders: () => this.authHeaders()
3618
- });
3619
- } catch (err) {
3620
- this.logger.warn?.(
3621
- "[atbash] telemetry setup failed \u2014 continuing without metrics",
3622
- { error: String(err) }
3664
+ if (isAllowlistedJudgeHost(this.endpoint)) {
3665
+ try {
3666
+ setupTelemetry({
3667
+ enabled: true,
3668
+ endpoint: this.endpoint,
3669
+ getAuthHeaders: () => this.authHeaders()
3670
+ });
3671
+ } catch (err) {
3672
+ this.logger.warn?.(
3673
+ "[atbash] telemetry setup failed \u2014 continuing without metrics",
3674
+ { error: String(err) }
3675
+ );
3676
+ }
3677
+ } else {
3678
+ this.logger.info?.(
3679
+ "[atbash] telemetry disabled \u2014 the agent bearer is only sent to hosts that already receive it, and this judge endpoint is not one",
3680
+ { endpoint: this.endpoint }
3623
3681
  );
3624
3682
  }
3625
3683
  }
@@ -4500,7 +4558,8 @@ var Atbash = class _Atbash {
4500
4558
  resp.status,
4501
4559
  await safeText(resp),
4502
4560
  resp.statusText,
4503
- this.endpoint
4561
+ this.endpoint,
4562
+ requestPath(resp.url)
4504
4563
  );
4505
4564
  }
4506
4565
  /**
@@ -4642,6 +4701,14 @@ function parseJson(bytes) {
4642
4701
  const parsed = tryParseJson(bytes.toString("utf-8"));
4643
4702
  return parsed === void 0 ? {} : parsed;
4644
4703
  }
4704
+ function requestPath(url) {
4705
+ if (!url) return void 0;
4706
+ try {
4707
+ return new URL(url).pathname || void 0;
4708
+ } catch {
4709
+ return void 0;
4710
+ }
4711
+ }
4645
4712
  async function safeText(resp) {
4646
4713
  try {
4647
4714
  return await resp.text();
@@ -35299,9 +35366,9 @@ var ZodUnion = class extends ZodType {
35299
35366
  }
35300
35367
  };
35301
35368
  types.ZodUnion = ZodUnion;
35302
- ZodUnion.create = (types2, params) => {
35369
+ ZodUnion.create = (types3, params) => {
35303
35370
  return new ZodUnion({
35304
- options: types2,
35371
+ options: types3,
35305
35372
  typeName: ZodFirstPartyTypeKind.ZodUnion,
35306
35373
  ...processCreateParams(params)
35307
35374
  });
@@ -38758,9 +38825,9 @@ _nodeUtil.exports;
38758
38825
  var freeProcess = moduleExports && freeGlobal2.process;
38759
38826
  var nodeUtil2 = (function() {
38760
38827
  try {
38761
- var types2 = freeModule && freeModule.require && freeModule.require("util").types;
38762
- if (types2) {
38763
- return types2;
38828
+ var types3 = freeModule && freeModule.require && freeModule.require("util").types;
38829
+ if (types3) {
38830
+ return types3;
38764
38831
  }
38765
38832
  return freeProcess && freeProcess.binding && freeProcess.binding("util");
38766
38833
  } catch (e) {
@@ -43008,7 +43075,7 @@ async function commitMemoryVersion(plaintext, auth, opts) {
43008
43075
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43009
43076
  const client = await buildChainClient(chainOpts);
43010
43077
  const { sigProvider } = buildSigner(auth);
43011
- await client.signAndSendUniqueTransaction(
43078
+ const receipt = await client.signAndSendUniqueTransaction(
43012
43079
  {
43013
43080
  name: native.OP_ADD_AGENT_MEMORY,
43014
43081
  args: [
@@ -43021,6 +43088,9 @@ async function commitMemoryVersion(plaintext, auth, opts) {
43021
43088
  },
43022
43089
  sigProvider
43023
43090
  );
43091
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43092
+ throw new Error(`Memory transaction was not confirmed (status=${receipt.status})`);
43093
+ }
43024
43094
  }
43025
43095
  async function decryptRow(row, key3) {
43026
43096
  const parsed = native.parseMemoryRow(gtvToFfiSafe(row));
@@ -43130,22 +43200,109 @@ async function rollbackMemory(toId, reason, auth, opts) {
43130
43200
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43131
43201
  const client = await buildChainClient(chainOpts);
43132
43202
  const { sigProvider } = buildSigner(auth);
43133
- await client.signAndSendUniqueTransaction(
43203
+ const receipt = await client.signAndSendUniqueTransaction(
43134
43204
  {
43135
43205
  name: native.OP_ROLLBACK_AGENT_MEMORY,
43136
43206
  args: [toGtxBytes(args.agent_pubkey), args.to_id, args.reason]
43137
43207
  },
43138
43208
  sigProvider
43139
43209
  );
43210
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43211
+ throw new Error(`Memory rollback was not confirmed (status=${receipt.status})`);
43212
+ }
43140
43213
  }
43141
43214
 
43142
43215
  // src-ts/memory/_json_safe.ts
43216
+ var import_node_util = require("util");
43217
+ var replacementKeys = [
43218
+ "newText",
43219
+ "new_string",
43220
+ "new_str",
43221
+ "newStr",
43222
+ "replacement"
43223
+ ];
43224
+ var argumentKeys = [
43225
+ "file_path",
43226
+ "path",
43227
+ "filename",
43228
+ "target",
43229
+ "file",
43230
+ "filePath",
43231
+ "content",
43232
+ ...replacementKeys,
43233
+ "value",
43234
+ "text"
43235
+ ];
43236
+ var maxPrototypeDepth = 32;
43237
+ var maxEditSlots = 65536;
43238
+ var maxTextUnits = 16 * 1024 * 1024;
43239
+ function invalid() {
43240
+ throw new TypeError("Unreadable or unsupported memory classifier input");
43241
+ }
43242
+ function dataProperty(value, key3) {
43243
+ let current = value;
43244
+ for (let depth = 0; current !== null; depth++) {
43245
+ if (depth >= maxPrototypeDepth || import_node_util.types.isProxy(current)) invalid();
43246
+ const descriptor = Object.getOwnPropertyDescriptor(current, key3);
43247
+ if (descriptor) {
43248
+ if (!("value" in descriptor)) invalid();
43249
+ return descriptor.value;
43250
+ }
43251
+ current = Object.getPrototypeOf(current);
43252
+ }
43253
+ return void 0;
43254
+ }
43143
43255
  function toJsonSafe(value) {
43144
- try {
43145
- return JSON.parse(JSON.stringify(value ?? null));
43146
- } catch {
43147
- return null;
43256
+ let textUnits = 0;
43257
+ function scalar(input) {
43258
+ if (input == null) return null;
43259
+ if (typeof input === "string") {
43260
+ textUnits += input.length;
43261
+ if (textUnits > maxTextUnits) invalid();
43262
+ return input;
43263
+ }
43264
+ if (typeof input === "boolean") return input;
43265
+ if (typeof input === "number" && Number.isFinite(input)) return input;
43266
+ return invalid();
43267
+ }
43268
+ function project(input, shape) {
43269
+ if (input == null || typeof input !== "object") return scalar(input);
43270
+ if (import_node_util.types.isProxy(input) || Array.isArray(input)) invalid();
43271
+ const output = /* @__PURE__ */ Object.create(null);
43272
+ const keys2 = shape === "envelope" ? ["toolName", "name"] : shape === "tool" ? ["name"] : shape === "args" ? argumentKeys : replacementKeys;
43273
+ for (const key3 of keys2) {
43274
+ const field = dataProperty(input, key3);
43275
+ if (field !== void 0) output[key3] = scalar(field);
43276
+ }
43277
+ if (shape === "envelope") {
43278
+ for (const key3 of ["params", "args", "arguments", "tool"]) {
43279
+ const field = dataProperty(input, key3);
43280
+ if (field !== void 0)
43281
+ output[key3] = project(field, key3 === "tool" ? "tool" : "args");
43282
+ }
43283
+ }
43284
+ if (shape === "args") {
43285
+ const edits = dataProperty(input, "edits");
43286
+ if (edits !== void 0) {
43287
+ if (edits !== null && typeof edits === "object" && import_node_util.types.isProxy(edits))
43288
+ invalid();
43289
+ if (Array.isArray(edits)) {
43290
+ const length = dataProperty(edits, "length");
43291
+ if (typeof length !== "number" || !Number.isInteger(length) || length < 0 || length > maxEditSlots)
43292
+ invalid();
43293
+ const entries = [];
43294
+ for (let index2 = 0; index2 < length; index2++) {
43295
+ entries.push(project(dataProperty(edits, String(index2)), "edit"));
43296
+ }
43297
+ output.edits = entries;
43298
+ } else {
43299
+ output.edits = scalar(edits);
43300
+ }
43301
+ }
43302
+ }
43303
+ return output;
43148
43304
  }
43305
+ return project(value, "envelope");
43149
43306
  }
43150
43307
 
43151
43308
  // src-ts/memory/classifier.ts
@@ -43191,6 +43348,7 @@ async function guardMemoryWrite(input) {
43191
43348
  endpoint,
43192
43349
  verifyPubKey,
43193
43350
  orgName,
43351
+ chainOpts,
43194
43352
  threshold,
43195
43353
  patterns,
43196
43354
  toolNames,
@@ -43198,7 +43356,22 @@ async function guardMemoryWrite(input) {
43198
43356
  debug: debug2 = false,
43199
43357
  logger: logger2
43200
43358
  } = input;
43201
- const memEntry = classifyMemoryWrite(event, ctx, { patterns, toolNames });
43359
+ let memEntry;
43360
+ try {
43361
+ memEntry = classifyMemoryWrite(event, ctx, { patterns, toolNames });
43362
+ } catch (err) {
43363
+ const why = err instanceof Error ? err.message : String(err);
43364
+ logger2?.warn?.("[atbash] memory classifier refused this tool call's input", { reason: why });
43365
+ if (!enforce) return { handled: false };
43366
+ return {
43367
+ handled: true,
43368
+ decision: {
43369
+ allow: false,
43370
+ block: true,
43371
+ reason: `Memory protection could not read this tool call (${why}); blocked because it could not be checked.`
43372
+ }
43373
+ };
43374
+ }
43202
43375
  if (debug2) emitDebugProbe(event, ctx, memEntry, logger2);
43203
43376
  if (!memEntry) return { handled: false };
43204
43377
  let scanResult;
@@ -43232,23 +43405,41 @@ async function guardMemoryWrite(input) {
43232
43405
  committed: false
43233
43406
  };
43234
43407
  }
43408
+ if (scanResult.verdict !== "green" && scanResult.verdict !== "yellow") {
43409
+ const reason = "Memory scan returned an unrecognized verdict.";
43410
+ return {
43411
+ handled: true,
43412
+ decision: enforce ? { allow: false, block: true, reason } : { allow: true, reason },
43413
+ scanResult,
43414
+ committed: false
43415
+ };
43416
+ }
43235
43417
  const filePath = import_node_path4.default.basename(memEntry.key);
43236
- commitMemoryVersion(memEntry.value, auth, {
43237
- score: scanResult.score,
43238
- filePath,
43239
- orgName,
43240
- endpoint,
43241
- verifyPubKey
43242
- }).catch((err) => {
43418
+ try {
43419
+ await commitMemoryVersion(memEntry.value, auth, {
43420
+ score: scanResult.score,
43421
+ filePath,
43422
+ orgName,
43423
+ endpoint,
43424
+ verifyPubKey,
43425
+ chainOpts
43426
+ });
43427
+ } catch (err) {
43243
43428
  const reason = err instanceof Error ? err.message : String(err);
43244
43429
  logger2?.warn?.("[atbash] memory commit to chain failed", {
43245
43430
  path: memEntry.key,
43246
43431
  filePath,
43247
43432
  reason
43248
43433
  });
43249
- });
43434
+ return {
43435
+ handled: true,
43436
+ decision: enforce ? { allow: false, block: true, reason: `Memory commit failed: ${reason}` } : { allow: true, reason: `Memory commit failed: ${reason}` },
43437
+ scanResult,
43438
+ committed: false
43439
+ };
43440
+ }
43250
43441
  logger2?.info?.(
43251
- scanResult.verdict === "yellow" ? "[atbash] memory HOLD" : "[atbash] memory ALLOW",
43442
+ scanResult.verdict === "yellow" ? "[atbash] memory ALLOW (yellow, low score)" : "[atbash] memory ALLOW",
43252
43443
  {
43253
43444
  path: memEntry.key,
43254
43445
  filePath,
@@ -43281,7 +43472,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
43281
43472
  if (withinTtl) {
43282
43473
  return { drifted: false, checked: false, pointer };
43283
43474
  }
43284
- const currentId = await getActiveMemoryId(auth, opts.chainOpts);
43475
+ const currentId = await getActiveMemoryId(auth, opts.chainOpts, opts.filePath);
43285
43476
  const nextPointer = { activeId: currentId, checkedAt: now };
43286
43477
  if (currentId === pointer.activeId) {
43287
43478
  return { drifted: false, checked: true, pointer: nextPointer };
@@ -43363,13 +43554,16 @@ function formatMeta(meta) {
43363
43554
  }
43364
43555
  function createFileLogger(filePath, upstream) {
43365
43556
  let queue = Promise.resolve();
43557
+ let writeError;
43366
43558
  async function ensureDir() {
43367
43559
  await import_node_fs5.promises.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
43368
43560
  }
43369
43561
  function append(level, message, meta) {
43370
43562
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
43371
43563
  `;
43372
- queue = queue.then(ensureDir).then(() => import_node_fs5.promises.appendFile(filePath, line, "utf8")).catch(() => {
43564
+ queue = queue.then(ensureDir).then(() => import_node_fs5.promises.appendFile(filePath, line, "utf8")).catch((error4) => {
43565
+ writeError = error4 instanceof Error ? error4 : new Error(String(error4));
43566
+ upstream?.warn("[atbash] plugin log write failed", { error: writeError.message });
43373
43567
  });
43374
43568
  }
43375
43569
  return {
@@ -43380,6 +43574,10 @@ function createFileLogger(filePath, upstream) {
43380
43574
  warn(message, meta) {
43381
43575
  upstream?.warn(message, meta ?? {});
43382
43576
  append("warn", message, meta);
43577
+ },
43578
+ async flush() {
43579
+ await queue;
43580
+ if (writeError) throw writeError;
43383
43581
  }
43384
43582
  };
43385
43583
  }
@@ -43469,7 +43667,15 @@ var MemoryGuardManager = class {
43469
43667
  orgName: this.opts.orgName
43470
43668
  });
43471
43669
  const resolved = await atbash.resolveChainForOrg(this.opts.orgName);
43472
- const chainOpts = { ...this.opts.chainOpts, network: resolved.network };
43670
+ if (!await atbash.checkAgentExists(this.agentPubkeyHex, { network: resolved.network })) {
43671
+ throw new Error(`agent is not registered on the resolved ${resolved.network} chain`);
43672
+ }
43673
+ const chainOpts = {
43674
+ ...this.opts.chainOpts,
43675
+ network: resolved.network,
43676
+ blockchainRid: resolved.blockchainRid,
43677
+ nodeUrls: this.opts.chainOpts?.nodeUrls ?? resolved.nodeUrls
43678
+ };
43473
43679
  this._resolvedChainOpts = chainOpts;
43474
43680
  this.logger.info(
43475
43681
  `[atbash] resolved org \u2192 chain \u2014 org=${this.opts.orgName} network=${resolved.network}`
@@ -43478,9 +43684,9 @@ var MemoryGuardManager = class {
43478
43684
  } catch (err) {
43479
43685
  const msg = err instanceof Error ? err.message : String(err);
43480
43686
  this.logger.warn(
43481
- `[atbash] org \u2192 chain resolution failed (falling back to SDK default): ${msg}`
43687
+ `[atbash] org \u2192 chain resolution failed (memory read will be blocked): ${msg}`
43482
43688
  );
43483
- return this.opts.chainOpts;
43689
+ throw err;
43484
43690
  } finally {
43485
43691
  this._resolveChainInflight = void 0;
43486
43692
  }
@@ -43499,13 +43705,18 @@ var MemoryGuardManager = class {
43499
43705
  const result = await syncLocalMemory(this.opts.auth, seed, {
43500
43706
  ttlMs: 0,
43501
43707
  force: true,
43502
- chainOpts
43708
+ chainOpts,
43709
+ filePath: import_node_path7.default.basename(this.memoryFilePath)
43503
43710
  });
43504
43711
  if (!result.drifted && result.pointer.activeId == null) {
43505
43712
  this.logger.info(
43506
43713
  `[atbash] no active memory on chain for agent=${this.agentPubkeyHex.slice(0, 16)}\u2026 org=${this.opts.orgName ?? "(none)"} \u2014 either the agent isn't registered on this chain or hasn't written any memory. Sync will remain a no-op until a write lands.`
43507
43714
  );
43508
43715
  } else if (result.drifted && result.current) {
43716
+ if (result.current.filePath !== import_node_path7.default.basename(this.memoryFilePath) || result.current.isActive !== true || result.current.decryptError) {
43717
+ this.logger.warn("[atbash] boot sync REFUSED mismatched memory version");
43718
+ return;
43719
+ }
43509
43720
  if (result.current.score < this.rollbackMinScore) {
43510
43721
  this.logger.warn(
43511
43722
  `[atbash] boot sync REFUSED refresh \u2014 id=${result.current.id} score=${result.current.score} below threshold ${this.rollbackMinScore}. Leaving MEMORY.md and pointer untouched; next memory read will be blocked.`
@@ -43519,7 +43730,8 @@ var MemoryGuardManager = class {
43519
43730
  await this.writeMemoryAtomic(result.current.content);
43520
43731
  } catch (err) {
43521
43732
  const msg = err instanceof Error ? err.message : String(err);
43522
- this.logger.warn("[atbash] boot sync write failed (serving whatever's on disk)", { error: msg });
43733
+ this.logger.warn("[atbash] boot sync write failed; pointer remains unchanged", { error: msg });
43734
+ return;
43523
43735
  }
43524
43736
  }
43525
43737
  await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
@@ -43533,16 +43745,30 @@ var MemoryGuardManager = class {
43533
43745
  }
43534
43746
  /**
43535
43747
  * Returns a `HookDecision` when the guard reached a decision about this event.
43536
- * Returns `null` when it did not — either the event isn't memory-related, or it
43537
- * is but the guard could not check it. In both cases the host falls through to
43538
- * its own audit.
43748
+ * Returns `null` for non-memory events, other files this manager does not
43749
+ * own, or unverified observe-mode reads. An enforced read of the managed
43750
+ * file blocks when verification cannot complete.
43539
43751
  *
43540
43752
  * A returned decision carries `audited` (see `HookDecision`). Only
43541
43753
  * `{ allow: true, audited: true }` means "checked and cleared"; anything else
43542
43754
  * that allows is a call the host still needs to judge.
43543
43755
  */
43544
43756
  async handleBeforeToolCall(event, ctx) {
43545
- if (classifyMemoryRead(event, ctx, this.opts.memoryReadClassifier)) {
43757
+ let isMemoryRead;
43758
+ try {
43759
+ isMemoryRead = classifyMemoryRead(event, ctx, this.opts.memoryReadClassifier);
43760
+ } catch (err) {
43761
+ const why = err instanceof Error ? err.message : String(err);
43762
+ this.logger.warn("[atbash] memory classifier refused this tool call's input", { reason: why });
43763
+ if (!this.enforce) return null;
43764
+ return {
43765
+ allow: false,
43766
+ block: true,
43767
+ blockReason: `Memory protection could not read this tool call (${why}); blocked because it could not be checked.`,
43768
+ audited: true
43769
+ };
43770
+ }
43771
+ if (isMemoryRead) {
43546
43772
  this.logger.info("[atbash] memory read intercepted \u2014 running sync check");
43547
43773
  return await this.handleMemoryRead(event, ctx);
43548
43774
  }
@@ -43557,6 +43783,7 @@ var MemoryGuardManager = class {
43557
43783
  endpoint: this.opts.judgeEndpoint,
43558
43784
  verifyPubKey: this.opts.judgeVerifyPubKey,
43559
43785
  orgName: this.opts.orgName,
43786
+ chainOpts: this.opts.chainOpts,
43560
43787
  patterns: this.opts.memoryPathPatterns,
43561
43788
  toolNames: this.opts.memoryWriteToolNames,
43562
43789
  enforce: this.enforce,
@@ -43601,6 +43828,14 @@ var MemoryGuardManager = class {
43601
43828
  reason: `memory scan returned ${sr2.verdict} but this guard is not enforcing it`
43602
43829
  };
43603
43830
  }
43831
+ if (guard.committed !== true) {
43832
+ return {
43833
+ allow: true,
43834
+ audited: false,
43835
+ verdict: sr2.verdict,
43836
+ reason: "memory scan passed but chain commit did not confirm"
43837
+ };
43838
+ }
43604
43839
  return { allow: true, audited: true, verdict: sr2.verdict };
43605
43840
  }
43606
43841
  /**
@@ -43635,62 +43870,73 @@ var MemoryGuardManager = class {
43635
43870
  return found.every((p) => import_node_path7.default.resolve(p) === managed);
43636
43871
  }
43637
43872
  async handleMemoryRead(event, ctx) {
43873
+ if (!this.vouchesForTarget(event, ctx)) return null;
43638
43874
  const pointer = await this.pointerStore.get(this.agentPubkeyHex);
43639
43875
  let result;
43876
+ let chainOpts;
43640
43877
  try {
43641
- const chainOpts = await this.resolveChainOpts();
43878
+ chainOpts = await this.resolveChainOpts();
43642
43879
  result = await syncLocalMemory(this.opts.auth, pointer, {
43643
43880
  ttlMs: this.ttlMs,
43644
- chainOpts
43881
+ force: this.enforce,
43882
+ chainOpts,
43883
+ filePath: import_node_path7.default.basename(this.memoryFilePath)
43645
43884
  });
43646
43885
  } catch (err) {
43647
43886
  if (err instanceof MemoryIntegrityError) {
43648
43887
  const reason = `Memory integrity check failed on id ${err.id} \u2014 read blocked.`;
43649
43888
  this.logger.warn("[atbash] MEMORY INTEGRITY FAILURE", { id: err.id, error: err.message });
43650
- if (!this.enforce) return null;
43651
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43889
+ return this.unverifiedRead(reason);
43652
43890
  }
43653
43891
  const msg = err instanceof Error ? err.message : String(err);
43654
- this.logger.warn("[atbash] memory sync failed (serving local copy)", { error: msg });
43655
- return null;
43892
+ this.logger.warn("[atbash] memory sync failed", { error: msg });
43893
+ return this.unverifiedRead(`Memory chain sync failed: ${msg}`);
43656
43894
  }
43657
- let onDiskIsCurrent = result.checked;
43658
- if (result.drifted) {
43659
- const fresh = result.current;
43660
- if (fresh) {
43661
- if (fresh.score < this.rollbackMinScore) {
43662
- const reason = `Rolled-back memory version #${fresh.id} scored ${fresh.score} (below threshold ${this.rollbackMinScore}) \u2014 read blocked.`;
43663
- this.logger.warn("[atbash] blocking read on low-score rollback", { id: fresh.id, score: fresh.score });
43664
- if (!this.enforce) return null;
43665
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43666
- }
43667
- this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", {
43668
- id: fresh.id,
43669
- score: fresh.score
43670
- });
43671
- try {
43672
- await this.writeMemoryAtomic(fresh.content);
43673
- } catch (err) {
43674
- const msg = err instanceof Error ? err.message : String(err);
43675
- this.logger.warn("[atbash] failed to write refreshed memory (serving old)", { error: msg });
43676
- onDiskIsCurrent = false;
43677
- }
43678
- } else {
43679
- this.logger.info("[atbash] active memory removed on chain", { pubkey: this.agentPubkeyHex });
43680
- onDiskIsCurrent = false;
43681
- }
43895
+ if (!result.checked) return this.unverifiedRead("Memory chain state was not checked.");
43896
+ const activeId = result.pointer.activeId;
43897
+ if (activeId === null) {
43898
+ return this.unverifiedRead("No active chain version exists for the managed memory file.");
43682
43899
  }
43683
- if (onDiskIsCurrent) {
43684
- await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43685
- } else {
43686
- this.logger.warn(
43687
- "[atbash] not advancing memory pointer \u2014 local file is stale or revoked; reads stay unaudited until it is refreshed"
43688
- );
43900
+ let active;
43901
+ try {
43902
+ active = result.drifted ? result.current : await getMemoryById(activeId, this.opts.auth, chainOpts);
43903
+ } catch (err) {
43904
+ const msg = err instanceof Error ? err.message : String(err);
43905
+ this.logger.warn("[atbash] active memory row could not be verified", { error: msg });
43906
+ return this.unverifiedRead(`Memory version could not be verified: ${msg}`);
43689
43907
  }
43690
- if (!onDiskIsCurrent) return null;
43691
- if (!this.vouchesForTarget(event, ctx)) return null;
43908
+ const filePath = import_node_path7.default.basename(this.memoryFilePath);
43909
+ if (!active || active.id !== activeId || active.filePath !== filePath || active.isActive !== true || active.decryptError) {
43910
+ return this.unverifiedRead("Active memory version does not match the managed file.");
43911
+ }
43912
+ if (active.score < this.rollbackMinScore) {
43913
+ return this.unverifiedRead(`Memory version #${active.id} is below the minimum safety score.`);
43914
+ }
43915
+ try {
43916
+ if (result.drifted) {
43917
+ this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", { id: active.id });
43918
+ await this.writeMemoryAtomic(active.content);
43919
+ }
43920
+ const onDisk = await import_node_fs6.promises.readFile(this.memoryFilePath, "utf8");
43921
+ if (onDisk !== active.content) {
43922
+ return this.unverifiedRead("Local memory bytes do not match the active chain version.");
43923
+ }
43924
+ } catch (err) {
43925
+ const msg = err instanceof Error ? err.message : String(err);
43926
+ this.logger.warn("[atbash] managed memory file could not be verified", { error: msg });
43927
+ return this.unverifiedRead(`Managed memory file could not be verified: ${msg}`);
43928
+ }
43929
+ await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43692
43930
  return { allow: true, audited: true };
43693
43931
  }
43932
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
43933
+ async flushLogs() {
43934
+ await this.logger.flush();
43935
+ }
43936
+ unverifiedRead(reason) {
43937
+ if (!this.enforce) return null;
43938
+ return { block: true, blockReason: reason, allow: false, reason, audited: true };
43939
+ }
43694
43940
  async writeMemoryAtomic(content) {
43695
43941
  await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(this.memoryFilePath), { recursive: true });
43696
43942
  const tmp = `${this.memoryFilePath}.${process.pid}.tmp`;