@atbash/sdk 0.7.0 → 0.7.1

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
@@ -3046,24 +3046,97 @@ var HttpClient = class {
3046
3046
  });
3047
3047
  }
3048
3048
  async fetch(url, init4) {
3049
- return fetch(url, { ...init4, signal: AbortSignal.timeout(this.timeoutMs) });
3049
+ try {
3050
+ return await fetch(url, {
3051
+ ...init4,
3052
+ signal: AbortSignal.timeout(this.timeoutMs)
3053
+ });
3054
+ } catch (err) {
3055
+ throw classifyTransportError(err, url, init4.method ?? "GET", this.timeoutMs);
3056
+ }
3050
3057
  }
3051
3058
  };
3059
+ var HttpTransportError = class extends Error {
3060
+ kind;
3061
+ constructor(kind, message, options) {
3062
+ super(message, options);
3063
+ this.name = "HttpTransportError";
3064
+ this.kind = kind;
3065
+ }
3066
+ };
3067
+ function classifyTransportError(err, url, method, timeoutMs) {
3068
+ const name2 = err instanceof Error ? err.name : "";
3069
+ const cause = err instanceof Error ? err.cause : void 0;
3070
+ const code2 = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
3071
+ if (name2 === "TimeoutError") {
3072
+ return new HttpTransportError(
3073
+ "timeout",
3074
+ `${method} ${url} did not respond within ${timeoutMs} ms \u2014 the judge may be slow to boot or the LLM is under load; retry in a moment`,
3075
+ { cause: err }
3076
+ );
3077
+ }
3078
+ if (name2 === "AbortError") {
3079
+ return new HttpTransportError(
3080
+ "aborted",
3081
+ `${method} ${url} was cancelled by the caller`,
3082
+ { cause: err }
3083
+ );
3084
+ }
3085
+ if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") {
3086
+ return new HttpTransportError(
3087
+ "dns",
3088
+ `could not resolve the judge hostname (${url}) \u2014 check the endpoint and DNS`,
3089
+ { cause: err }
3090
+ );
3091
+ }
3092
+ if (code2 === "ECONNREFUSED") {
3093
+ return new HttpTransportError(
3094
+ "connect_refused",
3095
+ `judge refused the connection (${url}) \u2014 the service may be down or restarting`,
3096
+ { cause: err }
3097
+ );
3098
+ }
3099
+ if (code2 === "ECONNRESET" || code2 === "EPIPE") {
3100
+ return new HttpTransportError(
3101
+ "connection_reset",
3102
+ `judge dropped the connection mid-request (${url}) \u2014 retry once`,
3103
+ { cause: err }
3104
+ );
3105
+ }
3106
+ return new HttpTransportError(
3107
+ "unknown",
3108
+ `${method} ${url} failed: ${err instanceof Error ? err.message : String(err)}`,
3109
+ { cause: err }
3110
+ );
3111
+ }
3052
3112
 
3053
3113
  // src-ts/keyLoader.ts
3054
- import { readFileSync } from "fs";
3114
+ import { existsSync, readFileSync } from "fs";
3115
+
3116
+ // src-ts/key-path.ts
3055
3117
  import { homedir } from "os";
3056
3118
  import { join } from "path";
3057
- var DEFAULT_KEY_PATH_REL = ".config/atbash/guard-client-key";
3058
- function resolveKeyPath(input) {
3059
- if (input) return expandHome(input);
3060
- const home = process.env.HOME || homedir() || "";
3061
- return join(home, DEFAULT_KEY_PATH_REL);
3119
+ var KEY_DIR_REL = ".config/atbash";
3120
+ var KEY_FILENAMES = ["guard-client-key", "atbash-client-key"];
3121
+ function home() {
3122
+ return process.env.HOME || homedir() || "";
3062
3123
  }
3063
3124
  function expandHome(p) {
3064
3125
  if (!p.startsWith("~/")) return p;
3065
- const home = process.env.HOME || homedir() || "";
3066
- return join(home, p.slice(2));
3126
+ return join(home(), p.slice(2));
3127
+ }
3128
+ function keyPathCandidates() {
3129
+ return KEY_FILENAMES.map((name2) => join(home(), KEY_DIR_REL, name2));
3130
+ }
3131
+ function chooseKeyPath(input, exists) {
3132
+ if (input) return expandHome(input);
3133
+ const candidates = keyPathCandidates();
3134
+ return candidates.find(exists) ?? candidates[0];
3135
+ }
3136
+
3137
+ // src-ts/keyLoader.ts
3138
+ function resolveKeyPath(input) {
3139
+ return chooseKeyPath(input, existsSync);
3067
3140
  }
3068
3141
  function readKeyFile(keyPath) {
3069
3142
  const content = String(readFileSync(keyPath, "utf8") || "").trim();
@@ -3093,6 +3166,12 @@ function readKeyFile(keyPath) {
3093
3166
  }
3094
3167
  function loadAgentFromFile(keyPath) {
3095
3168
  const resolved = resolveKeyPath(keyPath);
3169
+ if (!existsSync(resolved)) {
3170
+ const looked = keyPath ? [resolved] : keyPathCandidates();
3171
+ throw new Error(
3172
+ `atbash key file not found. Looked for: ${looked.join(", ")}`
3173
+ );
3174
+ }
3096
3175
  const { privKey } = readKeyFile(resolved);
3097
3176
  return native.loadAgent(privKey);
3098
3177
  }
@@ -3138,8 +3217,8 @@ var durationHistogram = null;
3138
3217
  var defaultSource = "sdk";
3139
3218
  function isTelemetryOptedOut() {
3140
3219
  try {
3141
- const home = process.env.HOME || homedir2() || "";
3142
- const filePath = join2(home, ".config", "atbash", "telemetry.json");
3220
+ const home2 = process.env.HOME || homedir2() || "";
3221
+ const filePath = join2(home2, ".config", "atbash", "telemetry.json");
3143
3222
  const raw2 = readFileSync2(filePath, "utf-8").trim();
3144
3223
  if (!raw2) return false;
3145
3224
  const config2 = JSON.parse(raw2);
@@ -3217,7 +3296,7 @@ async function shutdownTelemetry() {
3217
3296
  // src-ts/userConfig.ts
3218
3297
  import {
3219
3298
  chmodSync,
3220
- existsSync,
3299
+ existsSync as existsSync2,
3221
3300
  mkdirSync,
3222
3301
  readFileSync as readFileSync3,
3223
3302
  writeFileSync
@@ -3230,11 +3309,12 @@ var ENV_MAP = {
3230
3309
  judgeEndpoint: "ATBASH_ENDPOINT",
3231
3310
  blockchainRid: "ATBASH_BLOCKCHAIN_RID",
3232
3311
  provider: "ATBASH_PROVIDER",
3233
- providerModel: "ATBASH_PROVIDER_MODEL"
3312
+ providerModel: "ATBASH_PROVIDER_MODEL",
3313
+ debug: "ATBASH_DEBUG"
3234
3314
  };
3235
3315
  function getConfigDir() {
3236
- const home = process.env.HOME || homedir3() || "";
3237
- return join3(home, ".config", "atbash");
3316
+ const home2 = process.env.HOME || homedir3() || "";
3317
+ return join3(home2, ".config", "atbash");
3238
3318
  }
3239
3319
  function getConfigPath() {
3240
3320
  return join3(getConfigDir(), "config.json");
@@ -3242,7 +3322,7 @@ function getConfigPath() {
3242
3322
  function loadUserConfig() {
3243
3323
  try {
3244
3324
  const p = getConfigPath();
3245
- if (!existsSync(p)) return {};
3325
+ if (!existsSync2(p)) return {};
3246
3326
  const raw2 = readFileSync3(p, "utf-8").trim();
3247
3327
  if (!raw2) return {};
3248
3328
  return JSON.parse(raw2);
@@ -3253,7 +3333,7 @@ function loadUserConfig() {
3253
3333
  }
3254
3334
  function saveUserConfig(config2) {
3255
3335
  const dir = getConfigDir();
3256
- if (!existsSync(dir)) {
3336
+ if (!existsSync2(dir)) {
3257
3337
  mkdirSync(dir, { recursive: true, mode: 448 });
3258
3338
  }
3259
3339
  const filePath = getConfigPath();
@@ -3293,6 +3373,7 @@ var Atbash = class _Atbash {
3293
3373
  failClosed;
3294
3374
  /** Org key learned from the last agent-exists check, for this agent only. */
3295
3375
  _orgKeyFromChain = null;
3376
+ debug;
3296
3377
  logger;
3297
3378
  http;
3298
3379
  /**
@@ -3306,6 +3387,8 @@ var Atbash = class _Atbash {
3306
3387
  * server-side replay protection windows never expire it mid-session.
3307
3388
  */
3308
3389
  _authBearer = null;
3390
+ /** Guards `logEnvironmentOnce` — hosts construct several clients. */
3391
+ static environmentLogged = false;
3309
3392
  constructor(privkey, options = {}) {
3310
3393
  this.auth = native.loadAgent(privkey);
3311
3394
  this.endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "") || DEFAULT_ENDPOINT;
@@ -3315,8 +3398,10 @@ var Atbash = class _Atbash {
3315
3398
  this.verifyPubKey = options.verifyPubKey;
3316
3399
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
3317
3400
  this.failClosed = options.failClosed !== false;
3401
+ this.debug = options.debug === true;
3318
3402
  this.logger = options.logger ?? {};
3319
- this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 3e4);
3403
+ this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 6e4);
3404
+ this.logEnvironmentOnce();
3320
3405
  if (this.endpoint !== DEFAULT_ENDPOINT) {
3321
3406
  this.logger.warn?.("[atbash] running on non-default judge endpoint", {
3322
3407
  endpoint: this.endpoint,
@@ -3324,6 +3409,31 @@ var Atbash = class _Atbash {
3324
3409
  });
3325
3410
  }
3326
3411
  }
3412
+ /**
3413
+ * Say which environment this build talks to, once per process.
3414
+ *
3415
+ * The endpoint and both chain RIDs are compiled in, chosen by the npm tag,
3416
+ * and no configuration repoints a released build. So installing the build
3417
+ * for the wrong environment is invisible: the plugin loads, the hook fires,
3418
+ * and every judge call fails because the agent does not exist on the chain
3419
+ * this build targets. Organisation names are not unique across environments
3420
+ * either, so an org resolving is not evidence the build is right.
3421
+ */
3422
+ logEnvironmentOnce() {
3423
+ if (_Atbash.environmentLogged) return;
3424
+ _Atbash.environmentLogged = true;
3425
+ const brief = (rid) => rid ? `${rid.slice(0, 8)}\u2026` : "(unset)";
3426
+ this.logger.info?.(
3427
+ `[atbash] environment \u2014 judge=${this.endpoint} publicChain=${brief(native.DEFAULT_BLOCKCHAIN_RID)} privateChain=${brief(native.DEFAULT_PRIVATE_BLOCKCHAIN_RID)} activeChain=${brief(this.blockchainRid)} responseSignatureCheck=${this.verifyPubKey ? "on" : "off"}`,
3428
+ {
3429
+ judgeEndpoint: this.endpoint,
3430
+ publicBlockchainRid: native.DEFAULT_BLOCKCHAIN_RID,
3431
+ privateBlockchainRid: native.DEFAULT_PRIVATE_BLOCKCHAIN_RID,
3432
+ activeBlockchainRid: this.blockchainRid,
3433
+ responseSignatureCheck: Boolean(this.verifyPubKey)
3434
+ }
3435
+ );
3436
+ }
3327
3437
  /**
3328
3438
  * Construct from resolved config: explicit overrides → env vars → the
3329
3439
  * `~/.config/atbash/config.json` file (see userConfig.resolve). The private
@@ -3347,6 +3457,9 @@ var Atbash = class _Atbash {
3347
3457
  orgName: options.orgName,
3348
3458
  verifyPubKey: validated.verifyPubKey ?? void 0,
3349
3459
  failClosed: options.failClosed,
3460
+ // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
3461
+ // host's plugin config, which is usually the harder half.
3462
+ debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
3350
3463
  logger: options.logger
3351
3464
  });
3352
3465
  }
@@ -3446,11 +3559,15 @@ var Atbash = class _Atbash {
3446
3559
  * response bytes via the Rust core's `verifySignature`.
3447
3560
  */
3448
3561
  async judgeAction(action, context = "", options = {}) {
3449
- return this.track(
3450
- "judgeAction",
3451
- this.auth.pubkey,
3452
- () => this._judgeAction(action, context, options)
3453
- );
3562
+ return this.track("judgeAction", this.auth.pubkey, async () => {
3563
+ try {
3564
+ return await this._judgeAction(action, context, options);
3565
+ } catch (err) {
3566
+ if (!isEncryptionStateMismatch(err)) throw err;
3567
+ this._orgKeyFromChain = null;
3568
+ return await this._judgeAction(action, context, options);
3569
+ }
3570
+ });
3454
3571
  }
3455
3572
  async _judgeAction(action, context, options) {
3456
3573
  if (!action?.trim()) {
@@ -3588,8 +3705,9 @@ var Atbash = class _Atbash {
3588
3705
  });
3589
3706
  if (result.verdict === "No verdict") {
3590
3707
  if (result.status !== "logged") {
3591
- return this.fail(
3708
+ return this.failJudge(
3592
3709
  `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
3710
+ void 0,
3593
3711
  result.toolCallId
3594
3712
  );
3595
3713
  }
@@ -3641,16 +3759,38 @@ var Atbash = class _Atbash {
3641
3759
  toolCallId: result.toolCallId
3642
3760
  };
3643
3761
  }
3644
- return this.fail(
3762
+ return this.failJudge(
3645
3763
  "unrecognized action_type from judge",
3764
+ void 0,
3646
3765
  result.toolCallId
3647
3766
  );
3648
3767
  } catch (err) {
3649
- const message = errorMessage(err);
3650
- this.logger.warn?.("[atbash] judge API failed", { reason: message });
3651
- return this.fail(message);
3768
+ return this.failJudge(errorMessage(err), err);
3652
3769
  }
3653
3770
  }
3771
+ /**
3772
+ * One exit for every judge failure.
3773
+ *
3774
+ * Status and reason go in the *message*, not only in the meta object: hosts
3775
+ * print the message and drop the meta, which is why this read as a bare
3776
+ * "judge API failed" while the judge was answering with a precise reason.
3777
+ * The response body follows only under `debug`, since it can echo the action.
3778
+ */
3779
+ failJudge(reason, cause, toolCallId) {
3780
+ const api2 = cause instanceof AtbashAPIError ? cause : null;
3781
+ const status = api2 ? ` status=${api2.status || "no-response"}` : "";
3782
+ const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
3783
+ this.logger.warn?.(
3784
+ `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
3785
+ {
3786
+ reason,
3787
+ ...api2 ? { status: api2.status, body: api2.body } : {},
3788
+ endpoint: this.endpoint,
3789
+ ...toolCallId ? { toolCallId } : {}
3790
+ }
3791
+ );
3792
+ return this.fail(reason, toolCallId);
3793
+ }
3654
3794
  fail(reason, toolCallId) {
3655
3795
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
3656
3796
  }
@@ -3991,8 +4131,27 @@ var Atbash = class _Atbash {
3991
4131
  this.endpoint
3992
4132
  );
3993
4133
  }
3994
- /** Wrap a *transport* failure (fetch threw, no response) as an AtbashAPIError. */
4134
+ /**
4135
+ * Wrap a transport failure (fetch threw, no response) as an AtbashAPIError.
4136
+ *
4137
+ * `HttpTransportError.kind` names the cause; the message is already
4138
+ * human-readable. `debug` echoes the original exception so operators can
4139
+ * cross-reference with node / undici logs when a class doesn't match.
4140
+ */
3995
4141
  transportError(err) {
4142
+ if (err instanceof HttpTransportError) {
4143
+ if (this.debug) {
4144
+ this.logger.warn?.(
4145
+ `[atbash] transport failed \u2014 kind=${err.kind}`,
4146
+ {
4147
+ kind: err.kind,
4148
+ cause: err.cause instanceof Error ? err.cause.message : String(err.cause ?? ""),
4149
+ endpoint: this.endpoint
4150
+ }
4151
+ );
4152
+ }
4153
+ return new AtbashAPIError(0, err.message, "", this.endpoint);
4154
+ }
3996
4155
  return new AtbashAPIError(0, errorMessage(err), "", this.endpoint);
3997
4156
  }
3998
4157
  async json(resp) {
@@ -4124,6 +4283,10 @@ async function safeText(resp) {
4124
4283
  function errorMessage(err) {
4125
4284
  return err instanceof Error ? err.message : String(err);
4126
4285
  }
4286
+ function isEncryptionStateMismatch(err) {
4287
+ const msg = errorMessage(err);
4288
+ return msg.includes("must be a valid encryption envelope") || msg.includes("must be plaintext");
4289
+ }
4127
4290
  function stringifyArgs(args) {
4128
4291
  if (args === null || args === void 0) return "";
4129
4292
  if (typeof args === "string") return args;
@@ -4134,9 +4297,9 @@ function stringifyArgs(args) {
4134
4297
  }
4135
4298
  }
4136
4299
  var MAX_ACTION_LEN = 4e3;
4137
- function truncate(text) {
4138
- if (text.length <= MAX_ACTION_LEN) return text;
4139
- return text.slice(0, MAX_ACTION_LEN) + "\u2026";
4300
+ function truncate(text, limit = MAX_ACTION_LEN) {
4301
+ if (text.length <= limit) return text;
4302
+ return text.slice(0, limit) + "\u2026";
4140
4303
  }
4141
4304
 
4142
4305
  // src-ts/redact.ts
@@ -4179,6 +4342,17 @@ function verifyJudgeResponseSignature(bodyBytes, signatureHex, pubKeyHex) {
4179
4342
  };
4180
4343
  }
4181
4344
 
4345
+ // src-ts/memory/boot-sync-message.ts
4346
+ var BOOT_SYNC_HINT = "check the chain endpoint and orgName if the cause above does not explain it";
4347
+ function bootSyncFailureLine(cause) {
4348
+ const reason = cause instanceof Error ? cause.message : String(cause);
4349
+ const trimmed = reason.trim();
4350
+ return trimmed ? `[atbash] boot memory sync failed: ${trimmed}` : (
4351
+ // No cause to show: fall back to the advice rather than a bare colon.
4352
+ `[atbash] boot memory sync failed \u2014 ${BOOT_SYNC_HINT}`
4353
+ );
4354
+ }
4355
+
4182
4356
  // src-ts/memory/crypto.ts
4183
4357
  async function deriveMemoryKey(privkey) {
4184
4358
  return native.deriveMemoryKey(privkey);
@@ -4204,6 +4378,19 @@ async function scanMemory(entry, auth, opts) {
4204
4378
  toolArgsJson: opts?.toolArgsJson ?? JSON.stringify({ key: entry.key, source: entry.source }),
4205
4379
  mode: "memory-scan"
4206
4380
  });
4381
+ const knownAction = result.actionType === "allow" || result.actionType === "block" || result.actionType === "hold_for_user_confirm";
4382
+ const missingVerdict = result.verdict === "No verdict" && result.status !== "logged";
4383
+ const unknownAction = result.actionType !== "" && !knownAction;
4384
+ if (missingVerdict || unknownAction) {
4385
+ return {
4386
+ safe: false,
4387
+ verdict: "red",
4388
+ reason: unknownAction ? `judge returned unrecognised action_type "${result.actionType}"` : "judge returned no verdict",
4389
+ confidence: result.confidence,
4390
+ score: native.defaultScoreForVerdict("red"),
4391
+ toolCallId: result.toolCallId
4392
+ };
4393
+ }
4207
4394
  const verdict = native.mapVerdict(
4208
4395
  result.actionType,
4209
4396
  result.confidence,
@@ -42354,6 +42541,10 @@ var index = /* @__PURE__ */ getDefaultExportFromCjs(builtExports);
42354
42541
 
42355
42542
  // src-ts/memory/chain.ts
42356
42543
  var { createClient, encryption: encryption2, newSignatureProvider: newSignatureProvider2, Buffer: PolyBuffer } = index;
42544
+ var FAILOVER_CONFIG = {
42545
+ strategy: "tryNextOnError",
42546
+ attemptsPerEndpoint: 1
42547
+ };
42357
42548
  function toGtxBytes(bytes) {
42358
42549
  return PolyBuffer.from(new Uint8Array(bytes));
42359
42550
  }
@@ -42383,7 +42574,11 @@ function materializeChain(chainOpts) {
42383
42574
  }
42384
42575
  async function buildChainClient(chainOpts) {
42385
42576
  const { nodeUrls, blockchainRid } = materializeChain(chainOpts);
42386
- return createClient({ nodeUrlPool: [...nodeUrls], blockchainRid });
42577
+ return createClient({
42578
+ nodeUrlPool: [...nodeUrls],
42579
+ blockchainRid,
42580
+ failOverConfig: FAILOVER_CONFIG
42581
+ });
42387
42582
  }
42388
42583
  function buildSigner(auth) {
42389
42584
  const privKeyBuf = Buffer.from(auth.privkey, "hex");
@@ -42821,7 +43016,10 @@ var MemoryGuardManager = class {
42821
43016
  await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
42822
43017
  } catch (err) {
42823
43018
  const msg = err instanceof Error ? err.message : String(err);
42824
- this.logger.warn("[atbash] boot memory sync failed \u2014 check chain endpoint / orgName", { error: msg });
43019
+ this.logger.warn(bootSyncFailureLine(err), {
43020
+ error: msg,
43021
+ hint: BOOT_SYNC_HINT
43022
+ });
42825
43023
  }
42826
43024
  }
42827
43025
  /**
@@ -43049,6 +43247,7 @@ function diffMemorySnapshots(before, after) {
43049
43247
  export {
43050
43248
  Atbash,
43051
43249
  AtbashAPIError,
43250
+ BOOT_SYNC_HINT,
43052
43251
  DEFAULT_BLOCKCHAIN_RID,
43053
43252
  DEFAULT_CHROMIA_NODE_URLS,
43054
43253
  DEFAULT_ENDPOINT,
@@ -43056,11 +43255,16 @@ export {
43056
43255
  DEFAULT_MEMORY_READ_TOOL_NAMES,
43057
43256
  DEFAULT_MEMORY_WRITE_TOOL_NAMES,
43058
43257
  EciesDomain,
43258
+ HttpClient,
43259
+ HttpTransportError,
43260
+ KEY_FILENAMES,
43059
43261
  MemoryGuardManager,
43060
43262
  MemoryIntegrityError,
43061
43263
  PointerStore,
43062
43264
  SignatureVerificationError,
43265
+ bootSyncFailureLine,
43063
43266
  buildAllowedJudgeHosts,
43267
+ chooseKeyPath,
43064
43268
  claimHashHex,
43065
43269
  classifyMemoryRead,
43066
43270
  classifyMemoryWrite,
@@ -43095,6 +43299,7 @@ export {
43095
43299
  isEnvelope,
43096
43300
  isValidPrivateKey,
43097
43301
  keyFingerprintOf,
43302
+ keyPathCandidates,
43098
43303
  loadAgent,
43099
43304
  loadAgentFromFile,
43100
43305
  loadUserConfig,