@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.mjs CHANGED
@@ -2930,6 +2930,13 @@ function buildAllowedJudgeHosts() {
2930
2930
  ]);
2931
2931
  }
2932
2932
  var ALLOWED_JUDGE_HOSTS = buildAllowedJudgeHosts();
2933
+ function isAllowlistedJudgeHost(endpoint) {
2934
+ try {
2935
+ return ALLOWED_JUDGE_HOSTS.has(new URL(endpoint).hostname.toLowerCase());
2936
+ } catch {
2937
+ return false;
2938
+ }
2939
+ }
2933
2940
  function validateJudgeEndpoint(judge) {
2934
2941
  const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
2935
2942
  const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
@@ -3198,7 +3205,7 @@ function loadAgentFromFile(keyPath) {
3198
3205
  // src-ts/normalize.ts
3199
3206
  function normalizeVerdict(raw2) {
3200
3207
  if (raw2 === null || raw2 === void 0) return "No verdict";
3201
- const v = String(raw2).toUpperCase();
3208
+ const v = String(raw2).trim().toUpperCase();
3202
3209
  if (v === "ALLOW" || v === "GREEN") return "ALLOW";
3203
3210
  if (v === "HOLD" || v === "YELLOW") return "HOLD";
3204
3211
  if (v === "BLOCK" || v === "RED") return "BLOCK";
@@ -3235,17 +3242,58 @@ function allowSignalsPermit(data) {
3235
3242
  }
3236
3243
  return signals.length === 0 || signals.every((value) => value === true);
3237
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
+ }
3238
3281
  function wireActionType(data) {
3239
- if (data.action_type !== void 0)
3240
- return normalizeActionType(data.action_type);
3241
- return normalizeActionType(data.actionType);
3282
+ for (const source of decisionSources(data)) {
3283
+ const actionType = actionTypeOf(source);
3284
+ if (actionType !== "") return actionType;
3285
+ }
3286
+ return "";
3242
3287
  }
3243
3288
  function isAuditTier(data) {
3244
3289
  const absentVerdict = data.verdict === null || data.verdict === void 0;
3245
3290
  return absentVerdict && data.status === "logged";
3246
3291
  }
3292
+ function isAuditTierBody(data) {
3293
+ return isAuditTier(data);
3294
+ }
3247
3295
  function canonicalAllow(data) {
3248
- if (!allowSignalsPermit(data)) return false;
3296
+ if (explicitRefusal(data) !== "") return false;
3249
3297
  const actionType = wireActionType(data);
3250
3298
  if (actionType && actionType !== "allow") return false;
3251
3299
  if (isAuditTier(data)) return true;
@@ -3487,6 +3535,10 @@ var Atbash = class _Atbash {
3487
3535
  orgEncryptionPubKey;
3488
3536
  /** When true (default), `auditToolCall` denies on any error. */
3489
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;
3490
3542
  /** Org key learned from the last agent-exists check, for this agent only. */
3491
3543
  _orgKeyFromChain = null;
3492
3544
  debug;
@@ -3560,6 +3612,8 @@ var Atbash = class _Atbash {
3560
3612
  this.verifyPubKey = validated.verifyPubKey ?? void 0;
3561
3613
  this.orgEncryptionPubKey = options.orgEncryptionPubKey;
3562
3614
  this.failClosed = options.failClosed !== false;
3615
+ this.requireEnforcement = options.requireEnforcement !== false;
3616
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
3563
3617
  this.debug = options.debug === true;
3564
3618
  this.logger = options.logger ?? {};
3565
3619
  this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
@@ -3570,16 +3624,23 @@ var Atbash = class _Atbash {
3570
3624
  verifying: this.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
3571
3625
  });
3572
3626
  }
3573
- try {
3574
- setupTelemetry({
3575
- enabled: true,
3576
- endpoint: this.endpoint,
3577
- getAuthHeaders: () => this.authHeaders()
3578
- });
3579
- } catch (err) {
3580
- this.logger.warn?.(
3581
- "[atbash] telemetry setup failed \u2014 continuing without metrics",
3582
- { error: String(err) }
3627
+ if (isAllowlistedJudgeHost(this.endpoint)) {
3628
+ try {
3629
+ setupTelemetry({
3630
+ enabled: true,
3631
+ endpoint: this.endpoint,
3632
+ getAuthHeaders: () => this.authHeaders()
3633
+ });
3634
+ } catch (err) {
3635
+ this.logger.warn?.(
3636
+ "[atbash] telemetry setup failed \u2014 continuing without metrics",
3637
+ { error: String(err) }
3638
+ );
3639
+ }
3640
+ } else {
3641
+ this.logger.info?.(
3642
+ "[atbash] telemetry disabled \u2014 the agent bearer is only sent to hosts that already receive it, and this judge endpoint is not one",
3643
+ { endpoint: this.endpoint }
3583
3644
  );
3584
3645
  }
3585
3646
  }
@@ -3643,6 +3704,8 @@ var Atbash = class _Atbash {
3643
3704
  orgName: options.orgName,
3644
3705
  verifyPubKey: validated.verifyPubKey ?? void 0,
3645
3706
  failClosed: options.failClosed,
3707
+ requireEnforcement: options.requireEnforcement,
3708
+ decisionTimeoutMs: options.decisionTimeoutMs,
3646
3709
  // ATBASH_DEBUG lets an operator turn diagnostics on without editing a
3647
3710
  // host's plugin config, which is usually the harder half.
3648
3711
  debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
@@ -3841,34 +3904,16 @@ var Atbash = class _Atbash {
3841
3904
  }
3842
3905
  if (!resp.ok) throw await this.httpError(resp);
3843
3906
  const bodyBytes = Buffer.from(await resp.arrayBuffer());
3844
- const verifyPubKey = options.verifyPubKey ?? this.verifyPubKey;
3907
+ const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
3845
3908
  if (verifyPubKey !== void 0) {
3846
- const sig = resp.headers.get("X-Atbash-Signature");
3847
- if (!sig) {
3848
- throw new SignatureVerificationError(
3849
- "missing X-Atbash-Signature header"
3850
- );
3851
- }
3852
- let ok;
3853
- try {
3854
- ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
3855
- } catch (err) {
3856
- throw new SignatureVerificationError(
3857
- `signature verification threw: ${errorMessage(err)}`
3858
- );
3859
- }
3860
- if (!ok) {
3861
- throw new SignatureVerificationError(
3862
- "signature does not verify against configured verifyPubKey"
3863
- );
3864
- }
3909
+ verifyResponseSignature(bodyBytes, resp, verifyPubKey);
3865
3910
  }
3866
3911
  const data = parseJson(bodyBytes);
3867
3912
  const rawScore = data.score;
3868
3913
  const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
3869
3914
  return {
3870
3915
  verdict: normalizeVerdict(data.verdict),
3871
- allow: canonicalAllow(data),
3916
+ allow: this.executable(data),
3872
3917
  actionType: normalizeActionType(data.action_type),
3873
3918
  reason: String(data.reason ?? ""),
3874
3919
  confidence: Number(data.confidence ?? 0),
@@ -3879,7 +3924,8 @@ var Atbash = class _Atbash {
3879
3924
  enforced: Boolean(data.enforced),
3880
3925
  enforcementMode: String(data.enforcement_mode ?? ""),
3881
3926
  status: String(data.status ?? ""),
3882
- score
3927
+ score,
3928
+ refusal: explicitRefusal(data)
3883
3929
  };
3884
3930
  }
3885
3931
  /* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
@@ -3912,77 +3958,125 @@ var Atbash = class _Atbash {
3912
3958
  }
3913
3959
  try {
3914
3960
  this.logger.info?.("[atbash] judge API called", { tool: toolName });
3915
- const result = await this.judgeAction(actionText, contextText, {
3916
- toolName,
3917
- toolArgsJson: argsJson,
3918
- orgName: this.orgName,
3919
- // Passed through unredacted — caller vouches for the contents.
3920
- resolved: input.resolved
3921
- });
3922
- if (result.verdict === "No verdict") {
3923
- if (result.status !== "logged") {
3924
- return this.failJudge(
3925
- `judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
3926
- void 0,
3927
- result.toolCallId
3928
- );
3929
- }
3930
- return {
3931
- allow: true,
3932
- verdict: "ALLOW",
3933
- reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
3934
- toolCallId: result.toolCallId
3935
- };
3936
- }
3937
- const action = result.actionType;
3938
- 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);
3939
3975
  return {
3940
3976
  allow: false,
3941
- verdict: "BLOCK",
3942
- reason: result.reason,
3943
- toolCallId: result.toolCallId
3977
+ verdict: "ERROR",
3978
+ reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
3944
3979
  };
3945
3980
  }
3946
- if (action === "hold_for_user_confirm") {
3947
- return {
3948
- allow: false,
3949
- verdict: "HOLD",
3950
- reason: result.reason || "held for human confirmation",
3951
- toolCallId: result.toolCallId
3952
- };
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
+ );
3953
4035
  }
3954
- if (action === "allow") {
3955
- if (result.verdict === "HOLD") {
3956
- return {
3957
- allow: false,
3958
- verdict: "HOLD",
3959
- reason: result.reason,
3960
- toolCallId: result.toolCallId
3961
- };
3962
- }
3963
- if (result.verdict === "BLOCK") {
3964
- return {
3965
- allow: false,
3966
- verdict: "BLOCK",
3967
- reason: result.reason,
3968
- toolCallId: result.toolCallId
3969
- };
3970
- }
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
+ );
3971
4041
  return {
3972
- allow: true,
3973
- verdict: "ALLOW",
3974
- reason: result.reason,
3975
- 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
3976
4046
  };
3977
4047
  }
3978
- return this.failJudge(
3979
- "unrecognized action_type from judge",
3980
- void 0,
3981
- result.toolCallId
3982
- );
3983
- } catch (err) {
3984
- 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.");
3985
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
+ };
4074
+ }
4075
+ return this.failJudge(
4076
+ "unrecognized action_type from judge",
4077
+ void 0,
4078
+ toolCallId
4079
+ );
3986
4080
  }
3987
4081
  /**
3988
4082
  * One exit for every judge failure.
@@ -3994,18 +4088,30 @@ var Atbash = class _Atbash {
3994
4088
  */
3995
4089
  failJudge(reason, cause, toolCallId) {
3996
4090
  const api2 = cause instanceof AtbashAPIError ? cause : null;
4091
+ const userReason = userFacingReason(reason, api2);
3997
4092
  const status = api2 ? ` status=${api2.status || "no-response"}` : "";
4093
+ const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
3998
4094
  const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
3999
4095
  this.logger.warn?.(
4000
- `[atbash] judge API failed \u2014${status} reason=${truncate(reason, 300)}${body}`,
4096
+ `[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
4001
4097
  {
4002
- reason,
4003
- ...api2 ? { status: api2.status, body: api2.body } : {},
4098
+ reason: userReason,
4099
+ ...api2 ? { status: api2.status } : {},
4100
+ ...this.debug && api2 ? { body: api2.body, detail: reason } : {},
4004
4101
  endpoint: this.endpoint,
4005
4102
  ...toolCallId ? { toolCallId } : {}
4006
4103
  }
4007
4104
  );
4008
- 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));
4009
4115
  }
4010
4116
  fail(reason, toolCallId) {
4011
4117
  return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
@@ -4031,11 +4137,16 @@ var Atbash = class _Atbash {
4031
4137
  this.authHeaders(brid)
4032
4138
  );
4033
4139
  await this.raiseIfError(resp);
4034
- 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 : {};
4035
4146
  return {
4036
4147
  status: normalizeStatus(data.status),
4037
4148
  verdict: normalizeVerdict(data.verdict),
4038
- allow: canonicalAllow(data),
4149
+ allow: this.executable(data),
4039
4150
  reason: String(data.reason ?? ""),
4040
4151
  judgmentId: String(data.judgmentId ?? judgmentId),
4041
4152
  onChain: optBool(data.onChain),
@@ -4510,6 +4621,66 @@ var Atbash = class _Atbash {
4510
4621
  return native.containsEvasionCharacters(text);
4511
4622
  }
4512
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
+ }
4513
4684
  function baseToolCall(raw2) {
4514
4685
  return {
4515
4686
  toolCallId: String(raw2.tool_call_id ?? ""),
@@ -4723,7 +4894,11 @@ async function scanMemory(entry, auth, opts) {
4723
4894
  const atbash = new Atbash(auth.privkey, {
4724
4895
  endpoint: opts?.endpoint,
4725
4896
  verifyPubKey: opts?.verifyPubKey,
4726
- 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
4727
4902
  });
4728
4903
  const result = await atbash.judgeAction(request.prompt, request.context, {
4729
4904
  toolName: opts?.toolName ?? "memory_write",
@@ -4759,6 +4934,7 @@ async function scanMemory(entry, auth, opts) {
4759
4934
  verdict = "yellow";
4760
4935
  }
4761
4936
  }
4937
+ if (verdict === "green" && result.allow !== true) verdict = "red";
4762
4938
  const parsed = native.parseScoreFromReason(result.reason);
4763
4939
  const score = result.score ?? parsed.score ?? native.defaultScoreForVerdict(verdict);
4764
4940
  return {
@@ -42977,7 +43153,7 @@ async function commitMemoryVersion(plaintext, auth, opts) {
42977
43153
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
42978
43154
  const client = await buildChainClient(chainOpts);
42979
43155
  const { sigProvider } = buildSigner(auth);
42980
- await client.signAndSendUniqueTransaction(
43156
+ const receipt = await client.signAndSendUniqueTransaction(
42981
43157
  {
42982
43158
  name: native.OP_ADD_AGENT_MEMORY,
42983
43159
  args: [
@@ -42990,6 +43166,9 @@ async function commitMemoryVersion(plaintext, auth, opts) {
42990
43166
  },
42991
43167
  sigProvider
42992
43168
  );
43169
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43170
+ throw new Error(`Memory transaction was not confirmed (status=${receipt.status})`);
43171
+ }
42993
43172
  }
42994
43173
  async function decryptRow(row, key3) {
42995
43174
  const parsed = native.parseMemoryRow(gtvToFfiSafe(row));
@@ -43099,13 +43278,16 @@ async function rollbackMemory(toId, reason, auth, opts) {
43099
43278
  const chainOpts = await resolveChainOptsForOrg(opts, auth);
43100
43279
  const client = await buildChainClient(chainOpts);
43101
43280
  const { sigProvider } = buildSigner(auth);
43102
- await client.signAndSendUniqueTransaction(
43281
+ const receipt = await client.signAndSendUniqueTransaction(
43103
43282
  {
43104
43283
  name: native.OP_ROLLBACK_AGENT_MEMORY,
43105
43284
  args: [toGtxBytes(args.agent_pubkey), args.to_id, args.reason]
43106
43285
  },
43107
43286
  sigProvider
43108
43287
  );
43288
+ if (receipt.status !== index.ResponseStatus.Confirmed) {
43289
+ throw new Error(`Memory rollback was not confirmed (status=${receipt.status})`);
43290
+ }
43109
43291
  }
43110
43292
 
43111
43293
  // src-ts/memory/_json_safe.ts
@@ -43244,6 +43426,7 @@ async function guardMemoryWrite(input) {
43244
43426
  endpoint,
43245
43427
  verifyPubKey,
43246
43428
  orgName,
43429
+ chainOpts,
43247
43430
  threshold,
43248
43431
  patterns,
43249
43432
  toolNames,
@@ -43300,23 +43483,41 @@ async function guardMemoryWrite(input) {
43300
43483
  committed: false
43301
43484
  };
43302
43485
  }
43486
+ if (scanResult.verdict !== "green" && scanResult.verdict !== "yellow") {
43487
+ const reason = "Memory scan returned an unrecognized verdict.";
43488
+ return {
43489
+ handled: true,
43490
+ decision: enforce ? { allow: false, block: true, reason } : { allow: true, reason },
43491
+ scanResult,
43492
+ committed: false
43493
+ };
43494
+ }
43303
43495
  const filePath = path3.basename(memEntry.key);
43304
- commitMemoryVersion(memEntry.value, auth, {
43305
- score: scanResult.score,
43306
- filePath,
43307
- orgName,
43308
- endpoint,
43309
- verifyPubKey
43310
- }).catch((err) => {
43496
+ try {
43497
+ await commitMemoryVersion(memEntry.value, auth, {
43498
+ score: scanResult.score,
43499
+ filePath,
43500
+ orgName,
43501
+ endpoint,
43502
+ verifyPubKey,
43503
+ chainOpts
43504
+ });
43505
+ } catch (err) {
43311
43506
  const reason = err instanceof Error ? err.message : String(err);
43312
43507
  logger2?.warn?.("[atbash] memory commit to chain failed", {
43313
43508
  path: memEntry.key,
43314
43509
  filePath,
43315
43510
  reason
43316
43511
  });
43317
- });
43512
+ return {
43513
+ handled: true,
43514
+ decision: enforce ? { allow: false, block: true, reason: `Memory commit failed: ${reason}` } : { allow: true, reason: `Memory commit failed: ${reason}` },
43515
+ scanResult,
43516
+ committed: false
43517
+ };
43518
+ }
43318
43519
  logger2?.info?.(
43319
- scanResult.verdict === "yellow" ? "[atbash] memory HOLD" : "[atbash] memory ALLOW",
43520
+ scanResult.verdict === "yellow" ? "[atbash] memory ALLOW (yellow, low score)" : "[atbash] memory ALLOW",
43320
43521
  {
43321
43522
  path: memEntry.key,
43322
43523
  filePath,
@@ -43349,7 +43550,7 @@ async function syncLocalMemory(auth, pointer, opts = {}) {
43349
43550
  if (withinTtl) {
43350
43551
  return { drifted: false, checked: false, pointer };
43351
43552
  }
43352
- const currentId = await getActiveMemoryId(auth, opts.chainOpts);
43553
+ const currentId = await getActiveMemoryId(auth, opts.chainOpts, opts.filePath);
43353
43554
  const nextPointer = { activeId: currentId, checkedAt: now };
43354
43555
  if (currentId === pointer.activeId) {
43355
43556
  return { drifted: false, checked: true, pointer: nextPointer };
@@ -43431,13 +43632,16 @@ function formatMeta(meta) {
43431
43632
  }
43432
43633
  function createFileLogger(filePath, upstream) {
43433
43634
  let queue = Promise.resolve();
43635
+ let writeError;
43434
43636
  async function ensureDir() {
43435
43637
  await fs2.mkdir(path5.dirname(filePath), { recursive: true });
43436
43638
  }
43437
43639
  function append(level, message, meta) {
43438
43640
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}${formatMeta(meta)}
43439
43641
  `;
43440
- queue = queue.then(ensureDir).then(() => fs2.appendFile(filePath, line, "utf8")).catch(() => {
43642
+ queue = queue.then(ensureDir).then(() => fs2.appendFile(filePath, line, "utf8")).catch((error4) => {
43643
+ writeError = error4 instanceof Error ? error4 : new Error(String(error4));
43644
+ upstream?.warn("[atbash] plugin log write failed", { error: writeError.message });
43441
43645
  });
43442
43646
  }
43443
43647
  return {
@@ -43448,6 +43652,10 @@ function createFileLogger(filePath, upstream) {
43448
43652
  warn(message, meta) {
43449
43653
  upstream?.warn(message, meta ?? {});
43450
43654
  append("warn", message, meta);
43655
+ },
43656
+ async flush() {
43657
+ await queue;
43658
+ if (writeError) throw writeError;
43451
43659
  }
43452
43660
  };
43453
43661
  }
@@ -43537,7 +43745,15 @@ var MemoryGuardManager = class {
43537
43745
  orgName: this.opts.orgName
43538
43746
  });
43539
43747
  const resolved = await atbash.resolveChainForOrg(this.opts.orgName);
43540
- const chainOpts = { ...this.opts.chainOpts, network: resolved.network };
43748
+ if (!await atbash.checkAgentExists(this.agentPubkeyHex, { network: resolved.network })) {
43749
+ throw new Error(`agent is not registered on the resolved ${resolved.network} chain`);
43750
+ }
43751
+ const chainOpts = {
43752
+ ...this.opts.chainOpts,
43753
+ network: resolved.network,
43754
+ blockchainRid: resolved.blockchainRid,
43755
+ nodeUrls: this.opts.chainOpts?.nodeUrls ?? resolved.nodeUrls
43756
+ };
43541
43757
  this._resolvedChainOpts = chainOpts;
43542
43758
  this.logger.info(
43543
43759
  `[atbash] resolved org \u2192 chain \u2014 org=${this.opts.orgName} network=${resolved.network}`
@@ -43546,9 +43762,9 @@ var MemoryGuardManager = class {
43546
43762
  } catch (err) {
43547
43763
  const msg = err instanceof Error ? err.message : String(err);
43548
43764
  this.logger.warn(
43549
- `[atbash] org \u2192 chain resolution failed (falling back to SDK default): ${msg}`
43765
+ `[atbash] org \u2192 chain resolution failed (memory read will be blocked): ${msg}`
43550
43766
  );
43551
- return this.opts.chainOpts;
43767
+ throw err;
43552
43768
  } finally {
43553
43769
  this._resolveChainInflight = void 0;
43554
43770
  }
@@ -43567,13 +43783,18 @@ var MemoryGuardManager = class {
43567
43783
  const result = await syncLocalMemory(this.opts.auth, seed, {
43568
43784
  ttlMs: 0,
43569
43785
  force: true,
43570
- chainOpts
43786
+ chainOpts,
43787
+ filePath: path6.basename(this.memoryFilePath)
43571
43788
  });
43572
43789
  if (!result.drifted && result.pointer.activeId == null) {
43573
43790
  this.logger.info(
43574
43791
  `[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.`
43575
43792
  );
43576
43793
  } else if (result.drifted && result.current) {
43794
+ if (result.current.filePath !== path6.basename(this.memoryFilePath) || result.current.isActive !== true || result.current.decryptError) {
43795
+ this.logger.warn("[atbash] boot sync REFUSED mismatched memory version");
43796
+ return;
43797
+ }
43577
43798
  if (result.current.score < this.rollbackMinScore) {
43578
43799
  this.logger.warn(
43579
43800
  `[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.`
@@ -43587,7 +43808,8 @@ var MemoryGuardManager = class {
43587
43808
  await this.writeMemoryAtomic(result.current.content);
43588
43809
  } catch (err) {
43589
43810
  const msg = err instanceof Error ? err.message : String(err);
43590
- this.logger.warn("[atbash] boot sync write failed (serving whatever's on disk)", { error: msg });
43811
+ this.logger.warn("[atbash] boot sync write failed; pointer remains unchanged", { error: msg });
43812
+ return;
43591
43813
  }
43592
43814
  }
43593
43815
  await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
@@ -43601,9 +43823,9 @@ var MemoryGuardManager = class {
43601
43823
  }
43602
43824
  /**
43603
43825
  * Returns a `HookDecision` when the guard reached a decision about this event.
43604
- * Returns `null` when it did not — either the event isn't memory-related, or it
43605
- * is but the guard could not check it. In both cases the host falls through to
43606
- * its own audit.
43826
+ * Returns `null` for non-memory events, other files this manager does not
43827
+ * own, or unverified observe-mode reads. An enforced read of the managed
43828
+ * file blocks when verification cannot complete.
43607
43829
  *
43608
43830
  * A returned decision carries `audited` (see `HookDecision`). Only
43609
43831
  * `{ allow: true, audited: true }` means "checked and cleared"; anything else
@@ -43639,6 +43861,7 @@ var MemoryGuardManager = class {
43639
43861
  endpoint: this.opts.judgeEndpoint,
43640
43862
  verifyPubKey: this.opts.judgeVerifyPubKey,
43641
43863
  orgName: this.opts.orgName,
43864
+ chainOpts: this.opts.chainOpts,
43642
43865
  patterns: this.opts.memoryPathPatterns,
43643
43866
  toolNames: this.opts.memoryWriteToolNames,
43644
43867
  enforce: this.enforce,
@@ -43683,6 +43906,14 @@ var MemoryGuardManager = class {
43683
43906
  reason: `memory scan returned ${sr2.verdict} but this guard is not enforcing it`
43684
43907
  };
43685
43908
  }
43909
+ if (guard.committed !== true) {
43910
+ return {
43911
+ allow: true,
43912
+ audited: false,
43913
+ verdict: sr2.verdict,
43914
+ reason: "memory scan passed but chain commit did not confirm"
43915
+ };
43916
+ }
43686
43917
  return { allow: true, audited: true, verdict: sr2.verdict };
43687
43918
  }
43688
43919
  /**
@@ -43717,62 +43948,73 @@ var MemoryGuardManager = class {
43717
43948
  return found.every((p) => path6.resolve(p) === managed);
43718
43949
  }
43719
43950
  async handleMemoryRead(event, ctx) {
43951
+ if (!this.vouchesForTarget(event, ctx)) return null;
43720
43952
  const pointer = await this.pointerStore.get(this.agentPubkeyHex);
43721
43953
  let result;
43954
+ let chainOpts;
43722
43955
  try {
43723
- const chainOpts = await this.resolveChainOpts();
43956
+ chainOpts = await this.resolveChainOpts();
43724
43957
  result = await syncLocalMemory(this.opts.auth, pointer, {
43725
43958
  ttlMs: this.ttlMs,
43726
- chainOpts
43959
+ force: this.enforce,
43960
+ chainOpts,
43961
+ filePath: path6.basename(this.memoryFilePath)
43727
43962
  });
43728
43963
  } catch (err) {
43729
43964
  if (err instanceof MemoryIntegrityError) {
43730
43965
  const reason = `Memory integrity check failed on id ${err.id} \u2014 read blocked.`;
43731
43966
  this.logger.warn("[atbash] MEMORY INTEGRITY FAILURE", { id: err.id, error: err.message });
43732
- if (!this.enforce) return null;
43733
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43967
+ return this.unverifiedRead(reason);
43734
43968
  }
43735
43969
  const msg = err instanceof Error ? err.message : String(err);
43736
- this.logger.warn("[atbash] memory sync failed (serving local copy)", { error: msg });
43737
- return null;
43970
+ this.logger.warn("[atbash] memory sync failed", { error: msg });
43971
+ return this.unverifiedRead(`Memory chain sync failed: ${msg}`);
43738
43972
  }
43739
- let onDiskIsCurrent = result.checked;
43740
- if (result.drifted) {
43741
- const fresh = result.current;
43742
- if (fresh) {
43743
- if (fresh.score < this.rollbackMinScore) {
43744
- const reason = `Rolled-back memory version #${fresh.id} scored ${fresh.score} (below threshold ${this.rollbackMinScore}) \u2014 read blocked.`;
43745
- this.logger.warn("[atbash] blocking read on low-score rollback", { id: fresh.id, score: fresh.score });
43746
- if (!this.enforce) return null;
43747
- return { block: true, blockReason: reason, allow: false, reason, audited: true };
43748
- }
43749
- this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", {
43750
- id: fresh.id,
43751
- score: fresh.score
43752
- });
43753
- try {
43754
- await this.writeMemoryAtomic(fresh.content);
43755
- } catch (err) {
43756
- const msg = err instanceof Error ? err.message : String(err);
43757
- this.logger.warn("[atbash] failed to write refreshed memory (serving old)", { error: msg });
43758
- onDiskIsCurrent = false;
43759
- }
43760
- } else {
43761
- this.logger.info("[atbash] active memory removed on chain", { pubkey: this.agentPubkeyHex });
43762
- onDiskIsCurrent = false;
43763
- }
43973
+ if (!result.checked) return this.unverifiedRead("Memory chain state was not checked.");
43974
+ const activeId = result.pointer.activeId;
43975
+ if (activeId === null) {
43976
+ return this.unverifiedRead("No active chain version exists for the managed memory file.");
43764
43977
  }
43765
- if (onDiskIsCurrent) {
43766
- await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43767
- } else {
43768
- this.logger.warn(
43769
- "[atbash] not advancing memory pointer \u2014 local file is stale or revoked; reads stay unaudited until it is refreshed"
43770
- );
43978
+ let active;
43979
+ try {
43980
+ active = result.drifted ? result.current : await getMemoryById(activeId, this.opts.auth, chainOpts);
43981
+ } catch (err) {
43982
+ const msg = err instanceof Error ? err.message : String(err);
43983
+ this.logger.warn("[atbash] active memory row could not be verified", { error: msg });
43984
+ return this.unverifiedRead(`Memory version could not be verified: ${msg}`);
43771
43985
  }
43772
- if (!onDiskIsCurrent) return null;
43773
- if (!this.vouchesForTarget(event, ctx)) return null;
43986
+ const filePath = path6.basename(this.memoryFilePath);
43987
+ if (!active || active.id !== activeId || active.filePath !== filePath || active.isActive !== true || active.decryptError) {
43988
+ return this.unverifiedRead("Active memory version does not match the managed file.");
43989
+ }
43990
+ if (active.score < this.rollbackMinScore) {
43991
+ return this.unverifiedRead(`Memory version #${active.id} is below the minimum safety score.`);
43992
+ }
43993
+ try {
43994
+ if (result.drifted) {
43995
+ this.logger.info("[atbash] memory drift detected \u2014 refreshing local file", { id: active.id });
43996
+ await this.writeMemoryAtomic(active.content);
43997
+ }
43998
+ const onDisk = await fs3.readFile(this.memoryFilePath, "utf8");
43999
+ if (onDisk !== active.content) {
44000
+ return this.unverifiedRead("Local memory bytes do not match the active chain version.");
44001
+ }
44002
+ } catch (err) {
44003
+ const msg = err instanceof Error ? err.message : String(err);
44004
+ this.logger.warn("[atbash] managed memory file could not be verified", { error: msg });
44005
+ return this.unverifiedRead(`Managed memory file could not be verified: ${msg}`);
44006
+ }
44007
+ await this.pointerStore.set(this.agentPubkeyHex, result.pointer);
43774
44008
  return { allow: true, audited: true };
43775
44009
  }
44010
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
44011
+ async flushLogs() {
44012
+ await this.logger.flush();
44013
+ }
44014
+ unverifiedRead(reason) {
44015
+ if (!this.enforce) return null;
44016
+ return { block: true, blockReason: reason, allow: false, reason, audited: true };
44017
+ }
43776
44018
  async writeMemoryAtomic(content) {
43777
44019
  await fs3.mkdir(path6.dirname(this.memoryFilePath), { recursive: true });
43778
44020
  const tmp = `${this.memoryFilePath}.${process.pid}.tmp`;