@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.mjs CHANGED
@@ -3205,7 +3205,7 @@ function loadAgentFromFile(keyPath) {
3205
3205
  // src-ts/normalize.ts
3206
3206
  function normalizeVerdict(raw2) {
3207
3207
  if (raw2 === null || raw2 === void 0) return "No verdict";
3208
- const v = String(raw2).toUpperCase();
3208
+ const v = String(raw2).trim().toUpperCase();
3209
3209
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
3210
3210
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
3211
3211
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -3242,17 +3242,58 @@ function allowSignalsPermit(data) {
3242
3242
  }
3243
3243
  return signals.length === 0 || signals.every((value) => value === true);
3244
3244
  }
3245
+ function explicitRefusal(data) {
3246
+ let hold = false;
3247
+ for (const source of decisionSources(data)) {
3248
+ const verdict = verdictOf(source);
3249
+ if (verdict === "BLOCK" || verdict === "RED") return "BLOCK";
3250
+ if (verdict === "HOLD" || verdict === "YELLOW") hold = true;
3251
+ const actionType = actionTypeOf(source);
3252
+ if (actionType === "block") return "BLOCK";
3253
+ if (actionType === "hold_for_user_confirm") hold = true;
3254
+ }
3255
+ if (!allowSignalsPermit(data)) return "BLOCK";
3256
+ return hold ? "HOLD" : "";
3257
+ }
3258
+ function decisionSources(data) {
3259
+ return [data, isRecord(data.decision) ? data.decision : {}];
3260
+ }
3261
+ function verdictOf(source) {
3262
+ return typeof source.verdict === "string" ? source.verdict.trim().toUpperCase() : "";
3263
+ }
3264
+ function actionTypeRank(actionType) {
3265
+ if (actionType === "block") return 4;
3266
+ if (actionType === "hold_for_user_confirm") return 3;
3267
+ if (actionType === "") return 1;
3268
+ if (actionType === "allow") return 0;
3269
+ return 2;
3270
+ }
3271
+ function actionTypeOf(source) {
3272
+ let winner = "";
3273
+ for (const key3 of ["action_type", "actionType"]) {
3274
+ const raw2 = source[key3];
3275
+ if (raw2 === void 0 || raw2 === null) continue;
3276
+ const actionType = normalizeActionType(raw2);
3277
+ if (actionTypeRank(actionType) > actionTypeRank(winner)) winner = actionType;
3278
+ }
3279
+ return winner;
3280
+ }
3245
3281
  function wireActionType(data) {
3246
- if (data.action_type !== void 0)
3247
- return normalizeActionType(data.action_type);
3248
- return normalizeActionType(data.actionType);
3282
+ for (const source of decisionSources(data)) {
3283
+ const actionType = actionTypeOf(source);
3284
+ if (actionType !== "") return actionType;
3285
+ }
3286
+ return "";
3249
3287
  }
3250
3288
  function isAuditTier(data) {
3251
3289
  const absentVerdict = data.verdict === null || data.verdict === void 0;
3252
3290
  return absentVerdict && data.status === "logged";
3253
3291
  }
3292
+ function isAuditTierBody(data) {
3293
+ return isAuditTier(data);
3294
+ }
3254
3295
  function canonicalAllow(data) {
3255
- if (!allowSignalsPermit(data)) return false;
3296
+ if (explicitRefusal(data) !== "") return false;
3256
3297
  const actionType = wireActionType(data);
3257
3298
  if (actionType && actionType !== "allow") return false;
3258
3299
  if (isAuditTier(data)) return true;
@@ -3494,6 +3535,10 @@ var Atbash = class _Atbash {
3494
3535
  orgEncryptionPubKey;
3495
3536
  /** When true (default), `auditToolCall` denies on any error. */
3496
3537
  failClosed;
3538
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
3539
+ requireEnforcement;
3540
+ /** Overall deadline for one auditToolCall decision, in ms. */
3541
+ decisionTimeoutMs;
3497
3542
  /** Org key learned from the last agent-exists check, for this agent only. */
3498
3543
  _orgKeyFromChain = null;
3499
3544
  debug;
@@ -3567,6 +3612,8 @@ var Atbash = class _Atbash {
3567
3612
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
3568
3613
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
3569
3614
  this.failClosed = options.failClosed !== false;
3615
+ this.requireEnforcement = options.requireEnforcement !== false;
3616
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
3570
3617
  this.debug = options.debug === true;
3571
3618
  this.logger = options.logger ?? {};
3572
3619
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -3657,6 +3704,8 @@ var Atbash = class _Atbash {
3657
3704
  orgName: options.orgName,
3658
3705
  verifyPubKey: validated.verifyPubKey ?? void 0,
3659
3706
  failClosed: options.failClosed,
3707
+ requireEnforcement: options.requireEnforcement,
3708
+ decisionTimeoutMs: options.decisionTimeoutMs,
3660
3709
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
3661
3710
  // host's plugin config, which is usually the harder half.
3662
3711
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -3855,34 +3904,16 @@ var Atbash = class _Atbash {
3855
3904
  }
3856
3905
  if (!resp.ok) throw await this.httpError(resp);
3857
3906
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
3858
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
3907
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
3859
3908
  if (verifyPubKey !== void 0) {
3860
- const sig = resp.headers.get("X-Atbash-Signature");
3861
- if (!sig) {
3862
- throw new SignatureVerificationError(
3863
- "missing X-Atbash-Signature header"
3864
- );
3865
- }
3866
- let ok;
3867
- try {
3868
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
3869
- } catch (err) {
3870
- throw new SignatureVerificationError(
3871
- `signature verification threw: ${errorMessage(err)}`
3872
- );
3873
- }
3874
- if (!ok) {
3875
- throw new SignatureVerificationError(
3876
- "signature does not verify against configured verifyPubKey"
3877
- );
3878
- }
3909
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
3879
3910
  }
3880
3911
  const data = parseJson(bodyBytes);
3881
3912
  const rawScore = data.score;
3882
3913
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
3883
3914
  return {
3884
3915
  verdict: normalizeVerdict(data.verdict),
3885
- allow: canonicalAllow(data),
3916
+ allow: this.executable(data),
3886
3917
  actionType: normalizeActionType(data.action_type),
3887
3918
  reason: String(data.reason ?? ""),
3888
3919
  confidence: Number(data.confidence ?? 0),
@@ -3893,7 +3924,8 @@ var Atbash = class _Atbash {
3893
3924
  enforced: Boolean(data.enforced),
3894
3925
  enforcementMode: String(data.enforcement_mode ?? ""),
3895
3926
  status: String(data.status ?? ""),
3896
- score
3927
+ score,
3928
+ refusal: explicitRefusal(data)
3897
3929
  };
3898
3930
  }
3899
3931
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -3926,77 +3958,125 @@ var Atbash = class _Atbash {
3926
3958
  }
3927
3959
  try {
3928
3960
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
3929
- const result = await this.judgeAction(actionText, contextText, {
3930
- toolName,
3931
- toolArgsJson: argsJson,
3932
- orgName: this.orgName,
3933
- // Passed through unredacted — caller vouches for the contents.
3934
- resolved: input.resolved
3935
- });
3936
- if (result.verdict === "No verdict") {
3937
- if (result.status !== "logged") {
3938
- return this.failJudge(
3939
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
3940
- void 0,
3941
- result.toolCallId
3942
- );
3943
- }
3944
- return {
3945
- allow: true,
3946
- verdict: "ALLOW",
3947
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
3948
- toolCallId: result.toolCallId
3949
- };
3950
- }
3951
- const action = result.actionType;
3952
- if (action === "block") {
3961
+ const result = await withDeadline(
3962
+ this.judgeAction(actionText, contextText, {
3963
+ toolName,
3964
+ toolArgsJson: argsJson,
3965
+ orgName: this.orgName,
3966
+ // Passed through unredacted — caller vouches for the contents.
3967
+ resolved: input.resolved
3968
+ }),
3969
+ this.decisionTimeoutMs
3970
+ );
3971
+ return this.decide(result);
3972
+ } catch (err) {
3973
+ if (err instanceof SignatureVerificationError) {
3974
+ this.failJudge(errorMessage(err), err);
3953
3975
  return {
3954
3976
  allow: false,
3955
- verdict: "BLOCK",
3956
- reason: result.reason,
3957
- toolCallId: result.toolCallId
3977
+ verdict: "ERROR",
3978
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
3958
3979
  };
3959
3980
  }
3960
- if (action === "hold_for_user_confirm") {
3961
- return {
3962
- allow: false,
3963
- verdict: "HOLD",
3964
- reason: result.reason || "held for human confirmation",
3965
- toolCallId: result.toolCallId
3966
- };
3981
+ return this.failJudge(errorMessage(err), err);
3982
+ }
3983
+ }
3984
+ /**
3985
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
3986
+ * may only narrow what the client accepts: on a client pinned to a judge
3987
+ * key it must be that same key, never a replacement; on an unpinned client
3988
+ * it adds the check.
3989
+ */
3990
+ effectiveVerifyKey(perCall) {
3991
+ if (perCall === void 0) return this.verifyPubKey;
3992
+ const key3 = perCall.trim().toLowerCase();
3993
+ if (this.verifyPubKey !== void 0 && key3 !== this.verifyPubKey) {
3994
+ throw new SignatureVerificationError(
3995
+ "a per-call verifyPubKey cannot replace the judge key this client is pinned to"
3996
+ );
3997
+ }
3998
+ return key3;
3999
+ }
4000
+ /**
4001
+ * Collapse a judge result into a {@link Decision}. The most restrictive
4002
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
4003
+ * and the canonical `allow` are each able to deny, and only a result where
4004
+ * all three agree may execute. A present `allow` / `decision.allow` that is
4005
+ * not exactly `true` is the judge declining to grant permission, so it
4006
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
4007
+ * never overrides a refusal.
4008
+ */
4009
+ decide(result) {
4010
+ const toolCallId = result.toolCallId;
4011
+ const deny = (verdict, reason) => ({
4012
+ allow: false,
4013
+ verdict,
4014
+ reason,
4015
+ toolCallId
4016
+ });
4017
+ const notGranted = "the judge response did not grant permission";
4018
+ const held = deny("HOLD", result.reason || "held for human confirmation");
4019
+ const blocked = deny("BLOCK", result.reason || notGranted);
4020
+ if (result.refusal === "BLOCK") return blocked;
4021
+ if (result.verdict === "No verdict") {
4022
+ if (result.actionType === "block") {
4023
+ return blocked;
4024
+ }
4025
+ if (result.actionType === "hold_for_user_confirm") {
4026
+ return held;
4027
+ }
4028
+ if (result.refusal === "HOLD") return held;
4029
+ if (result.status !== "logged") {
4030
+ return this.failJudge(
4031
+ `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
4032
+ void 0,
4033
+ toolCallId
4034
+ );
3967
4035
  }
3968
- if (action === "allow") {
3969
- if (result.verdict === "HOLD") {
3970
- return {
3971
- allow: false,
3972
- verdict: "HOLD",
3973
- reason: result.reason,
3974
- toolCallId: result.toolCallId
3975
- };
3976
- }
3977
- if (result.verdict === "BLOCK") {
3978
- return {
3979
- allow: false,
3980
- verdict: "BLOCK",
3981
- reason: result.reason,
3982
- toolCallId: result.toolCallId
3983
- };
3984
- }
4036
+ if (this.requireEnforcement) {
4037
+ this.logger.warn?.(
4038
+ "[atbash] judge did not enforce this action (audit tier) and requireEnforcement is on: denied",
4039
+ { toolCallId }
4040
+ );
3985
4041
  return {
3986
- allow: true,
3987
- verdict: "ALLOW",
3988
- reason: result.reason,
3989
- toolCallId: result.toolCallId
4042
+ allow: false,
4043
+ verdict: "ERROR",
4044
+ 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.",
4045
+ toolCallId
3990
4046
  };
3991
4047
  }
3992
- return this.failJudge(
3993
- "unrecognized action_type from judge",
3994
- void 0,
3995
- result.toolCallId
3996
- );
3997
- } catch (err) {
3998
- return this.failJudge(errorMessage(err), err);
4048
+ if (result.allow !== true) return deny("BLOCK", notGranted);
4049
+ return {
4050
+ allow: true,
4051
+ verdict: "ALLOW",
4052
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
4053
+ toolCallId
4054
+ };
4055
+ }
4056
+ const action = result.actionType;
4057
+ if (action === "block") {
4058
+ return deny("BLOCK", result.reason || "Blocked by your organisation's Atbash policy.");
4059
+ }
4060
+ if (action === "hold_for_user_confirm") {
4061
+ return held;
4062
+ }
4063
+ if (result.refusal === "HOLD") return held;
4064
+ if (action === "allow") {
4065
+ if (result.verdict === "HOLD") return held;
4066
+ if (result.verdict === "BLOCK") return blocked;
4067
+ if (result.allow !== true) return deny("BLOCK", notGranted);
4068
+ return {
4069
+ allow: true,
4070
+ verdict: "ALLOW",
4071
+ reason: result.reason,
4072
+ toolCallId
4073
+ };
3999
4074
  }
4075
+ return this.failJudge(
4076
+ "unrecognized action_type from judge",
4077
+ void 0,
4078
+ toolCallId
4079
+ );
4000
4080
  }
4001
4081
  /**
4002
4082
  * One exit for every judge failure.
@@ -4008,18 +4088,30 @@ var Atbash = class _Atbash {
4008
4088
  */
4009
4089
  failJudge(reason, cause, toolCallId) {
4010
4090
  const api2 = cause instanceof AtbashAPIError ? cause : null;
4091
+ const userReason = userFacingReason(reason, api2);
4011
4092
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
4093
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
4012
4094
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
4013
4095
  this.logger.warn?.(
4014
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
4096
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
4015
4097
  {
4016
- reason,
4017
- ...api2 ? { status: api2.status, body: api2.body } : {},
4098
+ reason: userReason,
4099
+ ...api2 ? { status: api2.status } : {},
4100
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
4018
4101
  endpoint: this.endpoint,
4019
4102
  ...toolCallId ? { toolCallId } : {}
4020
4103
  }
4021
4104
  );
4022
- return this.fail(reason, toolCallId);
4105
+ return this.fail(userReason, toolCallId);
4106
+ }
4107
+ /**
4108
+ * The canonical permission for a response body under this client's
4109
+ * settings: {@link canonicalAllow}, minus the audit tier when
4110
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
4111
+ */
4112
+ executable(data) {
4113
+ if (!canonicalAllow(data)) return false;
4114
+ return !(this.requireEnforcement && isAuditTierBody(data));
4023
4115
  }
4024
4116
  fail(reason, toolCallId) {
4025
4117
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -4045,11 +4137,16 @@ var Atbash = class _Atbash {
4045
4137
  this.authHeaders(brid)
4046
4138
  );
4047
4139
  await this.raiseIfError(resp);
4048
- const data = await this.json(resp) ?? {};
4140
+ const bytes = Buffer.from(await resp.arrayBuffer());
4141
+ if (this.verifyPubKey !== void 0) {
4142
+ verifyResponseSignature(bytes, resp, this.verifyPubKey);
4143
+ }
4144
+ const parsed = tryParseJson(bytes.toString("utf-8"));
4145
+ const data = isRecord2(parsed) ? parsed : {};
4049
4146
  return {
4050
4147
  status: normalizeStatus(data.status),
4051
4148
  verdict: normalizeVerdict(data.verdict),
4052
- allow: canonicalAllow(data),
4149
+ allow: this.executable(data),
4053
4150
  reason: String(data.reason ?? ""),
4054
4151
  judgmentId: String(data.judgmentId ?? judgmentId),
4055
4152
  onChain: optBool(data.onChain),
@@ -4524,6 +4621,66 @@ var Atbash = class _Atbash {
4524
4621
  return native.containsEvasionCharacters(text);
4525
4622
  }
4526
4623
  };
4624
+ function userFacingReason(reason, api2) {
4625
+ if (api2 && api2.status > 0) {
4626
+ const where = ` (HTTP ${api2.status}${api2.path ? ` from ${api2.path}` : ""})`;
4627
+ const b = api2.body.toLowerCase();
4628
+ if (b.includes("agent not registered")) {
4629
+ return `Agent not registered with Atbash: onboard it in the Atbash dashboard${where}.`;
4630
+ }
4631
+ if (b.includes("jailed")) {
4632
+ return `This agent is jailed; unjail it in the Atbash dashboard${where}.`;
4633
+ }
4634
+ if (b.includes("agent has no policy") || b.includes("no policy configured")) {
4635
+ return `This agent has no policy; attach one in the Atbash dashboard${where}.`;
4636
+ }
4637
+ if (b.includes("audit tier") || b.includes("verdict disabled") || b.includes("verdict not supported")) {
4638
+ return `The organisation's plan does not include checked decisions${where}.`;
4639
+ }
4640
+ return `The Atbash judge answered with an error${where}, so the action was not approved.`;
4641
+ }
4642
+ const text = api2 ? api2.body || reason : reason;
4643
+ return truncate(text.replace(/https?:\/\/[^\s)]+/g, "the judge"), 300);
4644
+ }
4645
+ var DEFAULT_DECISION_TIMEOUT_MS = 6e4;
4646
+ async function withDeadline(p, ms) {
4647
+ let timer;
4648
+ const deadline = new Promise((_, reject) => {
4649
+ timer = setTimeout(
4650
+ () => reject(
4651
+ new Error(
4652
+ `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.`
4653
+ )
4654
+ ),
4655
+ ms
4656
+ );
4657
+ });
4658
+ p.catch(() => void 0);
4659
+ try {
4660
+ return await Promise.race([p, deadline]);
4661
+ } finally {
4662
+ clearTimeout(timer);
4663
+ }
4664
+ }
4665
+ function verifyResponseSignature(bodyBytes, resp, verifyPubKey) {
4666
+ const sig = resp.headers.get("X-Atbash-Signature");
4667
+ if (!sig) {
4668
+ throw new SignatureVerificationError("missing X-Atbash-Signature header");
4669
+ }
4670
+ let ok;
4671
+ try {
4672
+ ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
4673
+ } catch (err) {
4674
+ throw new SignatureVerificationError(
4675
+ `signature verification threw: ${errorMessage(err)}`
4676
+ );
4677
+ }
4678
+ if (!ok) {
4679
+ throw new SignatureVerificationError(
4680
+ "signature does not verify against configured verifyPubKey"
4681
+ );
4682
+ }
4683
+ }
4527
4684
  function baseToolCall(raw2) {
4528
4685
  return {
4529
4686
  toolCallId: String(raw2.tool_call_id ?? ""),
@@ -4737,7 +4894,11 @@ async function scanMemory(entry, auth, opts) {
4737
4894
  const atbash = new Atbash(auth.privkey, {
4738
4895
  endpoint: opts?.endpoint,
4739
4896
  verifyPubKey: opts?.verifyPubKey,
4740
- orgName: opts?.orgName
4897
+ orgName: opts?.orgName,
4898
+ // Memory scans keep their existing audit-tier semantics (an audit-tier
4899
+ // scan is not a defect the scanner invents). Whether an unenforced scan
4900
+ // may commit memory is the memory-guard PR's decision, not H6's.
4901
+ requireEnforcement: false
4741
4902
  });
4742
4903
  const result = await atbash.judgeAction(request.prompt, request.context, {
4743
4904
  toolName: opts?.toolName ?? "memory_write",
@@ -4773,6 +4934,7 @@ async function scanMemory(entry, auth, opts) {
4773
4934
  verdict = "yellow";
4774
4935
  }
4775
4936
  }
4937
+ if (verdict === "green" && result.allow !== true) verdict = "red";
4776
4938
  const parsed = native.parseScoreFromReason(result.reason);
4777
4939
  const score = result.score ?? parsed.score ?? native.defaultScoreForVerdict(verdict);
4778
4940
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/sdk",
3
- "version": "0.16.6-dev.0",
3
+ "version": "0.17.0-dev.0",
4
4
  "description": "TypeScript SDK for Atbash — the safety layer that evaluates AI agent actions against operator-defined policies before execution.",
5
5
  "keywords": [
6
6
  "atbash",
@@ -90,10 +90,10 @@
90
90
  "typescript-eslint": "^8.62.1"
91
91
  },
92
92
  "optionalDependencies": {
93
- "@atbash/sdk-linux-x64-gnu": "0.16.6-dev.0",
94
- "@atbash/sdk-linux-arm64-gnu": "0.16.6-dev.0",
95
- "@atbash/sdk-linux-x64-musl": "0.16.6-dev.0",
96
- "@atbash/sdk-darwin-arm64": "0.16.6-dev.0",
97
- "@atbash/sdk-win32-x64-msvc": "0.16.6-dev.0"
93
+ "@atbash/sdk-linux-x64-gnu": "0.17.0-dev.0",
94
+ "@atbash/sdk-linux-arm64-gnu": "0.17.0-dev.0",
95
+ "@atbash/sdk-linux-x64-musl": "0.17.0-dev.0",
96
+ "@atbash/sdk-darwin-arm64": "0.17.0-dev.0",
97
+ "@atbash/sdk-win32-x64-msvc": "0.17.0-dev.0"
98
98
  }
99
99
  }