@atbash/sdk 0.16.5-dev.0 → 0.17.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/browser.mjs CHANGED
@@ -44360,6 +44360,13 @@ function buildAllowedJudgeHosts() {
44360
44360
  ]);
44361
44361
  }
44362
44362
  var ALLOWED_JUDGE_HOSTS = buildAllowedJudgeHosts();
44363
+ function isAllowlistedJudgeHost(endpoint) {
44364
+ try {
44365
+ return ALLOWED_JUDGE_HOSTS.has(new URL(endpoint).hostname.toLowerCase());
44366
+ } catch {
44367
+ return false;
44368
+ }
44369
+ }
44363
44370
  function validateJudgeEndpoint(judge) {
44364
44371
  const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
44365
44372
  const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
@@ -44583,7 +44590,7 @@ init_define_ATBASH_CHROMIA_NODE_URLS();
44583
44590
  init_define_ATBASH_PRIVATE_NODE_URLS();
44584
44591
  function normalizeVerdict(raw2) {
44585
44592
  if (raw2 === null || raw2 === void 0) return "No verdict";
44586
- const v = String(raw2).toUpperCase();
44593
+ const v = String(raw2).trim().toUpperCase();
44587
44594
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
44588
44595
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
44589
44596
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -44620,17 +44627,58 @@ function allowSignalsPermit(data) {
44620
44627
  }
44621
44628
  return signals.length === 0 || signals.every((value) => value === true);
44622
44629
  }
44630
+ function explicitRefusal(data) {
44631
+ let hold = false;
44632
+ for (const source of decisionSources(data)) {
44633
+ const verdict = verdictOf(source);
44634
+ if (verdict === "BLOCK" || verdict === "RED") return "BLOCK";
44635
+ if (verdict === "HOLD" || verdict === "YELLOW") hold = true;
44636
+ const actionType = actionTypeOf(source);
44637
+ if (actionType === "block") return "BLOCK";
44638
+ if (actionType === "hold_for_user_confirm") hold = true;
44639
+ }
44640
+ if (!allowSignalsPermit(data)) return "BLOCK";
44641
+ return hold ? "HOLD" : "";
44642
+ }
44643
+ function decisionSources(data) {
44644
+ return [data, isRecord(data.decision) ? data.decision : {}];
44645
+ }
44646
+ function verdictOf(source) {
44647
+ return typeof source.verdict === "string" ? source.verdict.trim().toUpperCase() : "";
44648
+ }
44649
+ function actionTypeRank(actionType) {
44650
+ if (actionType === "block") return 4;
44651
+ if (actionType === "hold_for_user_confirm") return 3;
44652
+ if (actionType === "") return 1;
44653
+ if (actionType === "allow") return 0;
44654
+ return 2;
44655
+ }
44656
+ function actionTypeOf(source) {
44657
+ let winner = "";
44658
+ for (const key3 of ["action_type", "actionType"]) {
44659
+ const raw2 = source[key3];
44660
+ if (raw2 === void 0 || raw2 === null) continue;
44661
+ const actionType = normalizeActionType(raw2);
44662
+ if (actionTypeRank(actionType) > actionTypeRank(winner)) winner = actionType;
44663
+ }
44664
+ return winner;
44665
+ }
44623
44666
  function wireActionType(data) {
44624
- if (data.action_type !== void 0)
44625
- return normalizeActionType(data.action_type);
44626
- return normalizeActionType(data.actionType);
44667
+ for (const source of decisionSources(data)) {
44668
+ const actionType = actionTypeOf(source);
44669
+ if (actionType !== "") return actionType;
44670
+ }
44671
+ return "";
44627
44672
  }
44628
44673
  function isAuditTier(data) {
44629
44674
  const absentVerdict = data.verdict === null || data.verdict === void 0;
44630
44675
  return absentVerdict && data.status === "logged";
44631
44676
  }
44677
+ function isAuditTierBody(data) {
44678
+ return isAuditTier(data);
44679
+ }
44632
44680
  function canonicalAllow(data) {
44633
- if (!allowSignalsPermit(data)) return false;
44681
+ if (explicitRefusal(data) !== "") return false;
44634
44682
  const actionType = wireActionType(data);
44635
44683
  if (actionType && actionType !== "allow") return false;
44636
44684
  if (isAuditTier(data)) return true;
@@ -44713,6 +44761,10 @@ var Atbash = class _Atbash {
44713
44761
  orgEncryptionPubKey;
44714
44762
  /** When true (default), `auditToolCall` denies on any error. */
44715
44763
  failClosed;
44764
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
44765
+ requireEnforcement;
44766
+ /** Overall deadline for one auditToolCall decision, in ms. */
44767
+ decisionTimeoutMs;
44716
44768
  /** Org key learned from the last agent-exists check, for this agent only. */
44717
44769
  _orgKeyFromChain = null;
44718
44770
  debug;
@@ -44786,6 +44838,8 @@ var Atbash = class _Atbash {
44786
44838
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
44787
44839
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
44788
44840
  this.failClosed = options.failClosed !== false;
44841
+ this.requireEnforcement = options.requireEnforcement !== false;
44842
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
44789
44843
  this.debug = options.debug === true;
44790
44844
  this.logger = options.logger ?? {};
44791
44845
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -44796,16 +44850,23 @@ var Atbash = class _Atbash {
44796
44850
  verifying: this.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
44797
44851
  });
44798
44852
  }
44799
- try {
44800
- setupTelemetry({
44801
- enabled: true,
44802
- endpoint: this.endpoint,
44803
- getAuthHeaders: () => this.authHeaders()
44804
- });
44805
- } catch (err) {
44806
- this.logger.warn?.(
44807
- "[atbash] telemetry setup failed \u2014 continuing without metrics",
44808
- { error: String(err) }
44853
+ if (isAllowlistedJudgeHost(this.endpoint)) {
44854
+ try {
44855
+ setupTelemetry({
44856
+ enabled: true,
44857
+ endpoint: this.endpoint,
44858
+ getAuthHeaders: () => this.authHeaders()
44859
+ });
44860
+ } catch (err) {
44861
+ this.logger.warn?.(
44862
+ "[atbash] telemetry setup failed \u2014 continuing without metrics",
44863
+ { error: String(err) }
44864
+ );
44865
+ }
44866
+ } else {
44867
+ this.logger.info?.(
44868
+ "[atbash] telemetry disabled \u2014 the agent bearer is only sent to hosts that already receive it, and this judge endpoint is not one",
44869
+ { endpoint: this.endpoint }
44809
44870
  );
44810
44871
  }
44811
44872
  }
@@ -44869,6 +44930,8 @@ var Atbash = class _Atbash {
44869
44930
  orgName: options.orgName,
44870
44931
  verifyPubKey: validated.verifyPubKey ?? void 0,
44871
44932
  failClosed: options.failClosed,
44933
+ requireEnforcement: options.requireEnforcement,
44934
+ decisionTimeoutMs: options.decisionTimeoutMs,
44872
44935
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
44873
44936
  // host's plugin config, which is usually the harder half.
44874
44937
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -45067,34 +45130,16 @@ var Atbash = class _Atbash {
45067
45130
  }
45068
45131
  if (!resp.ok) throw await this.httpError(resp);
45069
45132
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
45070
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
45133
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
45071
45134
  if (verifyPubKey !== void 0) {
45072
- const sig = resp.headers.get("X-Atbash-Signature");
45073
- if (!sig) {
45074
- throw new SignatureVerificationError(
45075
- "missing X-Atbash-Signature header"
45076
- );
45077
- }
45078
- let ok;
45079
- try {
45080
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
45081
- } catch (err) {
45082
- throw new SignatureVerificationError(
45083
- `signature verification threw: ${errorMessage(err)}`
45084
- );
45085
- }
45086
- if (!ok) {
45087
- throw new SignatureVerificationError(
45088
- "signature does not verify against configured verifyPubKey"
45089
- );
45090
- }
45135
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
45091
45136
  }
45092
45137
  const data = parseJson(bodyBytes);
45093
45138
  const rawScore = data.score;
45094
45139
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
45095
45140
  return {
45096
45141
  verdict: normalizeVerdict(data.verdict),
45097
- allow: canonicalAllow(data),
45142
+ allow: this.executable(data),
45098
45143
  actionType: normalizeActionType(data.action_type),
45099
45144
  reason: String(data.reason ?? ""),
45100
45145
  confidence: Number(data.confidence ?? 0),
@@ -45105,7 +45150,8 @@ var Atbash = class _Atbash {
45105
45150
  enforced: Boolean(data.enforced),
45106
45151
  enforcementMode: String(data.enforcement_mode ?? ""),
45107
45152
  status: String(data.status ?? ""),
45108
- score
45153
+ score,
45154
+ refusal: explicitRefusal(data)
45109
45155
  };
45110
45156
  }
45111
45157
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -45138,77 +45184,125 @@ var Atbash = class _Atbash {
45138
45184
  }
45139
45185
  try {
45140
45186
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
45141
- const result = await this.judgeAction(actionText, contextText, {
45142
- toolName,
45143
- toolArgsJson: argsJson,
45144
- orgName: this.orgName,
45145
- // Passed through unredacted — caller vouches for the contents.
45146
- resolved: input.resolved
45147
- });
45148
- if (result.verdict === "No verdict") {
45149
- if (result.status !== "logged") {
45150
- return this.failJudge(
45151
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
45152
- void 0,
45153
- result.toolCallId
45154
- );
45155
- }
45156
- return {
45157
- allow: true,
45158
- verdict: "ALLOW",
45159
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
45160
- toolCallId: result.toolCallId
45161
- };
45162
- }
45163
- const action = result.actionType;
45164
- if (action === "block") {
45187
+ const result = await withDeadline(
45188
+ this.judgeAction(actionText, contextText, {
45189
+ toolName,
45190
+ toolArgsJson: argsJson,
45191
+ orgName: this.orgName,
45192
+ // Passed through unredacted — caller vouches for the contents.
45193
+ resolved: input.resolved
45194
+ }),
45195
+ this.decisionTimeoutMs
45196
+ );
45197
+ return this.decide(result);
45198
+ } catch (err) {
45199
+ if (err instanceof SignatureVerificationError) {
45200
+ this.failJudge(errorMessage(err), err);
45165
45201
  return {
45166
45202
  allow: false,
45167
- verdict: "BLOCK",
45168
- reason: result.reason,
45169
- toolCallId: result.toolCallId
45203
+ verdict: "ERROR",
45204
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
45170
45205
  };
45171
45206
  }
45172
- if (action === "hold_for_user_confirm") {
45173
- return {
45174
- allow: false,
45175
- verdict: "HOLD",
45176
- reason: result.reason || "held for human confirmation",
45177
- toolCallId: result.toolCallId
45178
- };
45207
+ return this.failJudge(errorMessage(err), err);
45208
+ }
45209
+ }
45210
+ /**
45211
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
45212
+ * may only narrow what the client accepts: on a client pinned to a judge
45213
+ * key it must be that same key, never a replacement; on an unpinned client
45214
+ * it adds the check.
45215
+ */
45216
+ effectiveVerifyKey(perCall) {
45217
+ if (perCall === void 0) return this.verifyPubKey;
45218
+ const key3 = perCall.trim().toLowerCase();
45219
+ if (this.verifyPubKey !== void 0 && key3 !== this.verifyPubKey) {
45220
+ throw new SignatureVerificationError(
45221
+ "a per-call verifyPubKey cannot replace the judge key this client is pinned to"
45222
+ );
45223
+ }
45224
+ return key3;
45225
+ }
45226
+ /**
45227
+ * Collapse a judge result into a {@link Decision}. The most restrictive
45228
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
45229
+ * and the canonical `allow` are each able to deny, and only a result where
45230
+ * all three agree may execute. A present `allow` / `decision.allow` that is
45231
+ * not exactly `true` is the judge declining to grant permission, so it
45232
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
45233
+ * never overrides a refusal.
45234
+ */
45235
+ decide(result) {
45236
+ const toolCallId = result.toolCallId;
45237
+ const deny = (verdict, reason) => ({
45238
+ allow: false,
45239
+ verdict,
45240
+ reason,
45241
+ toolCallId
45242
+ });
45243
+ const notGranted = "the judge response did not grant permission";
45244
+ const held = deny("HOLD", result.reason || "held for human confirmation");
45245
+ const blocked = deny("BLOCK", result.reason || notGranted);
45246
+ if (result.refusal === "BLOCK") return blocked;
45247
+ if (result.verdict === "No verdict") {
45248
+ if (result.actionType === "block") {
45249
+ return blocked;
45250
+ }
45251
+ if (result.actionType === "hold_for_user_confirm") {
45252
+ return held;
45253
+ }
45254
+ if (result.refusal === "HOLD") return held;
45255
+ if (result.status !== "logged") {
45256
+ return this.failJudge(
45257
+ `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
45258
+ void 0,
45259
+ toolCallId
45260
+ );
45179
45261
  }
45180
- if (action === "allow") {
45181
- if (result.verdict === "HOLD") {
45182
- return {
45183
- allow: false,
45184
- verdict: "HOLD",
45185
- reason: result.reason,
45186
- toolCallId: result.toolCallId
45187
- };
45188
- }
45189
- if (result.verdict === "BLOCK") {
45190
- return {
45191
- allow: false,
45192
- verdict: "BLOCK",
45193
- reason: result.reason,
45194
- toolCallId: result.toolCallId
45195
- };
45196
- }
45262
+ if (this.requireEnforcement) {
45263
+ this.logger.warn?.(
45264
+ "[atbash] judge did not enforce this action (audit tier) and requireEnforcement is on: denied",
45265
+ { toolCallId }
45266
+ );
45197
45267
  return {
45198
- allow: true,
45199
- verdict: "ALLOW",
45200
- reason: result.reason,
45201
- toolCallId: result.toolCallId
45268
+ allow: false,
45269
+ verdict: "ERROR",
45270
+ reason: "Atbash logged this action but did not check it (audit-tier plan), and this client requires a checked decision. Upgrade the plan, or set requireEnforcement: false for log-only mode.",
45271
+ toolCallId
45202
45272
  };
45203
45273
  }
45204
- return this.failJudge(
45205
- "unrecognized action_type from judge",
45206
- void 0,
45207
- result.toolCallId
45208
- );
45209
- } catch (err) {
45210
- return this.failJudge(errorMessage(err), err);
45274
+ if (result.allow !== true) return deny("BLOCK", notGranted);
45275
+ return {
45276
+ allow: true,
45277
+ verdict: "ALLOW",
45278
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
45279
+ toolCallId
45280
+ };
45281
+ }
45282
+ const action = result.actionType;
45283
+ if (action === "block") {
45284
+ return deny("BLOCK", result.reason || "Blocked by your organisation's Atbash policy.");
45285
+ }
45286
+ if (action === "hold_for_user_confirm") {
45287
+ return held;
45288
+ }
45289
+ if (result.refusal === "HOLD") return held;
45290
+ if (action === "allow") {
45291
+ if (result.verdict === "HOLD") return held;
45292
+ if (result.verdict === "BLOCK") return blocked;
45293
+ if (result.allow !== true) return deny("BLOCK", notGranted);
45294
+ return {
45295
+ allow: true,
45296
+ verdict: "ALLOW",
45297
+ reason: result.reason,
45298
+ toolCallId
45299
+ };
45211
45300
  }
45301
+ return this.failJudge(
45302
+ "unrecognized action_type from judge",
45303
+ void 0,
45304
+ toolCallId
45305
+ );
45212
45306
  }
45213
45307
  /**
45214
45308
  * One exit for every judge failure.
@@ -45220,18 +45314,30 @@ var Atbash = class _Atbash {
45220
45314
  */
45221
45315
  failJudge(reason, cause, toolCallId) {
45222
45316
  const api2 = cause instanceof AtbashAPIError ? cause : null;
45317
+ const userReason = userFacingReason(reason, api2);
45223
45318
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
45319
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
45224
45320
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
45225
45321
  this.logger.warn?.(
45226
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
45322
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
45227
45323
  {
45228
- reason,
45229
- ...api2 ? { status: api2.status, body: api2.body } : {},
45324
+ reason: userReason,
45325
+ ...api2 ? { status: api2.status } : {},
45326
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
45230
45327
  endpoint: this.endpoint,
45231
45328
  ...toolCallId ? { toolCallId } : {}
45232
45329
  }
45233
45330
  );
45234
- return this.fail(reason, toolCallId);
45331
+ return this.fail(userReason, toolCallId);
45332
+ }
45333
+ /**
45334
+ * The canonical permission for a response body under this client's
45335
+ * settings: {@link canonicalAllow}, minus the audit tier when
45336
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
45337
+ */
45338
+ executable(data) {
45339
+ if (!canonicalAllow(data)) return false;
45340
+ return !(this.requireEnforcement && isAuditTierBody(data));
45235
45341
  }
45236
45342
  fail(reason, toolCallId) {
45237
45343
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -45257,11 +45363,16 @@ var Atbash = class _Atbash {
45257
45363
  this.authHeaders(brid)
45258
45364
  );
45259
45365
  await this.raiseIfError(resp);
45260
- const data = await this.json(resp) ?? {};
45366
+ const bytes = Buffer.from(await resp.arrayBuffer());
45367
+ if (this.verifyPubKey !== void 0) {
45368
+ verifyResponseSignature(bytes, resp, this.verifyPubKey);
45369
+ }
45370
+ const parsed = tryParseJson(bytes.toString("utf-8"));
45371
+ const data = isRecord2(parsed) ? parsed : {};
45261
45372
  return {
45262
45373
  status: normalizeStatus(data.status),
45263
45374
  verdict: normalizeVerdict(data.verdict),
45264
- allow: canonicalAllow(data),
45375
+ allow: this.executable(data),
45265
45376
  reason: String(data.reason ?? ""),
45266
45377
  judgmentId: String(data.judgmentId ?? judgmentId),
45267
45378
  onChain: optBool(data.onChain),
@@ -45736,6 +45847,66 @@ var Atbash = class _Atbash {
45736
45847
  return native.containsEvasionCharacters(text);
45737
45848
  }
45738
45849
  };
45850
+ function userFacingReason(reason, api2) {
45851
+ if (api2 && api2.status > 0) {
45852
+ const where = ` (HTTP ${api2.status}${api2.path ? ` from ${api2.path}` : ""})`;
45853
+ const b = api2.body.toLowerCase();
45854
+ if (b.includes("agent not registered")) {
45855
+ return `Agent not registered with Atbash: onboard it in the Atbash dashboard${where}.`;
45856
+ }
45857
+ if (b.includes("jailed")) {
45858
+ return `This agent is jailed; unjail it in the Atbash dashboard${where}.`;
45859
+ }
45860
+ if (b.includes("agent has no policy") || b.includes("no policy configured")) {
45861
+ return `This agent has no policy; attach one in the Atbash dashboard${where}.`;
45862
+ }
45863
+ if (b.includes("audit tier") || b.includes("verdict disabled") || b.includes("verdict not supported")) {
45864
+ return `The organisation's plan does not include checked decisions${where}.`;
45865
+ }
45866
+ return `The Atbash judge answered with an error${where}, so the action was not approved.`;
45867
+ }
45868
+ const text = api2 ? api2.body || reason : reason;
45869
+ return truncate(text.replace(/https?:\/\/[^\s)]+/g, "the judge"), 300);
45870
+ }
45871
+ var DEFAULT_DECISION_TIMEOUT_MS = 6e4;
45872
+ async function withDeadline(p, ms) {
45873
+ let timer;
45874
+ const deadline = new Promise((_, reject) => {
45875
+ timer = setTimeout(
45876
+ () => reject(
45877
+ new Error(
45878
+ `The judge did not give a decision within ${Math.round(ms / 1e3)} s, so the action was not approved. Check the network, or raise decisionTimeoutMs.`
45879
+ )
45880
+ ),
45881
+ ms
45882
+ );
45883
+ });
45884
+ p.catch(() => void 0);
45885
+ try {
45886
+ return await Promise.race([p, deadline]);
45887
+ } finally {
45888
+ clearTimeout(timer);
45889
+ }
45890
+ }
45891
+ function verifyResponseSignature(bodyBytes, resp, verifyPubKey) {
45892
+ const sig = resp.headers.get("X-Atbash-Signature");
45893
+ if (!sig) {
45894
+ throw new SignatureVerificationError("missing X-Atbash-Signature header");
45895
+ }
45896
+ let ok;
45897
+ try {
45898
+ ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
45899
+ } catch (err) {
45900
+ throw new SignatureVerificationError(
45901
+ `signature verification threw: ${errorMessage(err)}`
45902
+ );
45903
+ }
45904
+ if (!ok) {
45905
+ throw new SignatureVerificationError(
45906
+ "signature does not verify against configured verifyPubKey"
45907
+ );
45908
+ }
45909
+ }
45739
45910
  function baseToolCall(raw2) {
45740
45911
  return {
45741
45912
  toolCallId: String(raw2.tool_call_id ?? ""),
@@ -46124,7 +46295,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
46124
46295
  if (withinTtl) {
46125
46296
  return { drifted: false, checked: false, pointer };
46126
46297
  }
46127
- const currentId = await getActiveMemoryId(auth, opts.chainOpts);
46298
+ const currentId = await getActiveMemoryId(auth, opts.chainOpts, opts.filePath);
46128
46299
  const nextPointer = { activeId: currentId, checkedAt: now };
46129
46300
  if (currentId === pointer.activeId) {
46130
46301
  return { drifted: false, checked: true, pointer: nextPointer };