@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.mjs CHANGED
@@ -2930,6 +2930,13 @@ function buildAllowedJudgeHosts() {
2930
2930
  ]);
2931
2931
  }
2932
2932
  var ALLOWED_JUDGE_HOSTS = buildAllowedJudgeHosts();
2933
+ function isAllowlistedJudgeHost(endpoint) {
2934
+ try {
2935
+ return ALLOWED_JUDGE_HOSTS.has(new URL(endpoint).hostname.toLowerCase());
2936
+ } catch {
2937
+ return false;
2938
+ }
2939
+ }
2933
2940
  function validateJudgeEndpoint(judge) {
2934
2941
  const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
2935
2942
  const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
@@ -2977,11 +2984,23 @@ var AtbashAPIError = class extends Error {
2977
2984
  status;
2978
2985
  /** Raw response body text (may be empty). */
2979
2986
  body;
2980
- constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT) {
2981
- super(enrich(status, body, statusText, endpoint));
2987
+ /**
2988
+ * Request path this status came from, e.g. `/api/v1/judge` — absent when the
2989
+ * request never reached a response (transport failures carry status 0).
2990
+ *
2991
+ * A status alone does not say what failed. Several routes answer one call, and
2992
+ * more than one of them can 404: a caller reading only `404` cannot tell "this
2993
+ * agent is not registered" from "the chain-routing lookup did not resolve", and
2994
+ * has to guess from the body's prose — which a 404 raised before any handler
2995
+ * ran does not carry. The path is the fact that settles it.
2996
+ */
2997
+ path;
2998
+ constructor(status, body, statusText = "", endpoint = DEFAULT_ENDPOINT, path7) {
2999
+ super(enrich(status, body, statusText, endpoint, path7));
2982
3000
  this.name = "AtbashAPIError";
2983
3001
  this.status = status;
2984
3002
  this.body = body;
3003
+ this.path = path7;
2985
3004
  }
2986
3005
  };
2987
3006
  var SignatureVerificationError = class extends Error {
@@ -2990,9 +3009,9 @@ var SignatureVerificationError = class extends Error {
2990
3009
  this.name = "SignatureVerificationError";
2991
3010
  }
2992
3011
  };
2993
- function enrich(status, body, statusText, endpoint) {
3012
+ function enrich(status, body, statusText, endpoint, path7) {
2994
3013
  const dashboard = endpoint.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
2995
- let msg = `API error ${status}: ${body || statusText}`;
3014
+ let msg = `API error ${status}${path7 ? ` from ${path7}` : ""}: ${body || statusText}`;
2996
3015
  const lowered = body.toLowerCase();
2997
3016
  if (lowered.includes("agent not registered")) {
2998
3017
  msg += `
@@ -3082,6 +3101,13 @@ function classifyTransportError(err, url, method, timeoutMs) {
3082
3101
  { cause: err }
3083
3102
  );
3084
3103
  }
3104
+ if (code2 === "UND_ERR_CONNECT_TIMEOUT") {
3105
+ return new HttpTransportError(
3106
+ "timeout",
3107
+ `${method} ${url} could not establish a connection before the transport deadline \u2014 check the endpoint and network`,
3108
+ { cause: err }
3109
+ );
3110
+ }
3085
3111
  if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") {
3086
3112
  return new HttpTransportError(
3087
3113
  "dns",
@@ -3239,7 +3265,10 @@ function canonicalAllow(data) {
3239
3265
  import { readFileSync as readFileSync2 } from "fs";
3240
3266
  import { homedir as homedir2 } from "os";
3241
3267
  import { join as join2 } from "path";
3242
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
3268
+ import {
3269
+ AggregationTemporalityPreference,
3270
+ OTLPMetricExporter
3271
+ } from "@opentelemetry/exporter-metrics-otlp-http";
3243
3272
  import { resourceFromAttributes } from "@opentelemetry/resources";
3244
3273
  import {
3245
3274
  MeterProvider,
@@ -3249,6 +3278,8 @@ var meterProvider = null;
3249
3278
  var callCounter = null;
3250
3279
  var durationHistogram = null;
3251
3280
  var defaultSource = "sdk";
3281
+ var sourceExplicitlySet = false;
3282
+ var requestedExportIntervalMs;
3252
3283
  function isTelemetryOptedOut() {
3253
3284
  const disabled = process.env.ATBASH_TELEMETRY_DISABLED?.trim().toLowerCase();
3254
3285
  if (disabled && ["1", "true", "yes", "on"].includes(disabled)) {
@@ -3272,20 +3303,41 @@ function autoInit() {
3272
3303
  }
3273
3304
  function setupTelemetry(config2) {
3274
3305
  if (!config2.enabled) return;
3275
- if (meterProvider) return;
3276
3306
  if (isTelemetryOptedOut()) return;
3307
+ if (config2.source && !sourceExplicitlySet) {
3308
+ defaultSource = config2.source;
3309
+ sourceExplicitlySet = true;
3310
+ }
3311
+ if (config2.exportIntervalMs !== void 0 && requestedExportIntervalMs === void 0) {
3312
+ requestedExportIntervalMs = config2.exportIntervalMs;
3313
+ }
3314
+ if (meterProvider) return;
3277
3315
  if (!config2.endpoint || !config2.getAuthHeaders) return;
3278
- if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(config2.endpoint)) return;
3279
- defaultSource = config2.source ?? "sdk";
3316
+ if (/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(
3317
+ config2.endpoint
3318
+ ))
3319
+ return;
3280
3320
  const proxyUrl = `${config2.endpoint.replace(/\/+$/, "")}/api/telemetry`;
3281
3321
  const getAuthHeaders = config2.getAuthHeaders;
3282
3322
  const exporter = new OTLPMetricExporter({
3283
3323
  url: proxyUrl,
3284
- headers: async () => getAuthHeaders()
3324
+ headers: async () => getAuthHeaders(),
3325
+ // DELTA, not the OTel default of CUMULATIVE.
3326
+ //
3327
+ // A cumulative counter reports a running total, and the backend recovers
3328
+ // the increment by differencing consecutive points of a series. That needs
3329
+ // at least two points. Most Atbash callers are short-lived — a CLI run
3330
+ // exports once and exits — so their series has exactly one point, nothing
3331
+ // to difference against, and the usage board reads them as zero: the calls
3332
+ // arrive, carry the right `source`, and still count for nothing.
3333
+ //
3334
+ // A delta counter reports the increment itself, so a single export is a
3335
+ // whole truthful datapoint and SUM over any window is the real call count.
3336
+ temporalityPreference: AggregationTemporalityPreference.DELTA
3285
3337
  });
3286
3338
  const reader = new PeriodicExportingMetricReader({
3287
3339
  exporter,
3288
- exportIntervalMillis: config2.exportIntervalMs ?? 6e4
3340
+ exportIntervalMillis: requestedExportIntervalMs ?? 6e4
3289
3341
  });
3290
3342
  meterProvider = new MeterProvider({
3291
3343
  resource: resourceFromAttributes({
@@ -3329,6 +3381,9 @@ async function shutdownTelemetry() {
3329
3381
  meterProvider = null;
3330
3382
  callCounter = null;
3331
3383
  durationHistogram = null;
3384
+ defaultSource = "sdk";
3385
+ sourceExplicitlySet = false;
3386
+ requestedExportIntervalMs = void 0;
3332
3387
  }
3333
3388
 
3334
3389
  // src-ts/userConfig.ts
@@ -3522,17 +3577,23 @@ var Atbash = class _Atbash {
3522
3577
  verifying: this.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
3523
3578
  });
3524
3579
  }
3525
- try {
3526
- setupTelemetry({
3527
- enabled: true,
3528
- source: "sdk",
3529
- endpoint: this.endpoint,
3530
- getAuthHeaders: () => this.authHeaders()
3531
- });
3532
- } catch (err) {
3533
- this.logger.warn?.(
3534
- "[atbash] telemetry setup failed \u2014 continuing without metrics",
3535
- { error: String(err) }
3580
+ if (isAllowlistedJudgeHost(this.endpoint)) {
3581
+ try {
3582
+ setupTelemetry({
3583
+ enabled: true,
3584
+ endpoint: this.endpoint,
3585
+ getAuthHeaders: () => this.authHeaders()
3586
+ });
3587
+ } catch (err) {
3588
+ this.logger.warn?.(
3589
+ "[atbash] telemetry setup failed \u2014 continuing without metrics",
3590
+ { error: String(err) }
3591
+ );
3592
+ }
3593
+ } else {
3594
+ this.logger.info?.(
3595
+ "[atbash] telemetry disabled \u2014 the agent bearer is only sent to hosts that already receive it, and this judge endpoint is not one",
3596
+ { endpoint: this.endpoint }
3536
3597
  );
3537
3598
  }
3538
3599
  }
@@ -4413,7 +4474,8 @@ var Atbash = class _Atbash {
4413
4474
  resp.status,
4414
4475
  await safeText(resp),
4415
4476
  resp.statusText,
4416
- this.endpoint
4477
+ this.endpoint,
4478
+ requestPath(resp.url)
4417
4479
  );
4418
4480
  }
4419
4481
  /**
@@ -4555,6 +4617,14 @@ function parseJson(bytes) {
4555
4617
  const parsed = tryParseJson(bytes.toString("utf-8"));
4556
4618
  return parsed === void 0 ? {} : parsed;
4557
4619
  }
4620
+ function requestPath(url) {
4621
+ if (!url) return void 0;
4622
+ try {
4623
+ return new URL(url).pathname || void 0;
4624
+ } catch {
4625
+ return void 0;
4626
+ }
4627
+ }
4558
4628
  async function safeText(resp) {
4559
4629
  try {
4560
4630
  return await resp.text();
@@ -35212,9 +35282,9 @@ var ZodUnion = class extends ZodType {
35212
35282
  }
35213
35283
  };
35214
35284
  types.ZodUnion = ZodUnion;
35215
- ZodUnion.create = (types2, params) => {
35285
+ ZodUnion.create = (types3, params) => {
35216
35286
  return new ZodUnion({
35217
- options: types2,
35287
+ options: types3,
35218
35288
  typeName: ZodFirstPartyTypeKind.ZodUnion,
35219
35289
  ...processCreateParams(params)
35220
35290
  });
@@ -38671,9 +38741,9 @@ _nodeUtil.exports;
38671
38741
  var freeProcess = moduleExports && freeGlobal2.process;
38672
38742
  var nodeUtil2 = (function() {
38673
38743
  try {
38674
- var types2 = freeModule && freeModule.require && freeModule.require("util").types;
38675
- if (types2) {
38676
- return types2;
38744
+ var types3 = freeModule && freeModule.require && freeModule.require("util").types;
38745
+ if (types3) {
38746
+ return types3;
38677
38747
  }
38678
38748
  return freeProcess && freeProcess.binding && freeProcess.binding("util");
38679
38749
  } catch (e) {
@@ -42921,7 +42991,7 @@ async function commitMemoryVersion(plaintext, auth, opts) {
42921
42991
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
42922
42992
  const client = await buildChainClient(chainOpts);
42923
42993
  const { sigProvider } = buildSigner(auth);
42924
- await client.signAndSendUniqueTransaction(
42994
+ const receipt = await client.signAndSendUniqueTransaction(
42925
42995
  {
42926
42996
  name: native.OP_ADD_AGENT_MEMORY,
42927
42997
  args: [
@@ -42934,6 +43004,9 @@ async function commitMemoryVersion(plaintext, auth, opts) {
42934
43004
  },
42935
43005
  sigProvider
42936
43006
  );
43007
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43008
+ throw new Error(`Memory transaction was not confirmed (status=${receipt.status})`);
43009
+ }
42937
43010
  }
42938
43011
  async function decryptRow(row, key3) {
42939
43012
  const parsed = native.parseMemoryRow(gtvToFfiSafe(row));
@@ -43043,22 +43116,109 @@ async function rollbackMemory(toId, reason, auth, opts) {
43043
43116
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43044
43117
  const client = await buildChainClient(chainOpts);
43045
43118
  const { sigProvider } = buildSigner(auth);
43046
- await client.signAndSendUniqueTransaction(
43119
+ const receipt = await client.signAndSendUniqueTransaction(
43047
43120
  {
43048
43121
  name: native.OP_ROLLBACK_AGENT_MEMORY,
43049
43122
  args: [toGtxBytes(args.agent_pubkey), args.to_id, args.reason]
43050
43123
  },
43051
43124
  sigProvider
43052
43125
  );
43126
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43127
+ throw new Error(`Memory rollback was not confirmed (status=${receipt.status})`);
43128
+ }
43053
43129
  }
43054
43130
 
43055
43131
  // src-ts/memory/_json_safe.ts
43132
+ import { types as types2 } from "util";
43133
+ var replacementKeys = [
43134
+ "newText",
43135
+ "new_string",
43136
+ "new_str",
43137
+ "newStr",
43138
+ "replacement"
43139
+ ];
43140
+ var argumentKeys = [
43141
+ "file_path",
43142
+ "path",
43143
+ "filename",
43144
+ "target",
43145
+ "file",
43146
+ "filePath",
43147
+ "content",
43148
+ ...replacementKeys,
43149
+ "value",
43150
+ "text"
43151
+ ];
43152
+ var maxPrototypeDepth = 32;
43153
+ var maxEditSlots = 65536;
43154
+ var maxTextUnits = 16 * 1024 * 1024;
43155
+ function invalid() {
43156
+ throw new TypeError("Unreadable or unsupported memory classifier input");
43157
+ }
43158
+ function dataProperty(value, key3) {
43159
+ let current = value;
43160
+ for (let depth = 0; current !== null; depth++) {
43161
+ if (depth >= maxPrototypeDepth || types2.isProxy(current)) invalid();
43162
+ const descriptor = Object.getOwnPropertyDescriptor(current, key3);
43163
+ if (descriptor) {
43164
+ if (!("value" in descriptor)) invalid();
43165
+ return descriptor.value;
43166
+ }
43167
+ current = Object.getPrototypeOf(current);
43168
+ }
43169
+ return void 0;
43170
+ }
43056
43171
  function toJsonSafe(value) {
43057
- try {
43058
- return JSON.parse(JSON.stringify(value ?? null));
43059
- } catch {
43060
- return null;
43172
+ let textUnits = 0;
43173
+ function scalar(input) {
43174
+ if (input == null) return null;
43175
+ if (typeof input === "string") {
43176
+ textUnits += input.length;
43177
+ if (textUnits > maxTextUnits) invalid();
43178
+ return input;
43179
+ }
43180
+ if (typeof input === "boolean") return input;
43181
+ if (typeof input === "number" && Number.isFinite(input)) return input;
43182
+ return invalid();
43183
+ }
43184
+ function project(input, shape) {
43185
+ if (input == null || typeof input !== "object") return scalar(input);
43186
+ if (types2.isProxy(input) || Array.isArray(input)) invalid();
43187
+ const output = /* @__PURE__ */ Object.create(null);
43188
+ const keys2 = shape === "envelope" ? ["toolName", "name"] : shape === "tool" ? ["name"] : shape === "args" ? argumentKeys : replacementKeys;
43189
+ for (const key3 of keys2) {
43190
+ const field = dataProperty(input, key3);
43191
+ if (field !== void 0) output[key3] = scalar(field);
43192
+ }
43193
+ if (shape === "envelope") {
43194
+ for (const key3 of ["params", "args", "arguments", "tool"]) {
43195
+ const field = dataProperty(input, key3);
43196
+ if (field !== void 0)
43197
+ output[key3] = project(field, key3 === "tool" ? "tool" : "args");
43198
+ }
43199
+ }
43200
+ if (shape === "args") {
43201
+ const edits = dataProperty(input, "edits");
43202
+ if (edits !== void 0) {
43203
+ if (edits !== null && typeof edits === "object" && types2.isProxy(edits))
43204
+ invalid();
43205
+ if (Array.isArray(edits)) {
43206
+ const length = dataProperty(edits, "length");
43207
+ if (typeof length !== "number" || !Number.isInteger(length) || length < 0 || length > maxEditSlots)
43208
+ invalid();
43209
+ const entries = [];
43210
+ for (let index2 = 0; index2 < length; index2++) {
43211
+ entries.push(project(dataProperty(edits, String(index2)), "edit"));
43212
+ }
43213
+ output.edits = entries;
43214
+ } else {
43215
+ output.edits = scalar(edits);
43216
+ }
43217
+ }
43218
+ }
43219
+ return output;
43061
43220
  }
43221
+ return project(value, "envelope");
43062
43222
  }
43063
43223
 
43064
43224
  // src-ts/memory/classifier.ts
@@ -43104,6 +43264,7 @@ async function guardMemoryWrite(input) {
43104
43264
  endpoint,
43105
43265
  verifyPubKey,
43106
43266
  orgName,
43267
+ chainOpts,
43107
43268
  threshold,
43108
43269
  patterns,
43109
43270
  toolNames,
@@ -43111,7 +43272,22 @@ async function guardMemoryWrite(input) {
43111
43272
  debug: debug2 = false,
43112
43273
  logger: logger2
43113
43274
  } = input;
43114
- const memEntry = classifyMemoryWrite(event, ctx, { patterns, toolNames });
43275
+ let memEntry;
43276
+ try {
43277
+ memEntry = classifyMemoryWrite(event, ctx, { patterns, toolNames });
43278
+ } catch (err) {
43279
+ const why = err instanceof Error ? err.message : String(err);
43280
+ logger2?.warn?.("[atbash] memory classifier refused this tool call's input", { reason: why });
43281
+ if (!enforce) return { handled: false };
43282
+ return {
43283
+ handled: true,
43284
+ decision: {
43285
+ allow: false,
43286
+ block: true,
43287
+ reason: `Memory protection could not read this tool call (${why}); blocked because it could not be checked.`
43288
+ }
43289
+ };
43290
+ }
43115
43291
  if (debug2) emitDebugProbe(event, ctx, memEntry, logger2);
43116
43292
  if (!memEntry) return { handled: false };
43117
43293
  let scanResult;
@@ -43145,23 +43321,41 @@ async function guardMemoryWrite(input) {
43145
43321
  committed: false
43146
43322
  };
43147
43323
  }
43324
+ if (scanResult.verdict !== "green" && scanResult.verdict !== "yellow") {
43325
+ const reason = "Memory scan returned an unrecognized verdict.";
43326
+ return {
43327
+ handled: true,
43328
+ decision: enforce ? { allow: false, block: true, reason } : { allow: true, reason },
43329
+ scanResult,
43330
+ committed: false
43331
+ };
43332
+ }
43148
43333
  const filePath = path3.basename(memEntry.key);
43149
- commitMemoryVersion(memEntry.value, auth, {
43150
- score: scanResult.score,
43151
- filePath,
43152
- orgName,
43153
- endpoint,
43154
- verifyPubKey
43155
- }).catch((err) => {
43334
+ try {
43335
+ await commitMemoryVersion(memEntry.value, auth, {
43336
+ score: scanResult.score,
43337
+ filePath,
43338
+ orgName,
43339
+ endpoint,
43340
+ verifyPubKey,
43341
+ chainOpts
43342
+ });
43343
+ } catch (err) {
43156
43344
  const reason = err instanceof Error ? err.message : String(err);
43157
43345
  logger2?.warn?.("[atbash] memory commit to chain failed", {
43158
43346
  path: memEntry.key,
43159
43347
  filePath,
43160
43348
  reason
43161
43349
  });
43162
- });
43350
+ return {
43351
+ handled: true,
43352
+ decision: enforce ? { allow: false, block: true, reason: `Memory commit failed: ${reason}` } : { allow: true, reason: `Memory commit failed: ${reason}` },
43353
+ scanResult,
43354
+ committed: false
43355
+ };
43356
+ }
43163
43357
  logger2?.info?.(
43164
- scanResult.verdict === "yellow" ? "[atbash] memory HOLD" : "[atbash] memory ALLOW",
43358
+ scanResult.verdict === "yellow" ? "[atbash] memory ALLOW (yellow, low score)" : "[atbash] memory ALLOW",
43165
43359
  {
43166
43360
  path: memEntry.key,
43167
43361
  filePath,
@@ -43194,7 +43388,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
43194
43388
  if (withinTtl) {
43195
43389
  return { drifted: false, checked: false, pointer };
43196
43390
  }
43197
- const currentId = await getActiveMemoryId(auth, opts.chainOpts);
43391
+ const currentId = await getActiveMemoryId(auth, opts.chainOpts, opts.filePath);
43198
43392
  const nextPointer = { activeId: currentId, checkedAt: now };
43199
43393
  if (currentId === pointer.activeId) {
43200
43394
  return { drifted: false, checked: true, pointer: nextPointer };
@@ -43276,13 +43470,16 @@ function formatMeta(meta) {
43276
43470
  }
43277
43471
  function createFileLogger(filePath, upstream) {
43278
43472
  let queue = Promise.resolve();
43473
+ let writeError;
43279
43474
  async function ensureDir() {
43280
43475
  await fs2.mkdir(path5.dirname(filePath), { recursive: true });
43281
43476
  }
43282
43477
  function append(level, message, meta) {
43283
43478
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
43284
43479
  `;
43285
- queue = queue.then(ensureDir).then(() => fs2.appendFile(filePath, line, "utf8")).catch(() => {
43480
+ queue = queue.then(ensureDir).then(() => fs2.appendFile(filePath, line, "utf8")).catch((error4) => {
43481
+ writeError = error4 instanceof Error ? error4 : new Error(String(error4));
43482
+ upstream?.warn("[atbash] plugin log write failed", { error: writeError.message });
43286
43483
  });
43287
43484
  }
43288
43485
  return {
@@ -43293,6 +43490,10 @@ function createFileLogger(filePath, upstream) {
43293
43490
  warn(message, meta) {
43294
43491
  upstream?.warn(message, meta ?? {});
43295
43492
  append("warn", message, meta);
43493
+ },
43494
+ async flush() {
43495
+ await queue;
43496
+ if (writeError) throw writeError;
43296
43497
  }
43297
43498
  };
43298
43499
  }
@@ -43382,7 +43583,15 @@ var MemoryGuardManager = class {
43382
43583
  orgName: this.opts.orgName
43383
43584
  });
43384
43585
  const resolved = await atbash.resolveChainForOrg(this.opts.orgName);
43385
- const chainOpts = { ...this.opts.chainOpts, network: resolved.network };
43586
+ if (!await atbash.checkAgentExists(this.agentPubkeyHex, { network: resolved.network })) {
43587
+ throw new Error(`agent is not registered on the resolved ${resolved.network} chain`);
43588
+ }
43589
+ const chainOpts = {
43590
+ ...this.opts.chainOpts,
43591
+ network: resolved.network,
43592
+ blockchainRid: resolved.blockchainRid,
43593
+ nodeUrls: this.opts.chainOpts?.nodeUrls ?? resolved.nodeUrls
43594
+ };
43386
43595
  this._resolvedChainOpts = chainOpts;
43387
43596
  this.logger.info(
43388
43597
  `[atbash] resolved org \u2192 chain \u2014 org=${this.opts.orgName} network=${resolved.network}`
@@ -43391,9 +43600,9 @@ var MemoryGuardManager = class {
43391
43600
  } catch (err) {
43392
43601
  const msg = err instanceof Error ? err.message : String(err);
43393
43602
  this.logger.warn(
43394
- `[atbash] org \u2192 chain resolution failed (falling back to SDK default): ${msg}`
43603
+ `[atbash] org \u2192 chain resolution failed (memory read will be blocked): ${msg}`
43395
43604
  );
43396
- return this.opts.chainOpts;
43605
+ throw err;
43397
43606
  } finally {
43398
43607
  this._resolveChainInflight = void 0;
43399
43608
  }
@@ -43412,13 +43621,18 @@ var MemoryGuardManager = class {
43412
43621
  const result = await syncLocalMemory(this.opts.auth, seed, {
43413
43622
  ttlMs: 0,
43414
43623
  force: true,
43415
- chainOpts
43624
+ chainOpts,
43625
+ filePath: path6.basename(this.memoryFilePath)
43416
43626
  });
43417
43627
  if (!result.drifted && result.pointer.activeId == null) {
43418
43628
  this.logger.info(
43419
43629
  `[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.`
43420
43630
  );
43421
43631
  } else if (result.drifted && result.current) {
43632
+ if (result.current.filePath !== path6.basename(this.memoryFilePath) || result.current.isActive !== true || result.current.decryptError) {
43633
+ this.logger.warn("[atbash] boot sync REFUSED mismatched memory version");
43634
+ return;
43635
+ }
43422
43636
  if (result.current.score < this.rollbackMinScore) {
43423
43637
  this.logger.warn(
43424
43638
  `[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.`
@@ -43432,7 +43646,8 @@ var MemoryGuardManager = class {
43432
43646
  await this.writeMemoryAtomic(result.current.content);
43433
43647
  } catch (err) {
43434
43648
  const msg = err instanceof Error ? err.message : String(err);
43435
- this.logger.warn("[atbash] boot sync write failed (serving whatever's on disk)", { error: msg });
43649
+ this.logger.warn("[atbash] boot sync write failed; pointer remains unchanged", { error: msg });
43650
+ return;
43436
43651
  }
43437
43652
  }
43438
43653
  await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
@@ -43446,16 +43661,30 @@ var MemoryGuardManager = class {
43446
43661
  }
43447
43662
  /**
43448
43663
  * Returns a `HookDecision` when the guard reached a decision about this event.
43449
- * Returns `null` when it did not — either the event isn't memory-related, or it
43450
- * is but the guard could not check it. In both cases the host falls through to
43451
- * its own audit.
43664
+ * Returns `null` for non-memory events, other files this manager does not
43665
+ * own, or unverified observe-mode reads. An enforced read of the managed
43666
+ * file blocks when verification cannot complete.
43452
43667
  *
43453
43668
  * A returned decision carries `audited` (see `HookDecision`). Only
43454
43669
  * `{ allow: true, audited: true }` means "checked and cleared"; anything else
43455
43670
  * that allows is a call the host still needs to judge.
43456
43671
  */
43457
43672
  async handleBeforeToolCall(event, ctx) {
43458
- if (classifyMemoryRead(event, ctx, this.opts.memoryReadClassifier)) {
43673
+ let isMemoryRead;
43674
+ try {
43675
+ isMemoryRead = classifyMemoryRead(event, ctx, this.opts.memoryReadClassifier);
43676
+ } catch (err) {
43677
+ const why = err instanceof Error ? err.message : String(err);
43678
+ this.logger.warn("[atbash] memory classifier refused this tool call's input", { reason: why });
43679
+ if (!this.enforce) return null;
43680
+ return {
43681
+ allow: false,
43682
+ block: true,
43683
+ blockReason: `Memory protection could not read this tool call (${why}); blocked because it could not be checked.`,
43684
+ audited: true
43685
+ };
43686
+ }
43687
+ if (isMemoryRead) {
43459
43688
  this.logger.info("[atbash] memory read intercepted \u2014 running sync check");
43460
43689
  return await this.handleMemoryRead(event, ctx);
43461
43690
  }
@@ -43470,6 +43699,7 @@ var MemoryGuardManager = class {
43470
43699
  endpoint: this.opts.judgeEndpoint,
43471
43700
  verifyPubKey: this.opts.judgeVerifyPubKey,
43472
43701
  orgName: this.opts.orgName,
43702
+ chainOpts: this.opts.chainOpts,
43473
43703
  patterns: this.opts.memoryPathPatterns,
43474
43704
  toolNames: this.opts.memoryWriteToolNames,
43475
43705
  enforce: this.enforce,
@@ -43514,6 +43744,14 @@ var MemoryGuardManager = class {
43514
43744
  reason: `memory scan returned ${sr2.verdict} but this guard is not enforcing it`
43515
43745
  };
43516
43746
  }
43747
+ if (guard.committed !== true) {
43748
+ return {
43749
+ allow: true,
43750
+ audited: false,
43751
+ verdict: sr2.verdict,
43752
+ reason: "memory scan passed but chain commit did not confirm"
43753
+ };
43754
+ }
43517
43755
  return { allow: true, audited: true, verdict: sr2.verdict };
43518
43756
  }
43519
43757
  /**
@@ -43548,62 +43786,73 @@ var MemoryGuardManager = class {
43548
43786
  return found.every((p) => path6.resolve(p) === managed);
43549
43787
  }
43550
43788
  async handleMemoryRead(event, ctx) {
43789
+ if (!this.vouchesForTarget(event, ctx)) return null;
43551
43790
  const pointer = await this.pointerStore.get(this.agentPubkeyHex);
43552
43791
  let result;
43792
+ let chainOpts;
43553
43793
  try {
43554
- const chainOpts = await this.resolveChainOpts();
43794
+ chainOpts = await this.resolveChainOpts();
43555
43795
  result = await syncLocalMemory(this.opts.auth, pointer, {
43556
43796
  ttlMs: this.ttlMs,
43557
- chainOpts
43797
+ force: this.enforce,
43798
+ chainOpts,
43799
+ filePath: path6.basename(this.memoryFilePath)
43558
43800
  });
43559
43801
  } catch (err) {
43560
43802
  if (err instanceof MemoryIntegrityError) {
43561
43803
  const reason = `Memory integrity check failed on id ${err.id} \u2014 read blocked.`;
43562
43804
  this.logger.warn("[atbash] MEMORY INTEGRITY FAILURE", { id: err.id, error: err.message });
43563
- if (!this.enforce) return null;
43564
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43805
+ return this.unverifiedRead(reason);
43565
43806
  }
43566
43807
  const msg = err instanceof Error ? err.message : String(err);
43567
- this.logger.warn("[atbash] memory sync failed (serving local copy)", { error: msg });
43568
- return null;
43808
+ this.logger.warn("[atbash] memory sync failed", { error: msg });
43809
+ return this.unverifiedRead(`Memory chain sync failed: ${msg}`);
43569
43810
  }
43570
- let onDiskIsCurrent = result.checked;
43571
- if (result.drifted) {
43572
- const fresh = result.current;
43573
- if (fresh) {
43574
- if (fresh.score < this.rollbackMinScore) {
43575
- const reason = `Rolled-back memory version #${fresh.id} scored ${fresh.score} (below threshold ${this.rollbackMinScore}) \u2014 read blocked.`;
43576
- this.logger.warn("[atbash] blocking read on low-score rollback", { id: fresh.id, score: fresh.score });
43577
- if (!this.enforce) return null;
43578
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43579
- }
43580
- this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", {
43581
- id: fresh.id,
43582
- score: fresh.score
43583
- });
43584
- try {
43585
- await this.writeMemoryAtomic(fresh.content);
43586
- } catch (err) {
43587
- const msg = err instanceof Error ? err.message : String(err);
43588
- this.logger.warn("[atbash] failed to write refreshed memory (serving old)", { error: msg });
43589
- onDiskIsCurrent = false;
43590
- }
43591
- } else {
43592
- this.logger.info("[atbash] active memory removed on chain", { pubkey: this.agentPubkeyHex });
43593
- onDiskIsCurrent = false;
43594
- }
43811
+ if (!result.checked) return this.unverifiedRead("Memory chain state was not checked.");
43812
+ const activeId = result.pointer.activeId;
43813
+ if (activeId === null) {
43814
+ return this.unverifiedRead("No active chain version exists for the managed memory file.");
43595
43815
  }
43596
- if (onDiskIsCurrent) {
43597
- await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43598
- } else {
43599
- this.logger.warn(
43600
- "[atbash] not advancing memory pointer \u2014 local file is stale or revoked; reads stay unaudited until it is refreshed"
43601
- );
43816
+ let active;
43817
+ try {
43818
+ active = result.drifted ? result.current : await getMemoryById(activeId, this.opts.auth, chainOpts);
43819
+ } catch (err) {
43820
+ const msg = err instanceof Error ? err.message : String(err);
43821
+ this.logger.warn("[atbash] active memory row could not be verified", { error: msg });
43822
+ return this.unverifiedRead(`Memory version could not be verified: ${msg}`);
43602
43823
  }
43603
- if (!onDiskIsCurrent) return null;
43604
- if (!this.vouchesForTarget(event, ctx)) return null;
43824
+ const filePath = path6.basename(this.memoryFilePath);
43825
+ if (!active || active.id !== activeId || active.filePath !== filePath || active.isActive !== true || active.decryptError) {
43826
+ return this.unverifiedRead("Active memory version does not match the managed file.");
43827
+ }
43828
+ if (active.score < this.rollbackMinScore) {
43829
+ return this.unverifiedRead(`Memory version #${active.id} is below the minimum safety score.`);
43830
+ }
43831
+ try {
43832
+ if (result.drifted) {
43833
+ this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", { id: active.id });
43834
+ await this.writeMemoryAtomic(active.content);
43835
+ }
43836
+ const onDisk = await fs3.readFile(this.memoryFilePath, "utf8");
43837
+ if (onDisk !== active.content) {
43838
+ return this.unverifiedRead("Local memory bytes do not match the active chain version.");
43839
+ }
43840
+ } catch (err) {
43841
+ const msg = err instanceof Error ? err.message : String(err);
43842
+ this.logger.warn("[atbash] managed memory file could not be verified", { error: msg });
43843
+ return this.unverifiedRead(`Managed memory file could not be verified: ${msg}`);
43844
+ }
43845
+ await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43605
43846
  return { allow: true, audited: true };
43606
43847
  }
43848
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
43849
+ async flushLogs() {
43850
+ await this.logger.flush();
43851
+ }
43852
+ unverifiedRead(reason) {
43853
+ if (!this.enforce) return null;
43854
+ return { block: true, blockReason: reason, allow: false, reason, audited: true };
43855
+ }
43607
43856
  async writeMemoryAtomic(content) {
43608
43857
  await fs3.mkdir(path6.dirname(this.memoryFilePath), { recursive: true });
43609
43858
  const tmp = `${this.memoryFilePath}.${process.pid}.tmp`;