@atbash/sdk 0.16.6-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.
@@ -198,6 +198,11 @@ interface JudgeResult {
198
198
  * missing" — never infer the former from a null verdict alone.
199
199
  */
200
200
  status: string;
201
+ /**
202
+ * The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
203
+ * "HOLD", or "" when it neither refused nor is a known refusal shape.
204
+ */
205
+ refusal?: "" | "BLOCK" | "HOLD";
201
206
  }
202
207
  interface JudgmentStatus {
203
208
  status: JudgmentState;
@@ -305,6 +310,13 @@ interface AtbashLogger {
305
310
  /** Options accepted by the `Atbash` constructor. */
306
311
  interface AtbashOptions {
307
312
  endpoint?: string;
313
+ /**
314
+ * Overall deadline for one `auditToolCall` decision, across every request
315
+ * it makes (org lookup, exists check, judge). Default 60 000 ms. When it
316
+ * passes the decision is an ERROR (denied unless `failClosed` is false).
317
+ * `timeoutMs` still bounds each single request.
318
+ */
319
+ decisionTimeoutMs?: number;
308
320
  timeoutMs?: number;
309
321
  /**
310
322
  * Full chain override — BRID + nodeUrls in one object. Wins over every
@@ -350,6 +362,16 @@ interface AtbashOptions {
350
362
  orgEncryptionPubKey?: string;
351
363
  /** When true (default), `auditToolCall` denies on any error. */
352
364
  failClosed?: boolean;
365
+ /**
366
+ * When true (the default), an answer that carries no enforcement decision
367
+ * (the audit tier's `{status:"logged"}` with no verdict) is refused:
368
+ * `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
369
+ * `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
370
+ * are false. Set it to `false` only to run deliberately in log-only mode
371
+ * (security review 2026-09-30, H6: an unsigned marker switched enforcement
372
+ * off).
373
+ */
374
+ requireEnforcement?: boolean;
353
375
  /**
354
376
  * Verbose diagnostics. Off by default. When on, a failed judge call also
355
377
  * logs the response body, which is the difference between "judge API failed"
@@ -400,6 +422,10 @@ interface FromConfigOptions {
400
422
  /** Default org name — see {@link AtbashOptions.orgName}. */
401
423
  orgName?: string;
402
424
  failClosed?: boolean;
425
+ /** See {@link AtbashOptions.requireEnforcement}. Default true. */
426
+ requireEnforcement?: boolean;
427
+ /** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
428
+ decisionTimeoutMs?: number;
403
429
  /** See {@link AtbashOptions.debug}. */
404
430
  debug?: boolean;
405
431
  logger?: AtbashLogger;
@@ -456,6 +482,10 @@ declare class Atbash {
456
482
  readonly orgEncryptionPubKey?: string;
457
483
  /** When true (default), `auditToolCall` denies on any error. */
458
484
  readonly failClosed: boolean;
485
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
486
+ readonly requireEnforcement: boolean;
487
+ /** Overall deadline for one auditToolCall decision, in ms. */
488
+ readonly decisionTimeoutMs: number;
459
489
  /** Org key learned from the last agent-exists check, for this agent only. */
460
490
  private _orgKeyFromChain;
461
491
  private readonly debug;
@@ -559,6 +589,23 @@ declare class Atbash {
559
589
  * explicitly false.
560
590
  */
561
591
  auditToolCall(input: ToolCallInput): Promise<Decision>;
592
+ /**
593
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
594
+ * may only narrow what the client accepts: on a client pinned to a judge
595
+ * key it must be that same key, never a replacement; on an unpinned client
596
+ * it adds the check.
597
+ */
598
+ private effectiveVerifyKey;
599
+ /**
600
+ * Collapse a judge result into a {@link Decision}. The most restrictive
601
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
602
+ * and the canonical `allow` are each able to deny, and only a result where
603
+ * all three agree may execute. A present `allow` / `decision.allow` that is
604
+ * not exactly `true` is the judge declining to grant permission, so it
605
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
606
+ * never overrides a refusal.
607
+ */
608
+ private decide;
562
609
  /**
563
610
  * One exit for every judge failure.
564
611
  *
@@ -568,6 +615,12 @@ declare class Atbash {
568
615
  * The response body follows only under `debug`, since it can echo the action.
569
616
  */
570
617
  private failJudge;
618
+ /**
619
+ * The canonical permission for a response body under this client's
620
+ * settings: {@link canonicalAllow}, minus the audit tier when
621
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
622
+ */
623
+ private executable;
571
624
  private fail;
572
625
  /**
573
626
  * Return the current status of a previously submitted judgment.
package/dist/browser.mjs CHANGED
@@ -44590,7 +44590,7 @@ init_define_ATBASH_CHROMIA_NODE_URLS();
44590
44590
  init_define_ATBASH_PRIVATE_NODE_URLS();
44591
44591
  function normalizeVerdict(raw2) {
44592
44592
  if (raw2 === null || raw2 === void 0) return "No verdict";
44593
- const v = String(raw2).toUpperCase();
44593
+ const v = String(raw2).trim().toUpperCase();
44594
44594
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
44595
44595
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
44596
44596
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -44627,17 +44627,58 @@ function allowSignalsPermit(data) {
44627
44627
  }
44628
44628
  return signals.length === 0 || signals.every((value) => value === true);
44629
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
+ }
44630
44666
  function wireActionType(data) {
44631
- if (data.action_type !== void 0)
44632
- return normalizeActionType(data.action_type);
44633
- return normalizeActionType(data.actionType);
44667
+ for (const source of decisionSources(data)) {
44668
+ const actionType = actionTypeOf(source);
44669
+ if (actionType !== "") return actionType;
44670
+ }
44671
+ return "";
44634
44672
  }
44635
44673
  function isAuditTier(data) {
44636
44674
  const absentVerdict = data.verdict === null || data.verdict === void 0;
44637
44675
  return absentVerdict && data.status === "logged";
44638
44676
  }
44677
+ function isAuditTierBody(data) {
44678
+ return isAuditTier(data);
44679
+ }
44639
44680
  function canonicalAllow(data) {
44640
- if (!allowSignalsPermit(data)) return false;
44681
+ if (explicitRefusal(data) !== "") return false;
44641
44682
  const actionType = wireActionType(data);
44642
44683
  if (actionType && actionType !== "allow") return false;
44643
44684
  if (isAuditTier(data)) return true;
@@ -44720,6 +44761,10 @@ var Atbash = class _Atbash {
44720
44761
  orgEncryptionPubKey;
44721
44762
  /** When true (default), `auditToolCall` denies on any error. */
44722
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;
44723
44768
  /** Org key learned from the last agent-exists check, for this agent only. */
44724
44769
  _orgKeyFromChain = null;
44725
44770
  debug;
@@ -44793,6 +44838,8 @@ var Atbash = class _Atbash {
44793
44838
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
44794
44839
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
44795
44840
  this.failClosed = options.failClosed !== false;
44841
+ this.requireEnforcement = options.requireEnforcement !== false;
44842
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
44796
44843
  this.debug = options.debug === true;
44797
44844
  this.logger = options.logger ?? {};
44798
44845
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -44883,6 +44930,8 @@ var Atbash = class _Atbash {
44883
44930
  orgName: options.orgName,
44884
44931
  verifyPubKey: validated.verifyPubKey ?? void 0,
44885
44932
  failClosed: options.failClosed,
44933
+ requireEnforcement: options.requireEnforcement,
44934
+ decisionTimeoutMs: options.decisionTimeoutMs,
44886
44935
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
44887
44936
  // host's plugin config, which is usually the harder half.
44888
44937
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -45081,34 +45130,16 @@ var Atbash = class _Atbash {
45081
45130
  }
45082
45131
  if (!resp.ok) throw await this.httpError(resp);
45083
45132
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
45084
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
45133
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
45085
45134
  if (verifyPubKey !== void 0) {
45086
- const sig = resp.headers.get("X-Atbash-Signature");
45087
- if (!sig) {
45088
- throw new SignatureVerificationError(
45089
- "missing X-Atbash-Signature header"
45090
- );
45091
- }
45092
- let ok;
45093
- try {
45094
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
45095
- } catch (err) {
45096
- throw new SignatureVerificationError(
45097
- `signature verification threw: ${errorMessage(err)}`
45098
- );
45099
- }
45100
- if (!ok) {
45101
- throw new SignatureVerificationError(
45102
- "signature does not verify against configured verifyPubKey"
45103
- );
45104
- }
45135
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
45105
45136
  }
45106
45137
  const data = parseJson(bodyBytes);
45107
45138
  const rawScore = data.score;
45108
45139
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
45109
45140
  return {
45110
45141
  verdict: normalizeVerdict(data.verdict),
45111
- allow: canonicalAllow(data),
45142
+ allow: this.executable(data),
45112
45143
  actionType: normalizeActionType(data.action_type),
45113
45144
  reason: String(data.reason ?? ""),
45114
45145
  confidence: Number(data.confidence ?? 0),
@@ -45119,7 +45150,8 @@ var Atbash = class _Atbash {
45119
45150
  enforced: Boolean(data.enforced),
45120
45151
  enforcementMode: String(data.enforcement_mode ?? ""),
45121
45152
  status: String(data.status ?? ""),
45122
- score
45153
+ score,
45154
+ refusal: explicitRefusal(data)
45123
45155
  };
45124
45156
  }
45125
45157
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -45152,77 +45184,125 @@ var Atbash = class _Atbash {
45152
45184
  }
45153
45185
  try {
45154
45186
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
45155
- const result = await this.judgeAction(actionText, contextText, {
45156
- toolName,
45157
- toolArgsJson: argsJson,
45158
- orgName: this.orgName,
45159
- // Passed through unredacted — caller vouches for the contents.
45160
- resolved: input.resolved
45161
- });
45162
- if (result.verdict === "No verdict") {
45163
- if (result.status !== "logged") {
45164
- return this.failJudge(
45165
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
45166
- void 0,
45167
- result.toolCallId
45168
- );
45169
- }
45170
- return {
45171
- allow: true,
45172
- verdict: "ALLOW",
45173
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
45174
- toolCallId: result.toolCallId
45175
- };
45176
- }
45177
- const action = result.actionType;
45178
- 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);
45179
45201
  return {
45180
45202
  allow: false,
45181
- verdict: "BLOCK",
45182
- reason: result.reason,
45183
- toolCallId: result.toolCallId
45203
+ verdict: "ERROR",
45204
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
45184
45205
  };
45185
45206
  }
45186
- if (action === "hold_for_user_confirm") {
45187
- return {
45188
- allow: false,
45189
- verdict: "HOLD",
45190
- reason: result.reason || "held for human confirmation",
45191
- toolCallId: result.toolCallId
45192
- };
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
+ );
45193
45261
  }
45194
- if (action === "allow") {
45195
- if (result.verdict === "HOLD") {
45196
- return {
45197
- allow: false,
45198
- verdict: "HOLD",
45199
- reason: result.reason,
45200
- toolCallId: result.toolCallId
45201
- };
45202
- }
45203
- if (result.verdict === "BLOCK") {
45204
- return {
45205
- allow: false,
45206
- verdict: "BLOCK",
45207
- reason: result.reason,
45208
- toolCallId: result.toolCallId
45209
- };
45210
- }
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
+ );
45211
45267
  return {
45212
- allow: true,
45213
- verdict: "ALLOW",
45214
- reason: result.reason,
45215
- 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
45216
45272
  };
45217
45273
  }
45218
- return this.failJudge(
45219
- "unrecognized action_type from judge",
45220
- void 0,
45221
- result.toolCallId
45222
- );
45223
- } catch (err) {
45224
- 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;
45225
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
+ };
45300
+ }
45301
+ return this.failJudge(
45302
+ "unrecognized action_type from judge",
45303
+ void 0,
45304
+ toolCallId
45305
+ );
45226
45306
  }
45227
45307
  /**
45228
45308
  * One exit for every judge failure.
@@ -45234,18 +45314,30 @@ var Atbash = class _Atbash {
45234
45314
  */
45235
45315
  failJudge(reason, cause, toolCallId) {
45236
45316
  const api2 = cause instanceof AtbashAPIError ? cause : null;
45317
+ const userReason = userFacingReason(reason, api2);
45237
45318
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
45319
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
45238
45320
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
45239
45321
  this.logger.warn?.(
45240
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
45322
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
45241
45323
  {
45242
- reason,
45243
- ...api2 ? { status: api2.status, body: api2.body } : {},
45324
+ reason: userReason,
45325
+ ...api2 ? { status: api2.status } : {},
45326
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
45244
45327
  endpoint: this.endpoint,
45245
45328
  ...toolCallId ? { toolCallId } : {}
45246
45329
  }
45247
45330
  );
45248
- 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));
45249
45341
  }
45250
45342
  fail(reason, toolCallId) {
45251
45343
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -45271,11 +45363,16 @@ var Atbash = class _Atbash {
45271
45363
  this.authHeaders(brid)
45272
45364
  );
45273
45365
  await this.raiseIfError(resp);
45274
- 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 : {};
45275
45372
  return {
45276
45373
  status: normalizeStatus(data.status),
45277
45374
  verdict: normalizeVerdict(data.verdict),
45278
- allow: canonicalAllow(data),
45375
+ allow: this.executable(data),
45279
45376
  reason: String(data.reason ?? ""),
45280
45377
  judgmentId: String(data.judgmentId ?? judgmentId),
45281
45378
  onChain: optBool(data.onChain),
@@ -45750,6 +45847,66 @@ var Atbash = class _Atbash {
45750
45847
  return native.containsEvasionCharacters(text);
45751
45848
  }
45752
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
+ }
45753
45910
  function baseToolCall(raw2) {
45754
45911
  return {
45755
45912
  toolCallId: String(raw2.tool_call_id ?? ""),
package/dist/index.d.mts CHANGED
@@ -198,6 +198,11 @@ interface JudgeResult {
198
198
  * missing" — never infer the former from a null verdict alone.
199
199
  */
200
200
  status: string;
201
+ /**
202
+ * The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
203
+ * "HOLD", or "" when it neither refused nor is a known refusal shape.
204
+ */
205
+ refusal?: "" | "BLOCK" | "HOLD";
201
206
  }
202
207
  interface JudgmentStatus {
203
208
  status: JudgmentState;
@@ -305,6 +310,13 @@ interface AtbashLogger {
305
310
  /** Options accepted by the `Atbash` constructor. */
306
311
  interface AtbashOptions {
307
312
  endpoint?: string;
313
+ /**
314
+ * Overall deadline for one `auditToolCall` decision, across every request
315
+ * it makes (org lookup, exists check, judge). Default 60 000 ms. When it
316
+ * passes the decision is an ERROR (denied unless `failClosed` is false).
317
+ * `timeoutMs` still bounds each single request.
318
+ */
319
+ decisionTimeoutMs?: number;
308
320
  timeoutMs?: number;
309
321
  /**
310
322
  * Full chain override — BRID + nodeUrls in one object. Wins over every
@@ -350,6 +362,16 @@ interface AtbashOptions {
350
362
  orgEncryptionPubKey?: string;
351
363
  /** When true (default), `auditToolCall` denies on any error. */
352
364
  failClosed?: boolean;
365
+ /**
366
+ * When true (the default), an answer that carries no enforcement decision
367
+ * (the audit tier's `{status:"logged"}` with no verdict) is refused:
368
+ * `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
369
+ * `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
370
+ * are false. Set it to `false` only to run deliberately in log-only mode
371
+ * (security review 2026-09-30, H6: an unsigned marker switched enforcement
372
+ * off).
373
+ */
374
+ requireEnforcement?: boolean;
353
375
  /**
354
376
  * Verbose diagnostics. Off by default. When on, a failed judge call also
355
377
  * logs the response body, which is the difference between "judge API failed"
@@ -400,6 +422,10 @@ interface FromConfigOptions {
400
422
  /** Default org name — see {@link AtbashOptions.orgName}. */
401
423
  orgName?: string;
402
424
  failClosed?: boolean;
425
+ /** See {@link AtbashOptions.requireEnforcement}. Default true. */
426
+ requireEnforcement?: boolean;
427
+ /** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
428
+ decisionTimeoutMs?: number;
403
429
  /** See {@link AtbashOptions.debug}. */
404
430
  debug?: boolean;
405
431
  logger?: AtbashLogger;
@@ -456,6 +482,10 @@ declare class Atbash {
456
482
  readonly orgEncryptionPubKey?: string;
457
483
  /** When true (default), `auditToolCall` denies on any error. */
458
484
  readonly failClosed: boolean;
485
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
486
+ readonly requireEnforcement: boolean;
487
+ /** Overall deadline for one auditToolCall decision, in ms. */
488
+ readonly decisionTimeoutMs: number;
459
489
  /** Org key learned from the last agent-exists check, for this agent only. */
460
490
  private _orgKeyFromChain;
461
491
  private readonly debug;
@@ -559,6 +589,23 @@ declare class Atbash {
559
589
  * explicitly false.
560
590
  */
561
591
  auditToolCall(input: ToolCallInput): Promise<Decision>;
592
+ /**
593
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
594
+ * may only narrow what the client accepts: on a client pinned to a judge
595
+ * key it must be that same key, never a replacement; on an unpinned client
596
+ * it adds the check.
597
+ */
598
+ private effectiveVerifyKey;
599
+ /**
600
+ * Collapse a judge result into a {@link Decision}. The most restrictive
601
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
602
+ * and the canonical `allow` are each able to deny, and only a result where
603
+ * all three agree may execute. A present `allow` / `decision.allow` that is
604
+ * not exactly `true` is the judge declining to grant permission, so it
605
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
606
+ * never overrides a refusal.
607
+ */
608
+ private decide;
562
609
  /**
563
610
  * One exit for every judge failure.
564
611
  *
@@ -568,6 +615,12 @@ declare class Atbash {
568
615
  * The response body follows only under `debug`, since it can echo the action.
569
616
  */
570
617
  private failJudge;
618
+ /**
619
+ * The canonical permission for a response body under this client's
620
+ * settings: {@link canonicalAllow}, minus the audit tier when
621
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
622
+ */
623
+ private executable;
571
624
  private fail;
572
625
  /**
573
626
  * Return the current status of a previously submitted judgment.