@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.
package/dist/index.d.ts 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.
package/dist/index.js CHANGED
@@ -3301,7 +3301,7 @@ function loadAgentFromFile(keyPath) {
3301
3301
  // src-ts/normalize.ts
3302
3302
  function normalizeVerdict(raw2) {
3303
3303
  if (raw2 === null || raw2 === void 0) return "No verdict";
3304
- const v = String(raw2).toUpperCase();
3304
+ const v = String(raw2).trim().toUpperCase();
3305
3305
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
3306
3306
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
3307
3307
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -3338,17 +3338,58 @@ function allowSignalsPermit(data) {
3338
3338
  }
3339
3339
  return signals.length === 0 || signals.every((value) => value === true);
3340
3340
  }
3341
+ function explicitRefusal(data) {
3342
+ let hold = false;
3343
+ for (const source of decisionSources(data)) {
3344
+ const verdict = verdictOf(source);
3345
+ if (verdict === "BLOCK" || verdict === "RED") return "BLOCK";
3346
+ if (verdict === "HOLD" || verdict === "YELLOW") hold = true;
3347
+ const actionType = actionTypeOf(source);
3348
+ if (actionType === "block") return "BLOCK";
3349
+ if (actionType === "hold_for_user_confirm") hold = true;
3350
+ }
3351
+ if (!allowSignalsPermit(data)) return "BLOCK";
3352
+ return hold ? "HOLD" : "";
3353
+ }
3354
+ function decisionSources(data) {
3355
+ return [data, isRecord(data.decision) ? data.decision : {}];
3356
+ }
3357
+ function verdictOf(source) {
3358
+ return typeof source.verdict === "string" ? source.verdict.trim().toUpperCase() : "";
3359
+ }
3360
+ function actionTypeRank(actionType) {
3361
+ if (actionType === "block") return 4;
3362
+ if (actionType === "hold_for_user_confirm") return 3;
3363
+ if (actionType === "") return 1;
3364
+ if (actionType === "allow") return 0;
3365
+ return 2;
3366
+ }
3367
+ function actionTypeOf(source) {
3368
+ let winner = "";
3369
+ for (const key3 of ["action_type", "actionType"]) {
3370
+ const raw2 = source[key3];
3371
+ if (raw2 === void 0 || raw2 === null) continue;
3372
+ const actionType = normalizeActionType(raw2);
3373
+ if (actionTypeRank(actionType) > actionTypeRank(winner)) winner = actionType;
3374
+ }
3375
+ return winner;
3376
+ }
3341
3377
  function wireActionType(data) {
3342
- if (data.action_type !== void 0)
3343
- return normalizeActionType(data.action_type);
3344
- return normalizeActionType(data.actionType);
3378
+ for (const source of decisionSources(data)) {
3379
+ const actionType = actionTypeOf(source);
3380
+ if (actionType !== "") return actionType;
3381
+ }
3382
+ return "";
3345
3383
  }
3346
3384
  function isAuditTier(data) {
3347
3385
  const absentVerdict = data.verdict === null || data.verdict === void 0;
3348
3386
  return absentVerdict && data.status === "logged";
3349
3387
  }
3388
+ function isAuditTierBody(data) {
3389
+ return isAuditTier(data);
3390
+ }
3350
3391
  function canonicalAllow(data) {
3351
- if (!allowSignalsPermit(data)) return false;
3392
+ if (explicitRefusal(data) !== "") return false;
3352
3393
  const actionType = wireActionType(data);
3353
3394
  if (actionType && actionType !== "allow") return false;
3354
3395
  if (isAuditTier(data)) return true;
@@ -3578,6 +3619,10 @@ var Atbash = class _Atbash {
3578
3619
  orgEncryptionPubKey;
3579
3620
  /** When true (default), `auditToolCall` denies on any error. */
3580
3621
  failClosed;
3622
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
3623
+ requireEnforcement;
3624
+ /** Overall deadline for one auditToolCall decision, in ms. */
3625
+ decisionTimeoutMs;
3581
3626
  /** Org key learned from the last agent-exists check, for this agent only. */
3582
3627
  _orgKeyFromChain = null;
3583
3628
  debug;
@@ -3651,6 +3696,8 @@ var Atbash = class _Atbash {
3651
3696
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
3652
3697
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
3653
3698
  this.failClosed = options.failClosed !== false;
3699
+ this.requireEnforcement = options.requireEnforcement !== false;
3700
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
3654
3701
  this.debug = options.debug === true;
3655
3702
  this.logger = options.logger ?? {};
3656
3703
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -3741,6 +3788,8 @@ var Atbash = class _Atbash {
3741
3788
  orgName: options.orgName,
3742
3789
  verifyPubKey: validated.verifyPubKey ?? void 0,
3743
3790
  failClosed: options.failClosed,
3791
+ requireEnforcement: options.requireEnforcement,
3792
+ decisionTimeoutMs: options.decisionTimeoutMs,
3744
3793
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
3745
3794
  // host's plugin config, which is usually the harder half.
3746
3795
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -3939,34 +3988,16 @@ var Atbash = class _Atbash {
3939
3988
  }
3940
3989
  if (!resp.ok) throw await this.httpError(resp);
3941
3990
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
3942
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
3991
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
3943
3992
  if (verifyPubKey !== void 0) {
3944
- const sig = resp.headers.get("X-Atbash-Signature");
3945
- if (!sig) {
3946
- throw new SignatureVerificationError(
3947
- "missing X-Atbash-Signature header"
3948
- );
3949
- }
3950
- let ok;
3951
- try {
3952
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
3953
- } catch (err) {
3954
- throw new SignatureVerificationError(
3955
- `signature verification threw: ${errorMessage(err)}`
3956
- );
3957
- }
3958
- if (!ok) {
3959
- throw new SignatureVerificationError(
3960
- "signature does not verify against configured verifyPubKey"
3961
- );
3962
- }
3993
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
3963
3994
  }
3964
3995
  const data = parseJson(bodyBytes);
3965
3996
  const rawScore = data.score;
3966
3997
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
3967
3998
  return {
3968
3999
  verdict: normalizeVerdict(data.verdict),
3969
- allow: canonicalAllow(data),
4000
+ allow: this.executable(data),
3970
4001
  actionType: normalizeActionType(data.action_type),
3971
4002
  reason: String(data.reason ?? ""),
3972
4003
  confidence: Number(data.confidence ?? 0),
@@ -3977,7 +4008,8 @@ var Atbash = class _Atbash {
3977
4008
  enforced: Boolean(data.enforced),
3978
4009
  enforcementMode: String(data.enforcement_mode ?? ""),
3979
4010
  status: String(data.status ?? ""),
3980
- score
4011
+ score,
4012
+ refusal: explicitRefusal(data)
3981
4013
  };
3982
4014
  }
3983
4015
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -4010,77 +4042,125 @@ var Atbash = class _Atbash {
4010
4042
  }
4011
4043
  try {
4012
4044
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
4013
- const result = await this.judgeAction(actionText, contextText, {
4014
- toolName,
4015
- toolArgsJson: argsJson,
4016
- orgName: this.orgName,
4017
- // Passed through unredacted — caller vouches for the contents.
4018
- resolved: input.resolved
4019
- });
4020
- if (result.verdict === "No verdict") {
4021
- if (result.status !== "logged") {
4022
- return this.failJudge(
4023
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
4024
- void 0,
4025
- result.toolCallId
4026
- );
4027
- }
4028
- return {
4029
- allow: true,
4030
- verdict: "ALLOW",
4031
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
4032
- toolCallId: result.toolCallId
4033
- };
4034
- }
4035
- const action = result.actionType;
4036
- if (action === "block") {
4045
+ const result = await withDeadline(
4046
+ this.judgeAction(actionText, contextText, {
4047
+ toolName,
4048
+ toolArgsJson: argsJson,
4049
+ orgName: this.orgName,
4050
+ // Passed through unredacted — caller vouches for the contents.
4051
+ resolved: input.resolved
4052
+ }),
4053
+ this.decisionTimeoutMs
4054
+ );
4055
+ return this.decide(result);
4056
+ } catch (err) {
4057
+ if (err instanceof SignatureVerificationError) {
4058
+ this.failJudge(errorMessage(err), err);
4037
4059
  return {
4038
4060
  allow: false,
4039
- verdict: "BLOCK",
4040
- reason: result.reason,
4041
- toolCallId: result.toolCallId
4061
+ verdict: "ERROR",
4062
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
4042
4063
  };
4043
4064
  }
4044
- if (action === "hold_for_user_confirm") {
4045
- return {
4046
- allow: false,
4047
- verdict: "HOLD",
4048
- reason: result.reason || "held for human confirmation",
4049
- toolCallId: result.toolCallId
4050
- };
4065
+ return this.failJudge(errorMessage(err), err);
4066
+ }
4067
+ }
4068
+ /**
4069
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
4070
+ * may only narrow what the client accepts: on a client pinned to a judge
4071
+ * key it must be that same key, never a replacement; on an unpinned client
4072
+ * it adds the check.
4073
+ */
4074
+ effectiveVerifyKey(perCall) {
4075
+ if (perCall === void 0) return this.verifyPubKey;
4076
+ const key3 = perCall.trim().toLowerCase();
4077
+ if (this.verifyPubKey !== void 0 && key3 !== this.verifyPubKey) {
4078
+ throw new SignatureVerificationError(
4079
+ "a per-call verifyPubKey cannot replace the judge key this client is pinned to"
4080
+ );
4081
+ }
4082
+ return key3;
4083
+ }
4084
+ /**
4085
+ * Collapse a judge result into a {@link Decision}. The most restrictive
4086
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
4087
+ * and the canonical `allow` are each able to deny, and only a result where
4088
+ * all three agree may execute. A present `allow` / `decision.allow` that is
4089
+ * not exactly `true` is the judge declining to grant permission, so it
4090
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
4091
+ * never overrides a refusal.
4092
+ */
4093
+ decide(result) {
4094
+ const toolCallId = result.toolCallId;
4095
+ const deny = (verdict, reason) => ({
4096
+ allow: false,
4097
+ verdict,
4098
+ reason,
4099
+ toolCallId
4100
+ });
4101
+ const notGranted = "the judge response did not grant permission";
4102
+ const held = deny("HOLD", result.reason || "held for human confirmation");
4103
+ const blocked = deny("BLOCK", result.reason || notGranted);
4104
+ if (result.refusal === "BLOCK") return blocked;
4105
+ if (result.verdict === "No verdict") {
4106
+ if (result.actionType === "block") {
4107
+ return blocked;
4108
+ }
4109
+ if (result.actionType === "hold_for_user_confirm") {
4110
+ return held;
4111
+ }
4112
+ if (result.refusal === "HOLD") return held;
4113
+ if (result.status !== "logged") {
4114
+ return this.failJudge(
4115
+ `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
4116
+ void 0,
4117
+ toolCallId
4118
+ );
4051
4119
  }
4052
- if (action === "allow") {
4053
- if (result.verdict === "HOLD") {
4054
- return {
4055
- allow: false,
4056
- verdict: "HOLD",
4057
- reason: result.reason,
4058
- toolCallId: result.toolCallId
4059
- };
4060
- }
4061
- if (result.verdict === "BLOCK") {
4062
- return {
4063
- allow: false,
4064
- verdict: "BLOCK",
4065
- reason: result.reason,
4066
- toolCallId: result.toolCallId
4067
- };
4068
- }
4120
+ if (this.requireEnforcement) {
4121
+ this.logger.warn?.(
4122
+ "[atbash] judge did not enforce this action (audit tier) and requireEnforcement is on: denied",
4123
+ { toolCallId }
4124
+ );
4069
4125
  return {
4070
- allow: true,
4071
- verdict: "ALLOW",
4072
- reason: result.reason,
4073
- toolCallId: result.toolCallId
4126
+ allow: false,
4127
+ verdict: "ERROR",
4128
+ 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.",
4129
+ toolCallId
4074
4130
  };
4075
4131
  }
4076
- return this.failJudge(
4077
- "unrecognized action_type from judge",
4078
- void 0,
4079
- result.toolCallId
4080
- );
4081
- } catch (err) {
4082
- return this.failJudge(errorMessage(err), err);
4132
+ if (result.allow !== true) return deny("BLOCK", notGranted);
4133
+ return {
4134
+ allow: true,
4135
+ verdict: "ALLOW",
4136
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
4137
+ toolCallId
4138
+ };
4139
+ }
4140
+ const action = result.actionType;
4141
+ if (action === "block") {
4142
+ return deny("BLOCK", result.reason || "Blocked by your organisation's Atbash policy.");
4143
+ }
4144
+ if (action === "hold_for_user_confirm") {
4145
+ return held;
4146
+ }
4147
+ if (result.refusal === "HOLD") return held;
4148
+ if (action === "allow") {
4149
+ if (result.verdict === "HOLD") return held;
4150
+ if (result.verdict === "BLOCK") return blocked;
4151
+ if (result.allow !== true) return deny("BLOCK", notGranted);
4152
+ return {
4153
+ allow: true,
4154
+ verdict: "ALLOW",
4155
+ reason: result.reason,
4156
+ toolCallId
4157
+ };
4083
4158
  }
4159
+ return this.failJudge(
4160
+ "unrecognized action_type from judge",
4161
+ void 0,
4162
+ toolCallId
4163
+ );
4084
4164
  }
4085
4165
  /**
4086
4166
  * One exit for every judge failure.
@@ -4092,18 +4172,30 @@ var Atbash = class _Atbash {
4092
4172
  */
4093
4173
  failJudge(reason, cause, toolCallId) {
4094
4174
  const api2 = cause instanceof AtbashAPIError ? cause : null;
4175
+ const userReason = userFacingReason(reason, api2);
4095
4176
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
4177
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
4096
4178
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
4097
4179
  this.logger.warn?.(
4098
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
4180
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
4099
4181
  {
4100
- reason,
4101
- ...api2 ? { status: api2.status, body: api2.body } : {},
4182
+ reason: userReason,
4183
+ ...api2 ? { status: api2.status } : {},
4184
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
4102
4185
  endpoint: this.endpoint,
4103
4186
  ...toolCallId ? { toolCallId } : {}
4104
4187
  }
4105
4188
  );
4106
- return this.fail(reason, toolCallId);
4189
+ return this.fail(userReason, toolCallId);
4190
+ }
4191
+ /**
4192
+ * The canonical permission for a response body under this client's
4193
+ * settings: {@link canonicalAllow}, minus the audit tier when
4194
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
4195
+ */
4196
+ executable(data) {
4197
+ if (!canonicalAllow(data)) return false;
4198
+ return !(this.requireEnforcement && isAuditTierBody(data));
4107
4199
  }
4108
4200
  fail(reason, toolCallId) {
4109
4201
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -4129,11 +4221,16 @@ var Atbash = class _Atbash {
4129
4221
  this.authHeaders(brid)
4130
4222
  );
4131
4223
  await this.raiseIfError(resp);
4132
- const data = await this.json(resp) ?? {};
4224
+ const bytes = Buffer.from(await resp.arrayBuffer());
4225
+ if (this.verifyPubKey !== void 0) {
4226
+ verifyResponseSignature(bytes, resp, this.verifyPubKey);
4227
+ }
4228
+ const parsed = tryParseJson(bytes.toString("utf-8"));
4229
+ const data = isRecord2(parsed) ? parsed : {};
4133
4230
  return {
4134
4231
  status: normalizeStatus(data.status),
4135
4232
  verdict: normalizeVerdict(data.verdict),
4136
- allow: canonicalAllow(data),
4233
+ allow: this.executable(data),
4137
4234
  reason: String(data.reason ?? ""),
4138
4235
  judgmentId: String(data.judgmentId ?? judgmentId),
4139
4236
  onChain: optBool(data.onChain),
@@ -4608,6 +4705,66 @@ var Atbash = class _Atbash {
4608
4705
  return native.containsEvasionCharacters(text);
4609
4706
  }
4610
4707
  };
4708
+ function userFacingReason(reason, api2) {
4709
+ if (api2 && api2.status > 0) {
4710
+ const where = ` (HTTP ${api2.status}${api2.path ? ` from ${api2.path}` : ""})`;
4711
+ const b = api2.body.toLowerCase();
4712
+ if (b.includes("agent not registered")) {
4713
+ return `Agent not registered with Atbash: onboard it in the Atbash dashboard${where}.`;
4714
+ }
4715
+ if (b.includes("jailed")) {
4716
+ return `This agent is jailed; unjail it in the Atbash dashboard${where}.`;
4717
+ }
4718
+ if (b.includes("agent has no policy") || b.includes("no policy configured")) {
4719
+ return `This agent has no policy; attach one in the Atbash dashboard${where}.`;
4720
+ }
4721
+ if (b.includes("audit tier") || b.includes("verdict disabled") || b.includes("verdict not supported")) {
4722
+ return `The organisation's plan does not include checked decisions${where}.`;
4723
+ }
4724
+ return `The Atbash judge answered with an error${where}, so the action was not approved.`;
4725
+ }
4726
+ const text = api2 ? api2.body || reason : reason;
4727
+ return truncate(text.replace(/https?:\/\/[^\s)]+/g, "the judge"), 300);
4728
+ }
4729
+ var DEFAULT_DECISION_TIMEOUT_MS = 6e4;
4730
+ async function withDeadline(p, ms) {
4731
+ let timer;
4732
+ const deadline = new Promise((_, reject) => {
4733
+ timer = setTimeout(
4734
+ () => reject(
4735
+ new Error(
4736
+ `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.`
4737
+ )
4738
+ ),
4739
+ ms
4740
+ );
4741
+ });
4742
+ p.catch(() => void 0);
4743
+ try {
4744
+ return await Promise.race([p, deadline]);
4745
+ } finally {
4746
+ clearTimeout(timer);
4747
+ }
4748
+ }
4749
+ function verifyResponseSignature(bodyBytes, resp, verifyPubKey) {
4750
+ const sig = resp.headers.get("X-Atbash-Signature");
4751
+ if (!sig) {
4752
+ throw new SignatureVerificationError("missing X-Atbash-Signature header");
4753
+ }
4754
+ let ok;
4755
+ try {
4756
+ ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
4757
+ } catch (err) {
4758
+ throw new SignatureVerificationError(
4759
+ `signature verification threw: ${errorMessage(err)}`
4760
+ );
4761
+ }
4762
+ if (!ok) {
4763
+ throw new SignatureVerificationError(
4764
+ "signature does not verify against configured verifyPubKey"
4765
+ );
4766
+ }
4767
+ }
4611
4768
  function baseToolCall(raw2) {
4612
4769
  return {
4613
4770
  toolCallId: String(raw2.tool_call_id ?? ""),
@@ -4821,7 +4978,11 @@ async function scanMemory(entry, auth, opts) {
4821
4978
  const atbash = new Atbash(auth.privkey, {
4822
4979
  endpoint: opts?.endpoint,
4823
4980
  verifyPubKey: opts?.verifyPubKey,
4824
- orgName: opts?.orgName
4981
+ orgName: opts?.orgName,
4982
+ // Memory scans keep their existing audit-tier semantics (an audit-tier
4983
+ // scan is not a defect the scanner invents). Whether an unenforced scan
4984
+ // may commit memory is the memory-guard PR's decision, not H6's.
4985
+ requireEnforcement: false
4825
4986
  });
4826
4987
  const result = await atbash.judgeAction(request.prompt, request.context, {
4827
4988
  toolName: opts?.toolName ?? "memory_write",
@@ -4857,6 +5018,7 @@ async function scanMemory(entry, auth, opts) {
4857
5018
  verdict = "yellow";
4858
5019
  }
4859
5020
  }
5021
+ if (verdict === "green" && result.allow !== true) verdict = "red";
4860
5022
  const parsed = native.parseScoreFromReason(result.reason);
4861
5023
  const score = result.score ?? parsed.score ?? native.defaultScoreForVerdict(verdict);
4862
5024
  return {