@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/index.js CHANGED
@@ -3026,6 +3026,13 @@ function buildAllowedJudgeHosts() {
3026
3026
  ]);
3027
3027
  }
3028
3028
  var ALLOWED_JUDGE_HOSTS = buildAllowedJudgeHosts();
3029
+ function isAllowlistedJudgeHost(endpoint) {
3030
+ try {
3031
+ return ALLOWED_JUDGE_HOSTS.has(new URL(endpoint).hostname.toLowerCase());
3032
+ } catch {
3033
+ return false;
3034
+ }
3035
+ }
3029
3036
  function validateJudgeEndpoint(judge) {
3030
3037
  const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
3031
3038
  const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
@@ -3294,7 +3301,7 @@ function loadAgentFromFile(keyPath) {
3294
3301
  // src-ts/normalize.ts
3295
3302
  function normalizeVerdict(raw2) {
3296
3303
  if (raw2 === null || raw2 === void 0) return "No verdict";
3297
- const v = String(raw2).toUpperCase();
3304
+ const v = String(raw2).trim().toUpperCase();
3298
3305
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
3299
3306
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
3300
3307
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -3331,17 +3338,58 @@ function allowSignalsPermit(data) {
3331
3338
  }
3332
3339
  return signals.length === 0 || signals.every((value) => value === true);
3333
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
+ }
3334
3377
  function wireActionType(data) {
3335
- if (data.action_type !== void 0)
3336
- return normalizeActionType(data.action_type);
3337
- return normalizeActionType(data.actionType);
3378
+ for (const source of decisionSources(data)) {
3379
+ const actionType = actionTypeOf(source);
3380
+ if (actionType !== "") return actionType;
3381
+ }
3382
+ return "";
3338
3383
  }
3339
3384
  function isAuditTier(data) {
3340
3385
  const absentVerdict = data.verdict === null || data.verdict === void 0;
3341
3386
  return absentVerdict && data.status === "logged";
3342
3387
  }
3388
+ function isAuditTierBody(data) {
3389
+ return isAuditTier(data);
3390
+ }
3343
3391
  function canonicalAllow(data) {
3344
- if (!allowSignalsPermit(data)) return false;
3392
+ if (explicitRefusal(data) !== "") return false;
3345
3393
  const actionType = wireActionType(data);
3346
3394
  if (actionType && actionType !== "allow") return false;
3347
3395
  if (isAuditTier(data)) return true;
@@ -3571,6 +3619,10 @@ var Atbash = class _Atbash {
3571
3619
  orgEncryptionPubKey;
3572
3620
  /** When true (default), `auditToolCall` denies on any error. */
3573
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;
3574
3626
  /** Org key learned from the last agent-exists check, for this agent only. */
3575
3627
  _orgKeyFromChain = null;
3576
3628
  debug;
@@ -3644,6 +3696,8 @@ var Atbash = class _Atbash {
3644
3696
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
3645
3697
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
3646
3698
  this.failClosed = options.failClosed !== false;
3699
+ this.requireEnforcement = options.requireEnforcement !== false;
3700
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
3647
3701
  this.debug = options.debug === true;
3648
3702
  this.logger = options.logger ?? {};
3649
3703
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -3654,16 +3708,23 @@ var Atbash = class _Atbash {
3654
3708
  verifying: this.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
3655
3709
  });
3656
3710
  }
3657
- try {
3658
- setupTelemetry({
3659
- enabled: true,
3660
- endpoint: this.endpoint,
3661
- getAuthHeaders: () => this.authHeaders()
3662
- });
3663
- } catch (err) {
3664
- this.logger.warn?.(
3665
- "[atbash] telemetry setup failed \u2014 continuing without metrics",
3666
- { error: String(err) }
3711
+ if (isAllowlistedJudgeHost(this.endpoint)) {
3712
+ try {
3713
+ setupTelemetry({
3714
+ enabled: true,
3715
+ endpoint: this.endpoint,
3716
+ getAuthHeaders: () => this.authHeaders()
3717
+ });
3718
+ } catch (err) {
3719
+ this.logger.warn?.(
3720
+ "[atbash] telemetry setup failed \u2014 continuing without metrics",
3721
+ { error: String(err) }
3722
+ );
3723
+ }
3724
+ } else {
3725
+ this.logger.info?.(
3726
+ "[atbash] telemetry disabled \u2014 the agent bearer is only sent to hosts that already receive it, and this judge endpoint is not one",
3727
+ { endpoint: this.endpoint }
3667
3728
  );
3668
3729
  }
3669
3730
  }
@@ -3727,6 +3788,8 @@ var Atbash = class _Atbash {
3727
3788
  orgName: options.orgName,
3728
3789
  verifyPubKey: validated.verifyPubKey ?? void 0,
3729
3790
  failClosed: options.failClosed,
3791
+ requireEnforcement: options.requireEnforcement,
3792
+ decisionTimeoutMs: options.decisionTimeoutMs,
3730
3793
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
3731
3794
  // host's plugin config, which is usually the harder half.
3732
3795
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -3925,34 +3988,16 @@ var Atbash = class _Atbash {
3925
3988
  }
3926
3989
  if (!resp.ok) throw await this.httpError(resp);
3927
3990
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
3928
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
3991
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
3929
3992
  if (verifyPubKey !== void 0) {
3930
- const sig = resp.headers.get("X-Atbash-Signature");
3931
- if (!sig) {
3932
- throw new SignatureVerificationError(
3933
- "missing X-Atbash-Signature header"
3934
- );
3935
- }
3936
- let ok;
3937
- try {
3938
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
3939
- } catch (err) {
3940
- throw new SignatureVerificationError(
3941
- `signature verification threw: ${errorMessage(err)}`
3942
- );
3943
- }
3944
- if (!ok) {
3945
- throw new SignatureVerificationError(
3946
- "signature does not verify against configured verifyPubKey"
3947
- );
3948
- }
3993
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
3949
3994
  }
3950
3995
  const data = parseJson(bodyBytes);
3951
3996
  const rawScore = data.score;
3952
3997
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
3953
3998
  return {
3954
3999
  verdict: normalizeVerdict(data.verdict),
3955
- allow: canonicalAllow(data),
4000
+ allow: this.executable(data),
3956
4001
  actionType: normalizeActionType(data.action_type),
3957
4002
  reason: String(data.reason ?? ""),
3958
4003
  confidence: Number(data.confidence ?? 0),
@@ -3963,7 +4008,8 @@ var Atbash = class _Atbash {
3963
4008
  enforced: Boolean(data.enforced),
3964
4009
  enforcementMode: String(data.enforcement_mode ?? ""),
3965
4010
  status: String(data.status ?? ""),
3966
- score
4011
+ score,
4012
+ refusal: explicitRefusal(data)
3967
4013
  };
3968
4014
  }
3969
4015
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -3996,77 +4042,125 @@ var Atbash = class _Atbash {
3996
4042
  }
3997
4043
  try {
3998
4044
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
3999
- const result = await this.judgeAction(actionText, contextText, {
4000
- toolName,
4001
- toolArgsJson: argsJson,
4002
- orgName: this.orgName,
4003
- // Passed through unredacted — caller vouches for the contents.
4004
- resolved: input.resolved
4005
- });
4006
- if (result.verdict === "No verdict") {
4007
- if (result.status !== "logged") {
4008
- return this.failJudge(
4009
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
4010
- void 0,
4011
- result.toolCallId
4012
- );
4013
- }
4014
- return {
4015
- allow: true,
4016
- verdict: "ALLOW",
4017
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
4018
- toolCallId: result.toolCallId
4019
- };
4020
- }
4021
- const action = result.actionType;
4022
- 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);
4023
4059
  return {
4024
4060
  allow: false,
4025
- verdict: "BLOCK",
4026
- reason: result.reason,
4027
- toolCallId: result.toolCallId
4061
+ verdict: "ERROR",
4062
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
4028
4063
  };
4029
4064
  }
4030
- if (action === "hold_for_user_confirm") {
4031
- return {
4032
- allow: false,
4033
- verdict: "HOLD",
4034
- reason: result.reason || "held for human confirmation",
4035
- toolCallId: result.toolCallId
4036
- };
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
+ );
4037
4119
  }
4038
- if (action === "allow") {
4039
- if (result.verdict === "HOLD") {
4040
- return {
4041
- allow: false,
4042
- verdict: "HOLD",
4043
- reason: result.reason,
4044
- toolCallId: result.toolCallId
4045
- };
4046
- }
4047
- if (result.verdict === "BLOCK") {
4048
- return {
4049
- allow: false,
4050
- verdict: "BLOCK",
4051
- reason: result.reason,
4052
- toolCallId: result.toolCallId
4053
- };
4054
- }
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
+ );
4055
4125
  return {
4056
- allow: true,
4057
- verdict: "ALLOW",
4058
- reason: result.reason,
4059
- 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
4060
4130
  };
4061
4131
  }
4062
- return this.failJudge(
4063
- "unrecognized action_type from judge",
4064
- void 0,
4065
- result.toolCallId
4066
- );
4067
- } catch (err) {
4068
- 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.");
4069
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
+ };
4158
+ }
4159
+ return this.failJudge(
4160
+ "unrecognized action_type from judge",
4161
+ void 0,
4162
+ toolCallId
4163
+ );
4070
4164
  }
4071
4165
  /**
4072
4166
  * One exit for every judge failure.
@@ -4078,18 +4172,30 @@ var Atbash = class _Atbash {
4078
4172
  */
4079
4173
  failJudge(reason, cause, toolCallId) {
4080
4174
  const api2 = cause instanceof AtbashAPIError ? cause : null;
4175
+ const userReason = userFacingReason(reason, api2);
4081
4176
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
4177
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
4082
4178
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
4083
4179
  this.logger.warn?.(
4084
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
4180
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
4085
4181
  {
4086
- reason,
4087
- ...api2 ? { status: api2.status, body: api2.body } : {},
4182
+ reason: userReason,
4183
+ ...api2 ? { status: api2.status } : {},
4184
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
4088
4185
  endpoint: this.endpoint,
4089
4186
  ...toolCallId ? { toolCallId } : {}
4090
4187
  }
4091
4188
  );
4092
- 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));
4093
4199
  }
4094
4200
  fail(reason, toolCallId) {
4095
4201
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -4115,11 +4221,16 @@ var Atbash = class _Atbash {
4115
4221
  this.authHeaders(brid)
4116
4222
  );
4117
4223
  await this.raiseIfError(resp);
4118
- 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 : {};
4119
4230
  return {
4120
4231
  status: normalizeStatus(data.status),
4121
4232
  verdict: normalizeVerdict(data.verdict),
4122
- allow: canonicalAllow(data),
4233
+ allow: this.executable(data),
4123
4234
  reason: String(data.reason ?? ""),
4124
4235
  judgmentId: String(data.judgmentId ?? judgmentId),
4125
4236
  onChain: optBool(data.onChain),
@@ -4594,6 +4705,66 @@ var Atbash = class _Atbash {
4594
4705
  return native.containsEvasionCharacters(text);
4595
4706
  }
4596
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
+ }
4597
4768
  function baseToolCall(raw2) {
4598
4769
  return {
4599
4770
  toolCallId: String(raw2.tool_call_id ?? ""),
@@ -4807,7 +4978,11 @@ async function scanMemory(entry, auth, opts) {
4807
4978
  const atbash = new Atbash(auth.privkey, {
4808
4979
  endpoint: opts?.endpoint,
4809
4980
  verifyPubKey: opts?.verifyPubKey,
4810
- 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
4811
4986
  });
4812
4987
  const result = await atbash.judgeAction(request.prompt, request.context, {
4813
4988
  toolName: opts?.toolName ?? "memory_write",
@@ -4843,6 +5018,7 @@ async function scanMemory(entry, auth, opts) {
4843
5018
  verdict = "yellow";
4844
5019
  }
4845
5020
  }
5021
+ if (verdict === "green" && result.allow !== true) verdict = "red";
4846
5022
  const parsed = native.parseScoreFromReason(result.reason);
4847
5023
  const score = result.score ?? parsed.score ?? native.defaultScoreForVerdict(verdict);
4848
5024
  return {
@@ -43061,7 +43237,7 @@ async function commitMemoryVersion(plaintext, auth, opts) {
43061
43237
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43062
43238
  const client = await buildChainClient(chainOpts);
43063
43239
  const { sigProvider } = buildSigner(auth);
43064
- await client.signAndSendUniqueTransaction(
43240
+ const receipt = await client.signAndSendUniqueTransaction(
43065
43241
  {
43066
43242
  name: native.OP_ADD_AGENT_MEMORY,
43067
43243
  args: [
@@ -43074,6 +43250,9 @@ async function commitMemoryVersion(plaintext, auth, opts) {
43074
43250
  },
43075
43251
  sigProvider
43076
43252
  );
43253
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43254
+ throw new Error(`Memory transaction was not confirmed (status=${receipt.status})`);
43255
+ }
43077
43256
  }
43078
43257
  async function decryptRow(row, key3) {
43079
43258
  const parsed = native.parseMemoryRow(gtvToFfiSafe(row));
@@ -43183,13 +43362,16 @@ async function rollbackMemory(toId, reason, auth, opts) {
43183
43362
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43184
43363
  const client = await buildChainClient(chainOpts);
43185
43364
  const { sigProvider } = buildSigner(auth);
43186
- await client.signAndSendUniqueTransaction(
43365
+ const receipt = await client.signAndSendUniqueTransaction(
43187
43366
  {
43188
43367
  name: native.OP_ROLLBACK_AGENT_MEMORY,
43189
43368
  args: [toGtxBytes(args.agent_pubkey), args.to_id, args.reason]
43190
43369
  },
43191
43370
  sigProvider
43192
43371
  );
43372
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43373
+ throw new Error(`Memory rollback was not confirmed (status=${receipt.status})`);
43374
+ }
43193
43375
  }
43194
43376
 
43195
43377
  // src-ts/memory/_json_safe.ts
@@ -43328,6 +43510,7 @@ async function guardMemoryWrite(input) {
43328
43510
  endpoint,
43329
43511
  verifyPubKey,
43330
43512
  orgName,
43513
+ chainOpts,
43331
43514
  threshold,
43332
43515
  patterns,
43333
43516
  toolNames,
@@ -43384,23 +43567,41 @@ async function guardMemoryWrite(input) {
43384
43567
  committed: false
43385
43568
  };
43386
43569
  }
43570
+ if (scanResult.verdict !== "green" && scanResult.verdict !== "yellow") {
43571
+ const reason = "Memory scan returned an unrecognized verdict.";
43572
+ return {
43573
+ handled: true,
43574
+ decision: enforce ? { allow: false, block: true, reason } : { allow: true, reason },
43575
+ scanResult,
43576
+ committed: false
43577
+ };
43578
+ }
43387
43579
  const filePath = import_node_path4.default.basename(memEntry.key);
43388
- commitMemoryVersion(memEntry.value, auth, {
43389
- score: scanResult.score,
43390
- filePath,
43391
- orgName,
43392
- endpoint,
43393
- verifyPubKey
43394
- }).catch((err) => {
43580
+ try {
43581
+ await commitMemoryVersion(memEntry.value, auth, {
43582
+ score: scanResult.score,
43583
+ filePath,
43584
+ orgName,
43585
+ endpoint,
43586
+ verifyPubKey,
43587
+ chainOpts
43588
+ });
43589
+ } catch (err) {
43395
43590
  const reason = err instanceof Error ? err.message : String(err);
43396
43591
  logger2?.warn?.("[atbash] memory commit to chain failed", {
43397
43592
  path: memEntry.key,
43398
43593
  filePath,
43399
43594
  reason
43400
43595
  });
43401
- });
43596
+ return {
43597
+ handled: true,
43598
+ decision: enforce ? { allow: false, block: true, reason: `Memory commit failed: ${reason}` } : { allow: true, reason: `Memory commit failed: ${reason}` },
43599
+ scanResult,
43600
+ committed: false
43601
+ };
43602
+ }
43402
43603
  logger2?.info?.(
43403
- scanResult.verdict === "yellow" ? "[atbash] memory HOLD" : "[atbash] memory ALLOW",
43604
+ scanResult.verdict === "yellow" ? "[atbash] memory ALLOW (yellow, low score)" : "[atbash] memory ALLOW",
43404
43605
  {
43405
43606
  path: memEntry.key,
43406
43607
  filePath,
@@ -43433,7 +43634,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
43433
43634
  if (withinTtl) {
43434
43635
  return { drifted: false, checked: false, pointer };
43435
43636
  }
43436
- const currentId = await getActiveMemoryId(auth, opts.chainOpts);
43637
+ const currentId = await getActiveMemoryId(auth, opts.chainOpts, opts.filePath);
43437
43638
  const nextPointer = { activeId: currentId, checkedAt: now };
43438
43639
  if (currentId === pointer.activeId) {
43439
43640
  return { drifted: false, checked: true, pointer: nextPointer };
@@ -43515,13 +43716,16 @@ function formatMeta(meta) {
43515
43716
  }
43516
43717
  function createFileLogger(filePath, upstream) {
43517
43718
  let queue = Promise.resolve();
43719
+ let writeError;
43518
43720
  async function ensureDir() {
43519
43721
  await import_node_fs5.promises.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
43520
43722
  }
43521
43723
  function append(level, message, meta) {
43522
43724
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
43523
43725
  `;
43524
- queue = queue.then(ensureDir).then(() => import_node_fs5.promises.appendFile(filePath, line, "utf8")).catch(() => {
43726
+ queue = queue.then(ensureDir).then(() => import_node_fs5.promises.appendFile(filePath, line, "utf8")).catch((error4) => {
43727
+ writeError = error4 instanceof Error ? error4 : new Error(String(error4));
43728
+ upstream?.warn("[atbash] plugin log write failed", { error: writeError.message });
43525
43729
  });
43526
43730
  }
43527
43731
  return {
@@ -43532,6 +43736,10 @@ function createFileLogger(filePath, upstream) {
43532
43736
  warn(message, meta) {
43533
43737
  upstream?.warn(message, meta ?? {});
43534
43738
  append("warn", message, meta);
43739
+ },
43740
+ async flush() {
43741
+ await queue;
43742
+ if (writeError) throw writeError;
43535
43743
  }
43536
43744
  };
43537
43745
  }
@@ -43621,7 +43829,15 @@ var MemoryGuardManager = class {
43621
43829
  orgName: this.opts.orgName
43622
43830
  });
43623
43831
  const resolved = await atbash.resolveChainForOrg(this.opts.orgName);
43624
- const chainOpts = { ...this.opts.chainOpts, network: resolved.network };
43832
+ if (!await atbash.checkAgentExists(this.agentPubkeyHex, { network: resolved.network })) {
43833
+ throw new Error(`agent is not registered on the resolved ${resolved.network} chain`);
43834
+ }
43835
+ const chainOpts = {
43836
+ ...this.opts.chainOpts,
43837
+ network: resolved.network,
43838
+ blockchainRid: resolved.blockchainRid,
43839
+ nodeUrls: this.opts.chainOpts?.nodeUrls ?? resolved.nodeUrls
43840
+ };
43625
43841
  this._resolvedChainOpts = chainOpts;
43626
43842
  this.logger.info(
43627
43843
  `[atbash] resolved org \u2192 chain \u2014 org=${this.opts.orgName} network=${resolved.network}`
@@ -43630,9 +43846,9 @@ var MemoryGuardManager = class {
43630
43846
  } catch (err) {
43631
43847
  const msg = err instanceof Error ? err.message : String(err);
43632
43848
  this.logger.warn(
43633
- `[atbash] org \u2192 chain resolution failed (falling back to SDK default): ${msg}`
43849
+ `[atbash] org \u2192 chain resolution failed (memory read will be blocked): ${msg}`
43634
43850
  );
43635
- return this.opts.chainOpts;
43851
+ throw err;
43636
43852
  } finally {
43637
43853
  this._resolveChainInflight = void 0;
43638
43854
  }
@@ -43651,13 +43867,18 @@ var MemoryGuardManager = class {
43651
43867
  const result = await syncLocalMemory(this.opts.auth, seed, {
43652
43868
  ttlMs: 0,
43653
43869
  force: true,
43654
- chainOpts
43870
+ chainOpts,
43871
+ filePath: import_node_path7.default.basename(this.memoryFilePath)
43655
43872
  });
43656
43873
  if (!result.drifted && result.pointer.activeId == null) {
43657
43874
  this.logger.info(
43658
43875
  `[atbash] no active memory on chain for agent=${this.agentPubkeyHex.slice(0, 16)}\u2026 org=${this.opts.orgName ?? "(none)"} \u2014 either the agent isn't registered on this chain or hasn't written any memory. Sync will remain a no-op until a write lands.`
43659
43876
  );
43660
43877
  } else if (result.drifted && result.current) {
43878
+ if (result.current.filePath !== import_node_path7.default.basename(this.memoryFilePath) || result.current.isActive !== true || result.current.decryptError) {
43879
+ this.logger.warn("[atbash] boot sync REFUSED mismatched memory version");
43880
+ return;
43881
+ }
43661
43882
  if (result.current.score < this.rollbackMinScore) {
43662
43883
  this.logger.warn(
43663
43884
  `[atbash] boot sync REFUSED refresh \u2014 id=${result.current.id} score=${result.current.score} below threshold ${this.rollbackMinScore}. Leaving MEMORY.md and pointer untouched; next memory read will be blocked.`
@@ -43671,7 +43892,8 @@ var MemoryGuardManager = class {
43671
43892
  await this.writeMemoryAtomic(result.current.content);
43672
43893
  } catch (err) {
43673
43894
  const msg = err instanceof Error ? err.message : String(err);
43674
- this.logger.warn("[atbash] boot sync write failed (serving whatever's on disk)", { error: msg });
43895
+ this.logger.warn("[atbash] boot sync write failed; pointer remains unchanged", { error: msg });
43896
+ return;
43675
43897
  }
43676
43898
  }
43677
43899
  await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
@@ -43685,9 +43907,9 @@ var MemoryGuardManager = class {
43685
43907
  }
43686
43908
  /**
43687
43909
  * Returns a `HookDecision` when the guard reached a decision about this event.
43688
- * Returns `null` when it did not — either the event isn't memory-related, or it
43689
- * is but the guard could not check it. In both cases the host falls through to
43690
- * its own audit.
43910
+ * Returns `null` for non-memory events, other files this manager does not
43911
+ * own, or unverified observe-mode reads. An enforced read of the managed
43912
+ * file blocks when verification cannot complete.
43691
43913
  *
43692
43914
  * A returned decision carries `audited` (see `HookDecision`). Only
43693
43915
  * `{ allow: true, audited: true }` means "checked and cleared"; anything else
@@ -43723,6 +43945,7 @@ var MemoryGuardManager = class {
43723
43945
  endpoint: this.opts.judgeEndpoint,
43724
43946
  verifyPubKey: this.opts.judgeVerifyPubKey,
43725
43947
  orgName: this.opts.orgName,
43948
+ chainOpts: this.opts.chainOpts,
43726
43949
  patterns: this.opts.memoryPathPatterns,
43727
43950
  toolNames: this.opts.memoryWriteToolNames,
43728
43951
  enforce: this.enforce,
@@ -43767,6 +43990,14 @@ var MemoryGuardManager = class {
43767
43990
  reason: `memory scan returned ${sr2.verdict} but this guard is not enforcing it`
43768
43991
  };
43769
43992
  }
43993
+ if (guard.committed !== true) {
43994
+ return {
43995
+ allow: true,
43996
+ audited: false,
43997
+ verdict: sr2.verdict,
43998
+ reason: "memory scan passed but chain commit did not confirm"
43999
+ };
44000
+ }
43770
44001
  return { allow: true, audited: true, verdict: sr2.verdict };
43771
44002
  }
43772
44003
  /**
@@ -43801,62 +44032,73 @@ var MemoryGuardManager = class {
43801
44032
  return found.every((p) => import_node_path7.default.resolve(p) === managed);
43802
44033
  }
43803
44034
  async handleMemoryRead(event, ctx) {
44035
+ if (!this.vouchesForTarget(event, ctx)) return null;
43804
44036
  const pointer = await this.pointerStore.get(this.agentPubkeyHex);
43805
44037
  let result;
44038
+ let chainOpts;
43806
44039
  try {
43807
- const chainOpts = await this.resolveChainOpts();
44040
+ chainOpts = await this.resolveChainOpts();
43808
44041
  result = await syncLocalMemory(this.opts.auth, pointer, {
43809
44042
  ttlMs: this.ttlMs,
43810
- chainOpts
44043
+ force: this.enforce,
44044
+ chainOpts,
44045
+ filePath: import_node_path7.default.basename(this.memoryFilePath)
43811
44046
  });
43812
44047
  } catch (err) {
43813
44048
  if (err instanceof MemoryIntegrityError) {
43814
44049
  const reason = `Memory integrity check failed on id ${err.id} \u2014 read blocked.`;
43815
44050
  this.logger.warn("[atbash] MEMORY INTEGRITY FAILURE", { id: err.id, error: err.message });
43816
- if (!this.enforce) return null;
43817
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
44051
+ return this.unverifiedRead(reason);
43818
44052
  }
43819
44053
  const msg = err instanceof Error ? err.message : String(err);
43820
- this.logger.warn("[atbash] memory sync failed (serving local copy)", { error: msg });
43821
- return null;
44054
+ this.logger.warn("[atbash] memory sync failed", { error: msg });
44055
+ return this.unverifiedRead(`Memory chain sync failed: ${msg}`);
43822
44056
  }
43823
- let onDiskIsCurrent = result.checked;
43824
- if (result.drifted) {
43825
- const fresh = result.current;
43826
- if (fresh) {
43827
- if (fresh.score < this.rollbackMinScore) {
43828
- const reason = `Rolled-back memory version #${fresh.id} scored ${fresh.score} (below threshold ${this.rollbackMinScore}) \u2014 read blocked.`;
43829
- this.logger.warn("[atbash] blocking read on low-score rollback", { id: fresh.id, score: fresh.score });
43830
- if (!this.enforce) return null;
43831
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43832
- }
43833
- this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", {
43834
- id: fresh.id,
43835
- score: fresh.score
43836
- });
43837
- try {
43838
- await this.writeMemoryAtomic(fresh.content);
43839
- } catch (err) {
43840
- const msg = err instanceof Error ? err.message : String(err);
43841
- this.logger.warn("[atbash] failed to write refreshed memory (serving old)", { error: msg });
43842
- onDiskIsCurrent = false;
43843
- }
43844
- } else {
43845
- this.logger.info("[atbash] active memory removed on chain", { pubkey: this.agentPubkeyHex });
43846
- onDiskIsCurrent = false;
43847
- }
44057
+ if (!result.checked) return this.unverifiedRead("Memory chain state was not checked.");
44058
+ const activeId = result.pointer.activeId;
44059
+ if (activeId === null) {
44060
+ return this.unverifiedRead("No active chain version exists for the managed memory file.");
43848
44061
  }
43849
- if (onDiskIsCurrent) {
43850
- await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43851
- } else {
43852
- this.logger.warn(
43853
- "[atbash] not advancing memory pointer \u2014 local file is stale or revoked; reads stay unaudited until it is refreshed"
43854
- );
44062
+ let active;
44063
+ try {
44064
+ active = result.drifted ? result.current : await getMemoryById(activeId, this.opts.auth, chainOpts);
44065
+ } catch (err) {
44066
+ const msg = err instanceof Error ? err.message : String(err);
44067
+ this.logger.warn("[atbash] active memory row could not be verified", { error: msg });
44068
+ return this.unverifiedRead(`Memory version could not be verified: ${msg}`);
43855
44069
  }
43856
- if (!onDiskIsCurrent) return null;
43857
- if (!this.vouchesForTarget(event, ctx)) return null;
44070
+ const filePath = import_node_path7.default.basename(this.memoryFilePath);
44071
+ if (!active || active.id !== activeId || active.filePath !== filePath || active.isActive !== true || active.decryptError) {
44072
+ return this.unverifiedRead("Active memory version does not match the managed file.");
44073
+ }
44074
+ if (active.score < this.rollbackMinScore) {
44075
+ return this.unverifiedRead(`Memory version #${active.id} is below the minimum safety score.`);
44076
+ }
44077
+ try {
44078
+ if (result.drifted) {
44079
+ this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", { id: active.id });
44080
+ await this.writeMemoryAtomic(active.content);
44081
+ }
44082
+ const onDisk = await import_node_fs6.promises.readFile(this.memoryFilePath, "utf8");
44083
+ if (onDisk !== active.content) {
44084
+ return this.unverifiedRead("Local memory bytes do not match the active chain version.");
44085
+ }
44086
+ } catch (err) {
44087
+ const msg = err instanceof Error ? err.message : String(err);
44088
+ this.logger.warn("[atbash] managed memory file could not be verified", { error: msg });
44089
+ return this.unverifiedRead(`Managed memory file could not be verified: ${msg}`);
44090
+ }
44091
+ await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43858
44092
  return { allow: true, audited: true };
43859
44093
  }
44094
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
44095
+ async flushLogs() {
44096
+ await this.logger.flush();
44097
+ }
44098
+ unverifiedRead(reason) {
44099
+ if (!this.enforce) return null;
44100
+ return { block: true, blockReason: reason, allow: false, reason, audited: true };
44101
+ }
43860
44102
  async writeMemoryAtomic(content) {
43861
44103
  await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(this.memoryFilePath), { recursive: true });
43862
44104
  const tmp = `${this.memoryFilePath}.${process.pid}.tmp`;