@atbash/sdk 0.14.1-dev.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import nodePath from 'node:path';
2
+ import { Logger } from '@opentelemetry/api-logs';
3
+
1
4
  type JudgeEndpointConfig = {
2
5
  policy?: "default";
3
6
  endpoint?: string;
@@ -28,6 +31,31 @@ interface ValidatedEndpoint {
28
31
  * @internal
29
32
  */
30
33
  declare function buildAllowedJudgeHosts(): ReadonlySet<string>;
34
+ /**
35
+ * Pin keys for a loopback judge, so tests can drive the pinned-host path
36
+ * against a local server.
37
+ *
38
+ * Only loopback hosts read this, and their traffic never leaves the machine,
39
+ * so it cannot loosen the check on a real Atbash host.
40
+ *
41
+ * @internal
42
+ * @param keys - Compressed pubkeys to pin; `[]` clears them.
43
+ */
44
+ declare function setLoopbackJudgePinsForTests(keys: readonly string[]): void;
45
+ /**
46
+ * The keys a judge reply must be signed by, for one endpoint.
47
+ *
48
+ * An Atbash host's pinned keys always decide: a configured `verifyPubKey` on
49
+ * such a host must be one of them, or the client refuses to start, because a
50
+ * key that only had to come from config could be anyone's. A host with no pins
51
+ * uses the configured key, or none.
52
+ *
53
+ * @param endpoint - The validated judge endpoint URL.
54
+ * @param configuredKey - The `verifyPubKey` the caller configured, if any.
55
+ * @returns The accepted keys, current first; empty means replies are not checked.
56
+ * @throws When `configuredKey` is not one of the host's pinned keys.
57
+ */
58
+ declare function judgeVerifyKeys(endpoint: string, configuredKey?: string): readonly string[];
31
59
  declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
32
60
 
33
61
  interface ChainConfig {
@@ -35,8 +63,12 @@ interface ChainConfig {
35
63
  readonly blockchainRid: string;
36
64
  readonly nodeUrls: readonly string[];
37
65
  }
38
- declare const PUBLIC_CHAIN: ChainConfig;
39
66
  declare const PRIVATE_CHAIN: ChainConfig;
67
+ /**
68
+ * The chain a network name selects. Only `"private"` exists; any other value,
69
+ * including `"public"` from an untyped caller, is refused with the same
70
+ * message the config switch gives, never routed to private in silence.
71
+ */
40
72
  declare function chainForNetwork(network: Network): ChainConfig;
41
73
 
42
74
  /**
@@ -127,23 +159,31 @@ interface MemoryScanOptions {
127
159
  toolArgsJson?: string;
128
160
  }
129
161
  /**
130
- * Which Atbash chain an action runs against. `public` is the shared
131
- * testnet (Free plan); `private` is reserved for Private / Swarm /
132
- * Enterprise tier subscribers. The SDK resolves which one to use from
133
- * the org's subscription via `orgName` — callers normally don't need
134
- * to pass this manually.
162
+ * Chain selector. Only `"private"` exists: the public testnet chains are
163
+ * retired. The SDK resolves an org's chain through the Dashboard via
164
+ * `orgName` — callers normally don't need to pass this manually.
135
165
  */
136
- type Network = "public" | "private";
166
+ type Network = "private";
137
167
  /**
138
168
  * Per-call chain overrides. Pass exactly what you need to override —
139
- * `network` selects between the SDK's known chains, `blockchainRid` /
140
- * `nodeUrls` pin a fully custom chain. Pass none of these to fall back
141
- * to the SDK's defaults (or to org-resolved chain when `orgName` is set).
169
+ * `network` selects between the SDK's known chains, `blockchainRid` pins a
170
+ * custom one, and `endpoint` names the Dashboard the calls travel over. Pass
171
+ * none of these to fall back to the SDK's defaults (or to the org-resolved
172
+ * chain when `orgName` is set).
142
173
  */
143
174
  interface ChainOpts {
144
175
  network?: Network;
145
176
  blockchainRid?: string;
177
+ /**
178
+ * No longer dialled: memory reaches the chain through the Dashboard. Kept
179
+ * because callers and the other bindings still carry it as chain config.
180
+ */
146
181
  nodeUrls?: readonly string[];
182
+ /**
183
+ * Dashboard base URL. Memory reaches the chain through it, so this is where
184
+ * those requests go; defaults to the endpoint compiled into the core.
185
+ */
186
+ endpoint?: string;
147
187
  }
148
188
  /** Canonical verdict after normalization. */
149
189
  type Verdict = "ALLOW" | "HOLD" | "BLOCK" | "No verdict";
@@ -169,19 +209,34 @@ interface LogToolCallResult {
169
209
  }
170
210
  interface JudgeResult {
171
211
  verdict: Verdict;
212
+ /**
213
+ * Canonical executable permission, computed by `canonicalAllow` — see that
214
+ * function for the rule. Never more permissive than `auditToolCall`.
215
+ */
216
+ allow: boolean;
172
217
  actionType: string;
173
218
  reason: string;
174
219
  confidence: number;
175
220
  provider: string;
176
221
  latencyMs: number;
177
222
  toolCallId: string;
223
+ /**
224
+ * The id the judge recorded this verdict under, when it differs from
225
+ * `toolCallId` (the on-chain judge path). Poll `getJudgmentStatus` with
226
+ * `judgmentId || toolCallId`. Absent when the reply names none.
227
+ */
228
+ judgmentId?: string;
178
229
  onChain: boolean;
230
+ /** What the reply stated about the chain; see {@link VerdictRecord}. */
231
+ record?: VerdictRecord;
179
232
  /** Severity 1 (poisonous) → 10 (benign). Present on memory-scan responses; absent for regular tool-call judgments. */
180
233
  score?: number;
181
234
  /**
182
235
  * Whether the org's protection mode actually acts on a HOLD/BLOCK verdict.
183
236
  * `false` in Monitor mode (verdict is logged for observation, agent is NOT
184
237
  * jailed and the action is not held); `true` in Enforce mode.
238
+ * A plain boolean: an omitted field reads as `false`. `Decision.enforced`
239
+ * keeps "unknown" apart; gate on that, not on this.
185
240
  */
186
241
  enforced: boolean;
187
242
  /** Server-reported protection mode: `off`, `monitor`, or `enforce`. */
@@ -193,15 +248,28 @@ interface JudgeResult {
193
248
  * missing" — never infer the former from a null verdict alone.
194
249
  */
195
250
  status: string;
251
+ /**
252
+ * The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
253
+ * "HOLD", or "" when it neither refused nor is a known refusal shape.
254
+ */
255
+ refusal?: "" | "BLOCK" | "HOLD";
196
256
  }
197
257
  interface JudgmentStatus {
198
258
  status: JudgmentState;
199
259
  verdict: Verdict;
260
+ /** Same canonical executable permission as {@link JudgeResult.allow}. */
261
+ allow: boolean;
200
262
  reason: string;
201
263
  judgmentId: string;
202
264
  onChain?: boolean;
203
265
  cached?: boolean;
204
266
  responseTimeMs?: number;
267
+ /**
268
+ * The verdict the judge reached (`"BLOCK"` or `"HOLD"`) when the
269
+ * organisation's signed mode (`shadow`, `monitor`, `off`) let the action run
270
+ * anyway, as {@link Decision.wouldVerdict}. Absent when the status is the verdict.
271
+ */
272
+ wouldVerdict?: "BLOCK" | "HOLD";
205
273
  }
206
274
  interface TierInfo {
207
275
  orgName: string;
@@ -298,6 +366,13 @@ interface AtbashLogger {
298
366
  /** Options accepted by the `Atbash` constructor. */
299
367
  interface AtbashOptions {
300
368
  endpoint?: string;
369
+ /**
370
+ * Overall deadline for one `auditToolCall` decision, across every request
371
+ * it makes (org lookup, exists check, judge). Default 60 000 ms. When it
372
+ * passes the decision is an ERROR (denied unless `failClosed` is false).
373
+ * `timeoutMs` still bounds each single request.
374
+ */
375
+ decisionTimeoutMs?: number;
301
376
  timeoutMs?: number;
302
377
  /**
303
378
  * Full chain override — BRID + nodeUrls in one object. Wins over every
@@ -306,7 +381,7 @@ interface AtbashOptions {
306
381
  */
307
382
  chain?: ChainConfig;
308
383
  /**
309
- * Preset chain selector — `"public"` or `"private"`. Resolves to the
384
+ * Preset chain selector — `"private"` is the only value. Resolves to the
310
385
  * matching `ChainConfig` via `chainForNetwork()`. Overridden by `chain`,
311
386
  * overrides env `ATBASH_DEFAULT_CHAIN_NETWORK` and the config file.
312
387
  */
@@ -341,8 +416,35 @@ interface AtbashOptions {
341
416
  * `orgEncryptionPubKey` overrides this, same as `verifyPubKey`.
342
417
  */
343
418
  orgEncryptionPubKey?: string;
344
- /** When true (default), `auditToolCall` denies on any error. */
419
+ /**
420
+ * When true (default), `auditToolCall` denies on any error. When false, it
421
+ * allows only when Atbash could not be reached or could not answer: a
422
+ * transport failure, a timeout, a 5xx or a 408 (and a readable reply that
423
+ * neither grants nor refuses). Atbash's own refusals still deny: an
424
+ * unregistered agent, any other 4xx (no policy, billing suspended, quota
425
+ * exhausted or rate limited, unauthorized), and a 2xx reply that is not a
426
+ * JSON object. Direct `judgeAction` callers get a `JudgeRefusalError` for
427
+ * an unregistered agent or an unreadable reply.
428
+ */
345
429
  failClosed?: boolean;
430
+ /**
431
+ * When true (the default), an answer that carries no enforcement decision
432
+ * (the audit tier's `{status:"logged"}` with no verdict) is refused:
433
+ * `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
434
+ * `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
435
+ * are false. Set it to `false` only to run deliberately in log-only mode
436
+ * (security review 2026-09-30, H6: an unsigned marker switched enforcement
437
+ * off).
438
+ */
439
+ requireEnforcement?: boolean;
440
+ /**
441
+ * When true, `auditToolCall` enforces the judge's verdict whatever
442
+ * enforcement mode the organisation is in. Default false: on a judge host
443
+ * whose replies are signature-checked, a signed `shadow`, `monitor` or `off`
444
+ * mode lets a BLOCK or HOLD run, reported in `Decision.wouldVerdict`. This
445
+ * can only make the client stricter.
446
+ */
447
+ enforceLocally?: boolean;
346
448
  /**
347
449
  * Verbose diagnostics. Off by default. When on, a failed judge call also
348
450
  * logs the response body, which is the difference between "judge API failed"
@@ -351,6 +453,44 @@ interface AtbashOptions {
351
453
  debug?: boolean;
352
454
  logger?: AtbashLogger;
353
455
  }
456
+ /**
457
+ * What the judge stated about recording this verdict. A host that must not run
458
+ * an unrecorded action gates on `verdictRecorded`. `onChain` is the judge's
459
+ * statement: the SDK does not read the chain.
460
+ */
461
+ interface VerdictRecord {
462
+ /** The judge's `on_chain`: the verdict is recorded on chain. */
463
+ onChain: boolean;
464
+ /** The judge's `on_chain_status`, such as `pending` or `skipped`; absent when not stated. */
465
+ status?: string;
466
+ /**
467
+ * The reply names the ids this call generated. False when it names another
468
+ * call, or none: `toolCallId` then falls back to this client's own id, which
469
+ * proves nothing about what the judge recorded.
470
+ */
471
+ boundToCall: boolean;
472
+ /**
473
+ * This reply's signature was checked against a judge key before it was read.
474
+ * False means nothing proves who wrote the reply: it can echo this call's ids
475
+ * and still read as bound, because the ids travel in the request.
476
+ */
477
+ signatureChecked: boolean;
478
+ /**
479
+ * The judge's own `status`, absent when not stated. `shadow_recorded` is a
480
+ * shadow ack: the call was stored and has no verdict yet, which is what
481
+ * separates it from an ALLOW whose `status` here is `pending`. Read it with
482
+ * `boundToCall` and `signatureChecked`: unsigned, it is only a claim.
483
+ */
484
+ judgeStatus?: string;
485
+ /**
486
+ * The judge answered this call with a verdict it states is recorded on chain,
487
+ * in a signature-checked reply: `onChain`, `boundToCall` and `signatureChecked`
488
+ * all true, and no `judgeStatus`, which only a reply without a verdict carries
489
+ * (a shadow ack, or an answer that was only logged). Still the judge's word:
490
+ * the SDK does not read the chain.
491
+ */
492
+ verdictRecorded: boolean;
493
+ }
354
494
  /** Canonical decision returned by `auditToolCall`. */
355
495
  type DecisionVerdict = "ALLOW" | "HOLD" | "BLOCK" | "ERROR";
356
496
  interface Decision {
@@ -358,6 +498,34 @@ interface Decision {
358
498
  verdict: DecisionVerdict;
359
499
  reason?: string;
360
500
  toolCallId?: string;
501
+ /**
502
+ * Whether the organisation acts on a HOLD/BLOCK verdict; false under Monitor mode and per-agent Shadow Mode. Absent means unknown: a
503
+ * transport error, or a judge that stated no mode. Test `=== false`, never
504
+ * `!decision.enforced`, or every outage counts as a would-block.
505
+ * Informational only; `allow` remains the gate.
506
+ */
507
+ enforced?: boolean;
508
+ /** Server-reported protection mode (`off`, `monitor`, `enforce`); absent when unknown. */
509
+ enforcementMode?: string;
510
+ /**
511
+ * The verdict the judge reached when the organisation's signed mode
512
+ * (`shadow`, `monitor`, `off`) let the action run anyway: what would have
513
+ * happened under enforcement. Absent when the decision is the verdict.
514
+ */
515
+ wouldVerdict?: "BLOCK" | "HOLD";
516
+ /**
517
+ * The id the judge recorded this verdict under, when it differs from
518
+ * `toolCallId` (the on-chain judge path), as {@link JudgeResult.judgmentId}.
519
+ * Poll `getJudgmentStatus` with `judgmentId || toolCallId`.
520
+ */
521
+ judgmentId?: string;
522
+ /**
523
+ * What the judge stated about recording this verdict. Absent means unknown:
524
+ * a transport error, a local jail hold, or a reply that did not state
525
+ * `on_chain`. Treat absent as not recorded. Informational only; `allow`
526
+ * remains the gate.
527
+ */
528
+ record?: VerdictRecord;
361
529
  }
362
530
  /** Input to `auditToolCall`. */
363
531
  interface ToolCallInput {
@@ -393,6 +561,12 @@ interface FromConfigOptions {
393
561
  /** Default org name — see {@link AtbashOptions.orgName}. */
394
562
  orgName?: string;
395
563
  failClosed?: boolean;
564
+ /** See {@link AtbashOptions.requireEnforcement}. Default true. */
565
+ requireEnforcement?: boolean;
566
+ /** See {@link AtbashOptions.enforceLocally}. Default false. */
567
+ enforceLocally?: boolean;
568
+ /** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
569
+ decisionTimeoutMs?: number;
396
570
  /** See {@link AtbashOptions.debug}. */
397
571
  debug?: boolean;
398
572
  logger?: AtbashLogger;
@@ -401,8 +575,6 @@ interface FromConfigOptions {
401
575
  interface JudgeOptions {
402
576
  toolName?: string;
403
577
  toolArgsJson?: string;
404
- provider?: string;
405
- model?: string;
406
578
  verifyPubKey?: string;
407
579
  /** Per-call override of {@link AtbashOptions.orgEncryptionPubKey}. */
408
580
  orgEncryptionPubKey?: string;
@@ -447,10 +619,23 @@ declare class Atbash {
447
619
  readonly orgName?: string;
448
620
  /** Default judge response-signing pubkey, if configured (see fromConfig). */
449
621
  readonly verifyPubKey?: string;
622
+ /**
623
+ * The keys every judge reply must be signed by: the host's pinned keys, or
624
+ * the configured `verifyPubKey` on a host with none. Empty means unchecked.
625
+ */
626
+ private readonly verifyKeys;
450
627
  /** Default org encryption key — see {@link AtbashOptions.orgEncryptionPubKey}. */
451
628
  readonly orgEncryptionPubKey?: string;
452
629
  /** When true (default), `auditToolCall` denies on any error. */
453
630
  readonly failClosed: boolean;
631
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
632
+ readonly requireEnforcement: boolean;
633
+ /** When true, a signed non-enforcing mode never relaxes a verdict. */
634
+ readonly enforceLocally: boolean;
635
+ /** Results whose enforcement mode came signed and bound to their call. */
636
+ private readonly modeTrusted;
637
+ /** Overall deadline for one auditToolCall decision, in ms. */
638
+ readonly decisionTimeoutMs: number;
454
639
  /** Org key learned from the last agent-exists check, for this agent only. */
455
640
  private _orgKeyFromChain;
456
641
  private readonly debug;
@@ -492,12 +677,6 @@ declare class Atbash {
492
677
  */
493
678
  private _agentExistsCache;
494
679
  private static readonly AGENT_EXISTS_TTL_MS;
495
- /**
496
- * Cached bearer token for risk-engine / insurance read calls. Built
497
- * lazily as a signed `log_tool_call` tx and refreshed every 4 min so
498
- * server-side replay protection windows never expire it mid-session.
499
- */
500
- private _authBearers;
501
680
  /** Guards `logEnvironmentOnce` — hosts construct several clients. */
502
681
  private static environmentLogged;
503
682
  constructor(privkey: string, options?: AtbashOptions);
@@ -520,6 +699,48 @@ declare class Atbash {
520
699
  * validated against the trusted allowlist / self-hosted policy; a
521
700
  * self-hosted endpoint's `verifyPubKey` becomes the client default.
522
701
  */
702
+ /**
703
+ * Decide which key this client signs as.
704
+ *
705
+ * Most specific wins: a key passed in code, then a key file the host named,
706
+ * then the environment, then the default key file. `config.json` is absent
707
+ * from that list on purpose — it is a settings file, writable by anything
708
+ * that can write the user's home and read by every Atbash integration on the
709
+ * machine, so letting it name the key meant one stray value silently
710
+ * re-pointed who each of them signed as.
711
+ *
712
+ * A named `keyPath` that does not exist throws rather than falling through.
713
+ * The caller said which file holds its identity; quietly signing as a
714
+ * different one is the outcome worth refusing.
715
+ *
716
+ * @param options - The `fromConfig` options, read for `agentKey` and `keyPath`.
717
+ * @returns The resolved agent identity.
718
+ * @throws When a named key file is missing, or no identity can be found.
719
+ */
720
+ private static resolveIdentity;
721
+ /**
722
+ * Add the one thing a machine with no key file needs to hear.
723
+ *
724
+ * This is the only path where the operator cannot see what happened: no key
725
+ * file, and the key they are looking for sits in `config.json`, which no
726
+ * longer selects the identity. A bare "key file not found" sends them hunting
727
+ * for a key they already have. Only this branch gets the hint — a caller who
728
+ * named a `keyPath` is told about that path, where a stranded key is noise.
729
+ *
730
+ * @param err - The "key file not found" error from the default lookup.
731
+ * @returns The same error, or one that also names the key in config.json.
732
+ */
733
+ private static explainStrandedKey;
734
+ /**
735
+ * Say so when `config.json` still holds an agent key.
736
+ *
737
+ * Nothing signs with it any more, and silence would leave an operator
738
+ * believing one identity is in force while another is. The pubkey is named,
739
+ * not the key, so the line is safe in a log.
740
+ *
741
+ * @param logger - The host logger, when one was supplied.
742
+ */
743
+ private static warnOnStrandedKey;
523
744
  static fromConfig(options?: FromConfigOptions): Atbash;
524
745
  get pubkey(): string;
525
746
  get privkey(): string;
@@ -539,6 +760,13 @@ declare class Atbash {
539
760
  * return the signed tx hex. The server broadcasts to chain.
540
761
  */
541
762
  logToolCall(action: string, context?: string, options?: LogToolCallOptions): Promise<LogToolCallResult>;
763
+ /**
764
+ * `raisePreflightErrors` (the judge path, like Python's
765
+ * `raise_preflight_errors`): a failed registration check throws instead of
766
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
767
+ * (not registered, or a 4xx) from an outage.
768
+ */
769
+ private _logToolCall;
542
770
  /**
543
771
  * Sign log_tool_call + optionally judge_action, POST /api/v1/judge.
544
772
  *
@@ -546,7 +774,29 @@ declare class Atbash {
546
774
  * response bytes via the Rust core's `verifySignature`.
547
775
  */
548
776
  judgeAction(action: string, context?: string, options?: JudgeOptions): Promise<JudgeResult>;
777
+ /**
778
+ * judgeAction, optionally asking the judge for the true verdict and the
779
+ * organisation's enforcement mode so this client applies the mode itself.
780
+ *
781
+ * Only auditToolCall opts in: judgeAction's callers keep the server-side
782
+ * downgrade they gate on today.
783
+ *
784
+ * @param clientEnforces - Send `X-Atbash-Client-Enforcement` when the reply can be trusted.
785
+ */
786
+ private judgeTracked;
549
787
  private _judgeAction;
788
+ /**
789
+ * Start a local hold on an enforced BLOCK that jails: one already on chain,
790
+ * or one the judge answered before its chain write landed
791
+ * (`on_chain_status: "pending"`), which jails once it lands. Refuse any other
792
+ * result that lands while the agent is held, or whose request was already in
793
+ * flight when the block arrived: the server judged it on pre-jail state.
794
+ *
795
+ * @param onChainStatus - The reply's `on_chain_status`; empty when absent.
796
+ */
797
+ private applyJailHold;
798
+ /** The server's jailed refusal, produced locally while the agent is held. */
799
+ private localJailBlock;
550
800
  /**
551
801
  * High-level guard: redact secrets, submit for judgement, and collapse the
552
802
  * result into an allow/deny `Decision`. Fails closed by default — any error
@@ -554,6 +804,33 @@ declare class Atbash {
554
804
  * explicitly false.
555
805
  */
556
806
  auditToolCall(input: ToolCallInput): Promise<Decision>;
807
+ /**
808
+ * The verify keys for one call (R4, governor tier-2 review). A per-call key
809
+ * may only narrow what the client accepts: on a client pinned to judge keys
810
+ * it must be one of them, never a replacement; on an unpinned client it
811
+ * adds the check.
812
+ */
813
+ private effectiveVerifyKeys;
814
+ /**
815
+ * Collapse a judge result into a {@link Decision}. The most restrictive
816
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
817
+ * and the canonical `allow` are each able to deny, and only a result where
818
+ * all three agree may execute. A present `allow` / `decision.allow` that is
819
+ * not exactly `true` is the judge declining to grant permission, so it
820
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
821
+ * never overrides a refusal.
822
+ */
823
+ private decide;
824
+ /**
825
+ * Apply the organisation's enforcement mode to the judge's true verdict.
826
+ *
827
+ * Only a mode that arrived signed and bound to this call counts; a missing,
828
+ * unsigned or unknown mode, or enforceLocally, means enforce. Under
829
+ * `shadow`, `monitor` or `off` a BLOCK or HOLD runs, reported in
830
+ * `wouldVerdict`. An ERROR stays an error: it is not a verdict.
831
+ */
832
+ private applyMode;
833
+ private decideInner;
557
834
  /**
558
835
  * One exit for every judge failure.
559
836
  *
@@ -563,16 +840,22 @@ declare class Atbash {
563
840
  * The response body follows only under `debug`, since it can echo the action.
564
841
  */
565
842
  private failJudge;
843
+ /**
844
+ * The canonical permission for a response body under this client's
845
+ * settings: {@link canonicalAllow}, minus the audit tier when
846
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
847
+ */
848
+ private executable;
566
849
  private fail;
567
850
  /**
568
851
  * Return the current status of a previously submitted judgment.
569
852
  *
570
- * `chainOpts` names which chain the judgment was signed against. The
571
- * server's GET /api/v1/judge routes to that chain when the SDK sends
572
- * a `brid` query param; without it, the server falls back to public.
573
- * Callers on the private chain must pass a `chainOpts` (or configure
574
- * the client on the private chain) — otherwise polling a POSTed
575
- * judgment on the private chain 404s at the server.
853
+ * `chainOpts` names which chain the judgment was signed against; the SDK
854
+ * sends it as the `brid` query param the server routes by. Without it the
855
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
856
+ * client's org chain when it has an `orgName`, else the client default.
857
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
858
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
576
859
  */
577
860
  getJudgmentStatus(judgmentId: string, agentPubkey?: string, chainOpts?: ChainOpts): Promise<JudgmentStatus>;
578
861
  getToolCalls(maxCount: number): Promise<ToolCallRecord[]>;
@@ -604,12 +887,9 @@ declare class Atbash {
604
887
  * Resolve which chain an org's actions should run against. Cached
605
888
  * per-client by orgName. Resolution order:
606
889
  * 1. `org_networks` map (authoritative).
607
- * 2. Per-chain subscription fallback — public + private records
608
- * are fetched in parallel, with `is_private_blockchain` and
609
- * `assigned_at` reconciling mixed states.
610
- * A lookup that names exactly one chain wins outright. Where it names
611
- * neither (a brand-new org) or cannot choose between them, the client's
612
- * configured default decides.
890
+ * 2. Subscription fallback — the org's subscription row on the chain.
891
+ * A lookup that finds the org wins outright. Where it finds nothing (a
892
+ * brand-new org), the client's configured default decides.
613
893
  */
614
894
  resolveChainForOrg(orgName: string): Promise<ChainConfig>;
615
895
  /**
@@ -637,6 +917,13 @@ declare class Atbash {
637
917
  * chains; otherwise the client's default.
638
918
  */
639
919
  private bridFromChainOpts;
920
+ /**
921
+ * The chain one call's transactions are signed for: the per-call override
922
+ * when given, otherwise the client's org, otherwise the client default.
923
+ * log_tool_call, judge_action and the judgment-status read all use this, so
924
+ * a request never pairs transactions signed for two different chains.
925
+ */
926
+ private callBrid;
640
927
  /**
641
928
  * Resolve the dashboard chain used by agent metadata/policy reads.
642
929
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -707,20 +994,48 @@ declare class Atbash {
707
994
  static containsEvasionCharacters(text: string): boolean;
708
995
  }
709
996
 
997
+ declare const DEFAULTS: {
998
+ ceilingMs: number;
999
+ pollMs: number;
1000
+ };
1001
+ /** Whether `pubkey` is currently held. */
1002
+ declare function isJailHeld(pubkey: string): boolean;
1003
+ /** Forget every hold and stop its poll; tests and hosts that reset state. */
1004
+ declare function releaseJailHolds(): void;
1005
+ /** Override the ceiling or poll interval; an empty object restores defaults. */
1006
+ declare function configureJailHold(next: Partial<typeof DEFAULTS>): void;
1007
+
710
1008
  declare const DEFAULT_ENDPOINT: string;
711
- declare const DEFAULT_CHROMIA_NODE_URLS: readonly string[];
712
- declare const DEFAULT_BLOCKCHAIN_RID: string;
713
1009
 
714
1010
  declare class AtbashAPIError extends Error {
715
1011
  /** HTTP status code (or 0 if the request never completed). */
716
1012
  readonly status: number;
717
1013
  /** Raw response body text (may be empty). */
718
1014
  readonly body: string;
719
- constructor(status: number, body: string, statusText?: string, endpoint?: string);
1015
+ /**
1016
+ * Request path this status came from, e.g. `/api/v1/judge` — absent when the
1017
+ * request never reached a response (transport failures carry status 0).
1018
+ *
1019
+ * A status alone does not say what failed. Several routes answer one call, and
1020
+ * more than one of them can 404: a caller reading only `404` cannot tell "this
1021
+ * agent is not registered" from "the chain-routing lookup did not resolve", and
1022
+ * has to guess from the body's prose — which a 404 raised before any handler
1023
+ * ran does not carry. The path is the fact that settles it.
1024
+ */
1025
+ readonly path?: string;
1026
+ constructor(status: number, body: string, statusText?: string, endpoint?: string, path?: string);
720
1027
  }
721
1028
  declare class SignatureVerificationError extends Error {
722
1029
  constructor(message: string);
723
1030
  }
1031
+ /**
1032
+ * Atbash answered, and the answer was not a grant: the agent is not
1033
+ * registered, or the reply could not be read. Unlike an unreachable judge this
1034
+ * always denies, whatever `failClosed` says.
1035
+ */
1036
+ declare class JudgeRefusalError extends Error {
1037
+ constructor(message: string);
1038
+ }
724
1039
 
725
1040
  /**
726
1041
  * Thin typed fetch wrapper.
@@ -759,9 +1074,25 @@ declare class HttpTransportError extends Error {
759
1074
  });
760
1075
  }
761
1076
 
1077
+ declare function canonicalAllow(data: Record<string, unknown>): boolean;
1078
+
762
1079
  /** Normalize wire shapes (verdict casing, status, pubkey) to canonical forms. */
763
1080
 
764
1081
  declare function normalizeVerdict(raw: unknown): Verdict;
1082
+ /**
1083
+ * Canonicalize `action_type` at the wire boundary, the way
1084
+ * {@link normalizeVerdict} already canonicalizes `verdict`.
1085
+ *
1086
+ * `verdict` has been normalized here since the beginning and has never
1087
+ * drifted between consumers. `action_type` was passed through raw, so every
1088
+ * reader invented its own folding policy — `auditToolCall` compared exactly
1089
+ * while `memory/scan.ts` trimmed and case-folded, and the same `" ALLOW "`
1090
+ * was therefore an error on one path and permission on the other.
1091
+ *
1092
+ * Trim and case-fold only. Zero-width and homoglyph variants survive
1093
+ * untouched, stay outside the known set, and still fail closed.
1094
+ */
1095
+ declare function normalizeActionType(raw: unknown): string;
765
1096
  declare function normalizeStatus(raw: unknown): JudgmentState;
766
1097
  /** Wire pubkey may be a hex string, a Buffer/Uint8Array, or `{ data: [...] }`. */
767
1098
  declare function pubkeyToHex(val: unknown): string;
@@ -784,8 +1115,6 @@ interface AtbashUserConfig {
784
1115
  * at construction instead.
785
1116
  */
786
1117
  defaultChainNetwork?: Network;
787
- provider?: string;
788
- providerModel?: string;
789
1118
  /** "1" / "true" turns on verbose diagnostics. See AtbashOptions.debug. */
790
1119
  debug?: string;
791
1120
  }
@@ -894,7 +1223,7 @@ interface CommitMemoryOptions {
894
1223
  filePath?: string;
895
1224
  /** Org name — when set, the SDK resolves which chain the agent lives on. */
896
1225
  orgName?: string;
897
- /** Atbash service endpoint for org→chain lookup. */
1226
+ /** Atbash service endpoint — where memory reaches the chain, and the org→chain lookup. */
898
1227
  endpoint?: string;
899
1228
  /** Self-hosted judge response-signing pubkey; required with a non-allowlisted `endpoint`. */
900
1229
  verifyPubKey?: string;
@@ -902,6 +1231,7 @@ interface CommitMemoryOptions {
902
1231
  }
903
1232
  interface RollbackMemoryOptions {
904
1233
  orgName?: string;
1234
+ /** Atbash service endpoint — where memory reaches the chain, and the org→chain lookup. */
905
1235
  endpoint?: string;
906
1236
  /** Self-hosted judge response-signing pubkey; required with a non-allowlisted `endpoint`. */
907
1237
  verifyPubKey?: string;
@@ -961,11 +1291,18 @@ declare function getRecentAgentMemory(auth: AgentAuth, chainOpts?: ChainOpts, fi
961
1291
  * when you need every active version regardless of age.
962
1292
  */
963
1293
  declare function getActiveAgentMemory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<AgentMemoryEntry[]>;
1294
+ interface MemoryHistoryPage {
1295
+ /** Most versions to return, at most 200. Omit for the full history. */
1296
+ maxCount?: number;
1297
+ /** Return only versions with a lower id: the last id of the previous page. */
1298
+ beforeId?: number;
1299
+ }
964
1300
  /**
965
1301
  * Full version history — active + inactive, most recent first. Used
966
- * by rollback UX to choose a target version.
1302
+ * by rollback UX to choose a target version. Pass `page` to read it in
1303
+ * pages; this needs a Dashboard and chain that support paging.
967
1304
  */
968
- declare function getMemoryHistory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<AgentMemoryEntry[]>;
1305
+ declare function getMemoryHistory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null, page?: MemoryHistoryPage): Promise<AgentMemoryEntry[]>;
969
1306
  /**
970
1307
  * Fetch a single memory entry by version id, including its current
971
1308
  * `is_active` state. Version ids are agent-unique on chain (not
@@ -1033,6 +1370,13 @@ interface ClassifyMemoryWriteOptions {
1033
1370
  *
1034
1371
  * Empty-content writes return `null`: they can't carry a poisoning
1035
1372
  * payload, and the regular audit path still sees them.
1373
+ *
1374
+ * THROWS on host input it cannot read without side effects: a proxy, an accessor where a data
1375
+ * property is expected, a value with no safe projection, or one past the size bounds. Refusing is
1376
+ * deliberate - a silent `null` would report "not a memory write" for a call nobody could classify,
1377
+ * and memory protection would be skipped exactly where the input was strange. Callers that sit in a
1378
+ * host hook MUST catch it and turn it into a decision; `guardMemoryWrite` and
1379
+ * `MemoryGuardManager.handleBeforeToolCall` already do.
1036
1380
  */
1037
1381
  declare function classifyMemoryWrite(event: unknown, ctx: unknown, opts?: ClassifyMemoryWriteOptions): MemoryEntry | null;
1038
1382
 
@@ -1058,6 +1402,8 @@ interface GuardMemoryWriteInput extends ClassifyMemoryWriteOptions {
1058
1402
  verifyPubKey?: string;
1059
1403
  /** Org name for chain resolution (public vs org-private chain). */
1060
1404
  orgName?: string;
1405
+ /** Explicit chain target when the host already resolved and verified it. */
1406
+ chainOpts?: ChainOpts;
1061
1407
  /** LLM confidence threshold for yellow escalation. Default 0.6. */
1062
1408
  threshold?: number;
1063
1409
  /** When false, red verdicts log but don't block. Default true. */
@@ -1066,6 +1412,17 @@ interface GuardMemoryWriteInput extends ClassifyMemoryWriteOptions {
1066
1412
  debug?: boolean;
1067
1413
  /** Optional logger for debug probe + persist-failure warnings. */
1068
1414
  logger?: GuardLogger;
1415
+ /** Agent home directory names to accept beyond the built-in list. */
1416
+ agentHomeDirs?: string[];
1417
+ /**
1418
+ * Root the caller keeps memory under, when it is not an agent home.
1419
+ *
1420
+ * Only pass a value the host stated outright. A root defaulted from the
1421
+ * process working directory would make whichever directory the host happened
1422
+ * to start in look like a memory store — which is how a stray MEMORY.md once
1423
+ * ended up inside a plugin checkout.
1424
+ */
1425
+ memoryRoot?: string;
1069
1426
  }
1070
1427
  /** Plugin-facing decision. Shape mirrors what plugins return from `before_tool_call`. */
1071
1428
  interface GuardMemoryDecision {
@@ -1080,25 +1437,57 @@ interface GuardMemoryWriteResult {
1080
1437
  decision?: GuardMemoryDecision;
1081
1438
  /** Present when handled and scan ran. Absent when scan threw. */
1082
1439
  scanResult?: MemoryScanResult;
1083
- /** Present when handled. True when a chain-commit was dispatched (fire-and-forget). */
1440
+ /** Present when handled. True only after the chain transaction confirms. */
1084
1441
  committed?: boolean;
1085
1442
  }
1086
1443
  /**
1087
- * Guarded memory-write flow. Returns `{ handled: false }` when the tool
1088
- * call isn't a memory write, otherwise returns a `decision` the plugin
1089
- * should return from its hook.
1444
+ * Guard a memory write, and record what was decided.
1090
1445
  *
1091
- * Verdict handling:
1092
- * - `red` → not persisted to chain; decision blocks (unless `enforce: false`)
1093
- * - `yellow` → persisted with LLM score; decision allows
1094
- * - `green` → persisted with LLM score; decision allows
1446
+ * Wraps the decision rather than threading telemetry through its eight exits,
1447
+ * so a later return added inside cannot quietly stop being counted.
1095
1448
  *
1096
- * Chain commit is fire-and-forget: network errors are logged via
1097
- * `logger.warn` but never surfaced to the caller. This mirrors the
1098
- * existing plugin behaviour — chain is for audit/recovery, not gating.
1449
+ * Only a call this guard actually handled is recorded. A tool call that was
1450
+ * never a memory write is not a memory operation, and counting it would put
1451
+ * every unrelated edit into the number a memory migration reads.
1452
+ *
1453
+ * @param input - The tool call and the agent to judge it for.
1454
+ * @returns What the guard decided; unchanged by the recording.
1099
1455
  */
1100
1456
  declare function guardMemoryWrite(input: GuardMemoryWriteInput): Promise<GuardMemoryWriteResult>;
1101
1457
 
1458
+ /**
1459
+ * The chain slot of a memory file, computed the same way everywhere.
1460
+ *
1461
+ * The Rust core finds the slot by stripping the agent's home from the file's
1462
+ * path, as an exact string prefix. The file path arrives resolved
1463
+ * (`path.resolve`), but the home did not: on Windows `HOME=/home/agent`
1464
+ * resolves the file to `C:\home\agent\...` and never matches, and a host that
1465
+ * spells the drive or the user folder differently (`c:\users\me` against
1466
+ * `C:\Users\Me`, both the same folder) did not match either. A file with no
1467
+ * slot is scanned but never recorded on chain, silently, so the on-chain
1468
+ * version record and its rollback/read checks were skipped for it.
1469
+ *
1470
+ * Here the home (and the memory root) is resolved like the file, and on a
1471
+ * case-insensitive file system it is passed to the core spelled the way the
1472
+ * file path spells it. The slot itself, the part below the agent home, keeps
1473
+ * the file's own spelling, so slots recorded before this change still match.
1474
+ *
1475
+ * Known limit: a folder with NTFS per-directory case sensitivity enabled
1476
+ * (`fsutil file setCaseSensitiveInfo`, or one created from WSL) can hold both
1477
+ * `Me` and `me`; there ASCII case does separate folders, and a sibling spelled
1478
+ * in another case would share the home's slots. Creating one needs write
1479
+ * access to the home's parent. A follow-up can confirm the same folder on disk
1480
+ * when the spellings differ.
1481
+ */
1482
+
1483
+ /** Path semantics: `node:path` here, `path.win32` / `path.posix` in tests. */
1484
+ type PathImpl = typeof nodePath;
1485
+ /**
1486
+ * The chain slot for `target`, or null when it gets no chain record. `p`
1487
+ * selects path semantics (tests pass `path.win32` to check Windows anywhere).
1488
+ */
1489
+ declare function memorySlotOf(target: string, home: string, root?: string, extraHomes?: string[], p?: PathImpl): string | null;
1490
+
1102
1491
  /**
1103
1492
  * Aligns a local `MemoryPointer` with chain's active memory version
1104
1493
  * via a TTL-cached pointer check. Inside the TTL window: no-op.
@@ -1125,6 +1514,8 @@ interface SyncMemoryOptions {
1125
1514
  /** Skip the TTL gate and force a chain check this call. Default false. */
1126
1515
  force?: boolean;
1127
1516
  chainOpts?: ChainOpts;
1517
+ /** Exact chain slot for the managed file; omission keeps the cross-file API behavior. */
1518
+ filePath?: string;
1128
1519
  }
1129
1520
  /**
1130
1521
  * `drifted: false` — pointer is still valid; caller can keep serving the local copy.
@@ -1158,10 +1549,19 @@ declare class PointerStore {
1158
1549
  private cache;
1159
1550
  private loading;
1160
1551
  constructor(filePath: string);
1161
- /** Resolves the pointer for `agentPubkeyHex`, or a zero-pointer that will force a sync on first use. */
1162
- get(agentPubkeyHex: string): Promise<MemoryPointer>;
1163
- /** Persists an updated pointer. Failures are swallowed to a logger callback (if provided) so sync never blocks the caller. */
1164
- set(agentPubkeyHex: string, pointer: MemoryPointer, onError?: (err: Error) => void): Promise<void>;
1552
+ /**
1553
+ * The pointer for one agent's file, or a zero-pointer forcing a sync.
1554
+ *
1555
+ * @param filePath - The file's chain slot, i.e. its basename.
1556
+ */
1557
+ get(agentPubkeyHex: string, filePath: string): Promise<MemoryPointer>;
1558
+ /**
1559
+ * Persist one file's pointer, leaving the agent's other files alone.
1560
+ *
1561
+ * Write failures are reported to `onError` rather than thrown: a cache that
1562
+ * cannot be written must not take down the read it was speeding up.
1563
+ */
1564
+ set(agentPubkeyHex: string, filePath: string, pointer: MemoryPointer, onError?: (err: Error) => void): Promise<void>;
1165
1565
  private ensureLoaded;
1166
1566
  private loadOnce;
1167
1567
  private persist;
@@ -1173,6 +1573,8 @@ declare function defaultPointerPath(workspaceDir?: string): string;
1173
1573
  interface WrappedLogger {
1174
1574
  info(message: string, meta?: Record<string, unknown>): void;
1175
1575
  warn(message: string, meta?: Record<string, unknown>): void;
1576
+ /** Wait for queued writes and surface any lost log entry. */
1577
+ flush(): Promise<void>;
1176
1578
  }
1177
1579
  interface UpstreamLogger {
1178
1580
  info: (...args: unknown[]) => void;
@@ -1201,6 +1603,13 @@ interface ClassifyMemoryReadOptions {
1201
1603
  * Caller-supplied `patterns` are MERGED with the defaults (matches
1202
1604
  * Node's original behavior — extending in one plugin doesn't disable
1203
1605
  * standard coverage).
1606
+ *
1607
+ * THROWS on host input it cannot read without side effects: a proxy, an accessor where a data
1608
+ * property is expected, a value with no safe projection, or one past the size bounds. Refusing is
1609
+ * deliberate - a silent `null` would report "not a memory write" for a call nobody could classify,
1610
+ * and memory protection would be skipped exactly where the input was strange. Callers that sit in a
1611
+ * host hook MUST catch it and turn it into a decision; `guardMemoryWrite` and
1612
+ * `MemoryGuardManager.handleBeforeToolCall` already do.
1204
1613
  */
1205
1614
  declare function classifyMemoryRead(event: unknown, ctx: unknown, opts?: ClassifyMemoryReadOptions): boolean;
1206
1615
 
@@ -1225,10 +1634,10 @@ interface HookDecision {
1225
1634
  * failure, a rolled-back version), and a host must never re-judge its way past
1226
1635
  * one.
1227
1636
  *
1228
- * Absent or false means the guard reached no decision — it was inside its cache
1229
- * window, chain was unreachable, the scan never ran, the file it can vouch for
1230
- * is not the file being read, or it is in observe mode. Those calls are
1231
- * unaudited: fall through to your own judge exactly as for a `null` return.
1637
+ * Absent or false means the guard reached no decision — for example, the scan
1638
+ * never ran or this manager does not own the requested file. In enforce mode,
1639
+ * failed chain verification of the managed file is a block, never an unaudited
1640
+ * allow. In observe mode unverified calls remain unaudited.
1232
1641
  *
1233
1642
  * So the host rule is:
1234
1643
  * `if (d.block) deny; else if (d.audited) allow; else judge it yourself;`
@@ -1247,6 +1656,17 @@ interface MemoryGuardManagerOptions {
1247
1656
  workspaceDir: string;
1248
1657
  /** Absolute MEMORY.md path. Default `<workspaceDir>/MEMORY.md`. */
1249
1658
  memoryFilePath?: string;
1659
+ /**
1660
+ * Root this host keeps memory under, when it is not an agent home.
1661
+ *
1662
+ * Only set this from a value the host stated outright — never from
1663
+ * `process.cwd()`. A guessed root makes whichever directory the host started
1664
+ * in look like a memory store, which is how a stray MEMORY.md once ended up
1665
+ * inside a plugin checkout.
1666
+ */
1667
+ memoryRoot?: string;
1668
+ /** Agent home directory names to accept beyond the built-in list. */
1669
+ agentHomeDirs?: string[];
1250
1670
  /** Persistent pointer file. Default `<workspaceDir>/.atbash/memory-pointer.json`. */
1251
1671
  pointerFilePath?: string;
1252
1672
  /** File-backed log. Default `<workspaceDir>/.atbash/plugin.log`. */
@@ -1293,11 +1713,45 @@ declare class MemoryGuardManager {
1293
1713
  private readonly pointerStore;
1294
1714
  private readonly logger;
1295
1715
  private readonly memoryFilePath;
1716
+ /** Base for resolving the relative paths a host sends. */
1717
+ private readonly workspaceDir;
1718
+ /** Set by `verifyOneFile` when the file it checked was actually on chain. */
1719
+ private lastVerified;
1296
1720
  private readonly ttlMs;
1297
1721
  private readonly rollbackMinScore;
1298
1722
  private readonly enforce;
1299
1723
  private readonly agentPubkeyHex;
1724
+ /**
1725
+ * Memoized org→chain resolution. Reads have to hit the SAME chain
1726
+ * writes did, so an org-scoped guard must resolve `orgName` to
1727
+ * network exactly like `commitMemoryVersion` does. Without this
1728
+ * cache the read path would either (a) hit the SDK-default chain
1729
+ * every time — silently returning "no active memory on chain" when
1730
+ * writes landed on the org's actual chain, or (b) hammer
1731
+ * `/api/org-network` on every read. `undefined` means "not yet
1732
+ * resolved"; a resolved `null` means "no org / use raw chainOpts".
1733
+ */
1734
+ private _resolvedChainOpts;
1735
+ private _resolveChainInflight?;
1300
1736
  constructor(opts: MemoryGuardManagerOptions);
1737
+ /**
1738
+ * Carry this manager's host into `chainOpts`.
1739
+ *
1740
+ * Memory reads reach the chain through that host, and they are handed only
1741
+ * `chainOpts` — without this a self-hosted deployment would read from the
1742
+ * compiled-in default host while writing to its own.
1743
+ */
1744
+ private withEndpoint;
1745
+ /**
1746
+ * Resolve `orgName` → chain once, cache forever. `commitMemoryVersion`
1747
+ * already does this for writes; without the same call on the read
1748
+ * path, a client on the SDK's baked default chain reads from the wrong
1749
+ * chain and reports "no active memory" for an agent whose writes did
1750
+ * land — on the org's actual chain. An explicit `chainOpts.blockchainRid`
1751
+ * still wins (caller vouched for it); everything else honors the
1752
+ * dashboard's `org_networks` map.
1753
+ */
1754
+ private resolveChainOpts;
1301
1755
  /**
1302
1756
  * One-shot chain probe at plugin registration. Refreshes MEMORY.md
1303
1757
  * from chain when drifted and score passes threshold. Fire-and-forget
@@ -1305,15 +1759,27 @@ declare class MemoryGuardManager {
1305
1759
  */
1306
1760
  runBootProbe(): Promise<void>;
1307
1761
  /**
1308
- * Returns a `HookDecision` when the guard reached a decision about this event.
1309
- * Returns `null` when it did not — either the event isn't memory-related, or it
1310
- * is but the guard could not check it. In both cases the host falls through to
1311
- * its own audit.
1762
+ * Bring one chain file into step with its local counterpart.
1763
+ *
1764
+ * Only a file that already exists locally is touched. Restoring one that is
1765
+ * absent would mean choosing where to create it, and the root is a guess the
1766
+ * moment more than one could apply — the same guess that once wrote a
1767
+ * MEMORY.md into a plugin checkout.
1768
+ *
1769
+ * @returns What happened, for the single summary line the caller logs.
1770
+ */
1771
+ private syncOneAtBoot;
1772
+ /**
1773
+ * Where a chain slot's file lives locally, or null when we do not have it.
1312
1774
  *
1313
- * A returned decision carries `audited` (see `HookDecision`). Only
1314
- * `{ allow: true, audited: true }` means "checked and cleared"; anything else
1315
- * that allows is a call the host still needs to judge.
1775
+ * A stated root is tried first, then each agent home that actually exists in
1776
+ * the user's home directory — the same rule that produced the slot, run
1777
+ * backwards. Only an existing file is returned: creating one would mean
1778
+ * picking a root, and picking is guessing.
1316
1779
  */
1780
+ private locate;
1781
+ /** Roots a chain slot may be joined to, most specific first. */
1782
+ private memoryRoots;
1317
1783
  handleBeforeToolCall(event: unknown, ctx: unknown): Promise<HookDecision | null>;
1318
1784
  private mapGuardResult;
1319
1785
  /**
@@ -1331,9 +1797,70 @@ declare class MemoryGuardManager {
1331
1797
  * also covers events carrying two different path keys, where the classifier
1332
1798
  * and the host could otherwise disagree about which one is authoritative.
1333
1799
  */
1334
- private vouchesForTarget;
1800
+ /**
1801
+ * The memory files this tool call reads, as absolute paths.
1802
+ *
1803
+ * Relative paths resolve against the workspace, not `process.cwd()`. The host
1804
+ * sends paths relative to the agent's workspace; resolving them against the
1805
+ * process's own directory made the guard protect whichever directory the host
1806
+ * happened to start in, which on one machine was a plugin checkout.
1807
+ *
1808
+ * @returns Every named target, de-duplicated; empty when the call names none.
1809
+ */
1810
+ private readTargets;
1811
+ /**
1812
+ * Say what the read classifier saw and what it concluded.
1813
+ *
1814
+ * The write path has `emitDebugProbe`; this is its counterpart, so a tool
1815
+ * call can be followed across the fork instead of only after it. `false` here
1816
+ * is the normal answer for a write — it means the call goes to the write
1817
+ * classifier next, not that it was ignored.
1818
+ *
1819
+ * @param isMemoryRead - What `classifyMemoryRead` returned.
1820
+ */
1821
+ private emitReadProbe;
1822
+ /**
1823
+ * The chain slot for a path, or null when the file gets no chain record.
1824
+ *
1825
+ * One helper so the boot probe and every read agree: taking a basename in one
1826
+ * place and a relative path in the other would verify a file against a record
1827
+ * that is not its own.
1828
+ */
1829
+ private slotFor;
1830
+ /**
1831
+ * Verify every memory file this call reads, each against its own chain slot.
1832
+ *
1833
+ * A call naming several files is only cleared when all of them are, so one
1834
+ * unverifiable file cannot ride along with a verified one.
1835
+ *
1836
+ * @returns A block for the first file that fails, an audited allow when all
1837
+ * were verified, or null when none of them is on chain.
1838
+ */
1335
1839
  private handleMemoryRead;
1840
+ /**
1841
+ * Check one memory file against the chain slot of the same name.
1842
+ *
1843
+ * A file with no chain version is passed through rather than blocked: there
1844
+ * is no claim to protect, so refusing would only stop an agent reading memory
1845
+ * it has never committed. A file that IS on chain must match, or the read is
1846
+ * refused — that is the whole guarantee.
1847
+ *
1848
+ * @returns A decision when the read must not proceed, else null.
1849
+ */
1850
+ private verifyOneFile;
1851
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
1852
+ flushLogs(): Promise<void>;
1853
+ /**
1854
+ * A read this guard could not verify.
1855
+ *
1856
+ * Enforcing, that is a block. Observing, the host audits it as usual — but
1857
+ * the reason is said out loud either way, or an unverified read looks
1858
+ * identical to a clean one in the log.
1859
+ */
1860
+ private unverifiedRead;
1336
1861
  private writeMemoryAtomic;
1862
+ /** Replace `target` in one step, so a reader never sees a half-written file. */
1863
+ private writeFileAtomic;
1337
1864
  }
1338
1865
  declare function createMemoryGuardManager(opts: MemoryGuardManagerOptions): MemoryGuardManager;
1339
1866
 
@@ -1367,6 +1894,20 @@ interface TelemetryConfig {
1367
1894
  * `{ Authorization: "Bearer <hex>" }`). Required to actually export.
1368
1895
  */
1369
1896
  getAuthHeaders?: () => Record<string, string>;
1897
+ /**
1898
+ * Hashes the agent key before it is emitted on a wide event.
1899
+ *
1900
+ * Defaults to an unsalted SHA-256, which is de-identification rather than
1901
+ * anonymisation: agent pubkeys are public on chain and can be enumerated, so
1902
+ * the mapping is recoverable by anyone holding that list. Supply a salted
1903
+ * hasher to make it uncorrelatable, at the cost of joining across
1904
+ * deployments.
1905
+ *
1906
+ * Injected rather than imported so the telemetry modules need nothing from
1907
+ * the native addon. Left unset, the agent field is dropped from events
1908
+ * rather than falling back to the raw key.
1909
+ */
1910
+ hashAgentKey?: (pubkeyHex: string) => string;
1370
1911
  }
1371
1912
  declare function setupTelemetry(config: TelemetryConfig): void;
1372
1913
  /**
@@ -1389,6 +1930,111 @@ declare function flushTelemetry(): Promise<void>;
1389
1930
  */
1390
1931
  declare function shutdownTelemetry(): Promise<void>;
1391
1932
 
1933
+ /**
1934
+ * Wide events: one row per SDK operation, alongside the counters.
1935
+ *
1936
+ * The counters in `telemetry.ts` are aggregated inside this process before
1937
+ * export, so Honeycomb never receives a per-operation row. That is fine for
1938
+ * "is it up" and useless for everything else: a question nobody attached an
1939
+ * attribute for in advance cannot be answered later, because the data to
1940
+ * answer it never existed. "Which org sees the most failures", "what preceded
1941
+ * a fail-closed", "is the new release slower" are all that shape.
1942
+ *
1943
+ * So each operation also emits one event carrying its whole context. The
1944
+ * counters stay — they are the right tool for health boards and alerts, and
1945
+ * they are not sampled.
1946
+ *
1947
+ * TWO RULES SHAPE THIS MODULE.
1948
+ *
1949
+ * The payload is METADATA ONLY, and the allowlist below is the whole of it:
1950
+ * function name, outcome, latency, error class, error status and path, SDK
1951
+ * version, chain, and a hash of the agent key. Action text, tool arguments, policy text, organisation
1952
+ * names and keys never appear — not as a field that is emptied, but as a field
1953
+ * that does not exist, so there is nothing to forget to strip.
1954
+ *
1955
+ * The agent key is hashed, never sent raw, and the hasher is INJECTED rather
1956
+ * than imported: this module needs nothing from the native addon. With no
1957
+ * hasher supplied the field is DROPPED rather than falling back to the raw
1958
+ * key, so the guarantee does not depend on wiring somebody could forget.
1959
+ *
1960
+ * NOTE on what hashing does and does not buy. Agent pubkeys are public on
1961
+ * chain and can be enumerated, so an UNSALTED hash is reversible by anyone who
1962
+ * can read the chain: it is de-identification, not anonymisation. A salted
1963
+ * hasher makes it uncorrelatable and costs cross-deployment joins. Which one
1964
+ * is wanted is the caller's decision; this module only guarantees the raw key
1965
+ * never leaves.
1966
+ *
1967
+ * And the richer payload has its own switch. Counters carry a function name
1968
+ * and a source. These events carry agent and org identity off a customer's
1969
+ * machine, which is a different proposition for a security product, so
1970
+ * `ATBASH_TELEMETRY_EVENTS_DISABLED=1` turns them off while leaving the health
1971
+ * counters on. Both remain subject to the existing global opt-out.
1972
+ */
1973
+
1974
+ /** Attributes an event may carry. Anything not listed here cannot be emitted. */
1975
+ interface OperationEvent {
1976
+ /** SDK function, matching the counter's `function.name` so the two join. */
1977
+ functionName: string;
1978
+ durationMs: number;
1979
+ status: "success" | "error";
1980
+ /**
1981
+ * Hex pubkey of the agent the call was made as. Hashed before it is emitted,
1982
+ * and omitted entirely when no hasher is configured.
1983
+ */
1984
+ agentPubkey?: string;
1985
+ /**
1986
+ * Blockchain RID of the chain this call concerned.
1987
+ *
1988
+ * The RID rather than a network label: it is public infrastructure, it is
1989
+ * unambiguous, and it identifies the deployment without naming a customer.
1990
+ */
1991
+ chain?: string;
1992
+ /** Constructor name of the thrown error, e.g. `AtbashAPIError`. */
1993
+ errorClass?: string;
1994
+ /** HTTP status, or 0 when the request never completed. */
1995
+ errorStatus?: number;
1996
+ /**
1997
+ * Request path the status came from, e.g. `/api/v1/judge`.
1998
+ *
1999
+ * The class alone does not say what failed: several routes answer one call
2000
+ * and more than one can 404, so `AtbashAPIError` cannot distinguish "this
2001
+ * agent is not registered" from "the chain-routing lookup did not resolve".
2002
+ * The path settles it.
2003
+ *
2004
+ * Carries no query string — `requestPath` strips it at the point the error
2005
+ * is built, because a query string carries org names, tool call ids and the
2006
+ * agent pubkey. Structured and already sanitised, so this stays inside the
2007
+ * metadata rule where an error message would not.
2008
+ */
2009
+ errorPath?: string;
2010
+ /**
2011
+ * Short categorical result, when "success" is too coarse — e.g. a memory
2012
+ * write that scanned clean but never reached the chain.
2013
+ *
2014
+ * Categorical by contract: a value a caller chooses from a fixed vocabulary,
2015
+ * never free text and never anything derived from user content.
2016
+ */
2017
+ outcome?: string;
2018
+ }
2019
+ /** Hashes an agent key for emission. Supplied by `setupTelemetry`. */
2020
+ type AgentKeyHasher = (pubkeyHex: string) => string;
2021
+ declare function configureEvents(next: Logger | null, opts?: {
2022
+ version?: string;
2023
+ source?: string;
2024
+ hashAgentKey?: AgentKeyHasher;
2025
+ }): void;
2026
+ /** True when an event would actually be emitted. Exported for tests and callers that build context lazily. */
2027
+ declare function eventsAreOn(): boolean;
2028
+ /**
2029
+ * Emit one wide event for a completed operation.
2030
+ *
2031
+ * Never throws and never blocks: telemetry failing must not change what the
2032
+ * caller observes. Does nothing when events are off.
2033
+ *
2034
+ * @param e - The completed operation.
2035
+ */
2036
+ declare function recordEvent(e: OperationEvent): void;
2037
+
1392
2038
  /**
1393
2039
  * Rust core does the work; `browser/encrypted-toolcall.ts` mirrors it for the
1394
2040
  * browser and must produce the same columns.
@@ -1486,4 +2132,4 @@ declare function containsSecret(text: string): boolean;
1486
2132
  declare function createMemorySnapshot(entries: MemoryEntry[], takenAt: number): MemorySnapshot;
1487
2133
  declare function diffMemorySnapshots(before: MemorySnapshot, after: MemorySnapshot): MemoryDiffResult;
1488
2134
 
1489
- export { type ActionType, type AgentAuth, type AgentLookupOptions, type AgentMemoryEntry, type AgentPolicy, type AnomalySeverity, type AnomalyType, Atbash, AtbashAPIError, type AtbashLogger, type AtbashOptions, type AtbashUserConfig, BOOT_SYNC_HINT, type ChainConfig, type ChainOpts, type ClassifierToolContext, type ClassifierToolEvent, type ClassifyMemoryReadOptions, type ClassifyMemoryWriteOptions, type ClientSource, type CommitMemoryOptions, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, DEFAULT_MEMORY_PATH_PATTERNS, DEFAULT_MEMORY_READ_TOOL_NAMES, DEFAULT_MEMORY_WRITE_TOOL_NAMES, type Decision, type DecisionVerdict, EciesDomain, type EncryptedMemory, type Envelope, type FromConfigOptions, type GuardLogger, type GuardMemoryDecision, type GuardMemoryWriteInput, type GuardMemoryWriteResult, type HeldAction, type HeldActionReview, type HookDecision, HttpClient, HttpTransportError, type JudgeEndpointConfig, type JudgeOptions, type JudgeResult, type JudgmentState, type JudgmentStatus, KEY_FILENAMES, type KeyPair, type LogToolCallOptions, type LogToolCallResult, type MemoryAnomaly, type MemoryDiffResult, type MemoryEntry, MemoryGuardManager, type MemoryGuardManagerOptions, MemoryIntegrityError, type MemoryPointer, type MemoryRollbackEvent, type MemoryScanOptions, type MemoryScanResult, type MemoryScanVerdict, type MemorySnapshot, type ModifiedEntry, type Network, type OrgSubscription, PRIVATE_CHAIN, PUBLIC_CHAIN, PointerStore, type Provider, type PubkeyValue, type RedactResult, type RollbackMemoryOptions, type SecretKind, type SecretMatch, SignatureVerificationError, type Subscription, type SyncMemoryOptions, type SyncMemoryResult, type TelemetryConfig, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, type WrappedLogger, bootSyncFailureLine, buildAllowedJudgeHosts, chainForNetwork, chooseKeyPath, claimHashHex, classifyMemoryRead, classifyMemoryWrite, columnAad, commitMemoryVersion, containsEvasionCharacters, containsSecret, createFileLogger, createMemoryGuardManager, createMemorySnapshot, decryptForOrg, decryptMemoryContent, defaultPluginLogPath, defaultPointerPath, deriveMemoryKey, derivePublicKey, diffMemorySnapshots, encryptForOrg, encryptMemoryContent, encryptedLength, flushTelemetry, generateKeypair, getActiveAgentMemory, getActiveMemoryId, getConfigDir, getConfigPath, getMemoryById, getMemoryHistory, getRecentAgentMemory, getRollbackHistory, guardMemoryWrite, isEnvelope, isValidPrivateKey, keyFingerprintOf, keyPathCandidates, loadAgent, loadAgentFromFile, loadUserConfig, normalizeActionForHash, normalizeForMatching, normalizeStatus, normalizeVerdict, packEnvelope, parseEnvelope, pubkeyToHex, recordCall, recordDuration, redactJsonStrings, redactSecrets, resolve, resolveKeyPath, rollbackMemory, saveUserConfig, scanMemory, scanMemoryBatch, setupTelemetry, shutdownTelemetry, signEncryptedToolCall, signJudgeAction, signLogToolCall, syncLocalMemory, validateJudgeEndpoint, verifyJudgeResponseSignature, verifySignature };
2135
+ export { type ActionType, type AgentAuth, type AgentLookupOptions, type AgentMemoryEntry, type AgentPolicy, type AnomalySeverity, type AnomalyType, Atbash, AtbashAPIError, type AtbashLogger, type AtbashOptions, type AtbashUserConfig, BOOT_SYNC_HINT, type ChainConfig, type ChainOpts, type ClassifierToolContext, type ClassifierToolEvent, type ClassifyMemoryReadOptions, type ClassifyMemoryWriteOptions, type ClientSource, type CommitMemoryOptions, DEFAULT_ENDPOINT, DEFAULT_MEMORY_PATH_PATTERNS, DEFAULT_MEMORY_READ_TOOL_NAMES, DEFAULT_MEMORY_WRITE_TOOL_NAMES, type Decision, type DecisionVerdict, EciesDomain, type EncryptedMemory, type Envelope, type FromConfigOptions, type GuardLogger, type GuardMemoryDecision, type GuardMemoryWriteInput, type GuardMemoryWriteResult, type HeldAction, type HeldActionReview, type HookDecision, HttpClient, HttpTransportError, type JudgeEndpointConfig, type JudgeOptions, JudgeRefusalError, type JudgeResult, type JudgmentState, type JudgmentStatus, KEY_FILENAMES, type KeyPair, type LogToolCallOptions, type LogToolCallResult, type MemoryAnomaly, type MemoryDiffResult, type MemoryEntry, MemoryGuardManager, type MemoryGuardManagerOptions, type MemoryHistoryPage, MemoryIntegrityError, type MemoryPointer, type MemoryRollbackEvent, type MemoryScanOptions, type MemoryScanResult, type MemoryScanVerdict, type MemorySnapshot, type ModifiedEntry, type Network, type OperationEvent, type OrgSubscription, PRIVATE_CHAIN, PointerStore, type Provider, type PubkeyValue, type RedactResult, type RollbackMemoryOptions, type SecretKind, type SecretMatch, SignatureVerificationError, type Subscription, type SyncMemoryOptions, type SyncMemoryResult, type TelemetryConfig, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, type VerdictRecord, type WrappedLogger, bootSyncFailureLine, buildAllowedJudgeHosts, canonicalAllow, chainForNetwork, chooseKeyPath, claimHashHex, classifyMemoryRead, classifyMemoryWrite, columnAad, commitMemoryVersion, configureEvents, configureJailHold, containsEvasionCharacters, containsSecret, createFileLogger, createMemoryGuardManager, createMemorySnapshot, decryptForOrg, decryptMemoryContent, defaultPluginLogPath, defaultPointerPath, deriveMemoryKey, derivePublicKey, diffMemorySnapshots, encryptForOrg, encryptMemoryContent, encryptedLength, eventsAreOn, flushTelemetry, generateKeypair, getActiveAgentMemory, getActiveMemoryId, getConfigDir, getConfigPath, getMemoryById, getMemoryHistory, getRecentAgentMemory, getRollbackHistory, guardMemoryWrite, isEnvelope, isJailHeld, isValidPrivateKey, judgeVerifyKeys, keyFingerprintOf, keyPathCandidates, loadAgent, loadAgentFromFile, loadUserConfig, memorySlotOf, normalizeActionForHash, normalizeActionType, normalizeForMatching, normalizeStatus, normalizeVerdict, packEnvelope, parseEnvelope, pubkeyToHex, recordCall, recordDuration, recordEvent, redactJsonStrings, redactSecrets, releaseJailHolds, resolve, resolveKeyPath, rollbackMemory, saveUserConfig, scanMemory, scanMemoryBatch, setLoopbackJudgePinsForTests, setupTelemetry, shutdownTelemetry, signEncryptedToolCall, signJudgeAction, signLogToolCall, syncLocalMemory, validateJudgeEndpoint, verifyJudgeResponseSignature, verifySignature };