@atbash/sdk 0.10.12-dev.0 → 0.11.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.mts 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;
@@ -30,6 +33,19 @@ interface ValidatedEndpoint {
30
33
  declare function buildAllowedJudgeHosts(): ReadonlySet<string>;
31
34
  declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
32
35
 
36
+ interface ChainConfig {
37
+ readonly network: Network;
38
+ readonly blockchainRid: string;
39
+ readonly nodeUrls: readonly string[];
40
+ }
41
+ declare const PRIVATE_CHAIN: ChainConfig;
42
+ /**
43
+ * The chain a network name selects. Only `"private"` exists; any other value,
44
+ * including `"public"` from an untyped caller, is refused with the same
45
+ * message the config switch gives, never routed to private in silence.
46
+ */
47
+ declare function chainForNetwork(network: Network): ChainConfig;
48
+
33
49
  /**
34
50
  * User-facing types. Two groups:
35
51
  * - Core types — the exact shapes the Rust core emits across the NAPI
@@ -118,23 +134,31 @@ interface MemoryScanOptions {
118
134
  toolArgsJson?: string;
119
135
  }
120
136
  /**
121
- * Which Atbash chain an action runs against. `public` is the shared
122
- * testnet (Free plan); `private` is reserved for Private / Swarm /
123
- * Enterprise tier subscribers. The SDK resolves which one to use from
124
- * the org's subscription via `orgName` — callers normally don't need
125
- * to pass this manually.
137
+ * Chain selector. Only `"private"` exists: the public testnet chains are
138
+ * retired. The SDK resolves an org's chain through the Dashboard via
139
+ * `orgName` — callers normally don't need to pass this manually.
126
140
  */
127
- type Network = "public" | "private";
141
+ type Network = "private";
128
142
  /**
129
143
  * Per-call chain overrides. Pass exactly what you need to override —
130
- * `network` selects between the SDK's known chains, `blockchainRid` /
131
- * `nodeUrls` pin a fully custom chain. Pass none of these to fall back
132
- * to the SDK's defaults (or to org-resolved chain when `orgName` is set).
144
+ * `network` selects between the SDK's known chains, `blockchainRid` pins a
145
+ * custom one, and `endpoint` names the Dashboard the calls travel over. Pass
146
+ * none of these to fall back to the SDK's defaults (or to the org-resolved
147
+ * chain when `orgName` is set).
133
148
  */
134
149
  interface ChainOpts {
135
150
  network?: Network;
136
151
  blockchainRid?: string;
152
+ /**
153
+ * No longer dialled: memory reaches the chain through the Dashboard. Kept
154
+ * because callers and the other bindings still carry it as chain config.
155
+ */
137
156
  nodeUrls?: readonly string[];
157
+ /**
158
+ * Dashboard base URL. Memory reaches the chain through it, so this is where
159
+ * those requests go; defaults to the endpoint compiled into the core.
160
+ */
161
+ endpoint?: string;
138
162
  }
139
163
  /** Canonical verdict after normalization. */
140
164
  type Verdict = "ALLOW" | "HOLD" | "BLOCK" | "No verdict";
@@ -160,6 +184,11 @@ interface LogToolCallResult {
160
184
  }
161
185
  interface JudgeResult {
162
186
  verdict: Verdict;
187
+ /**
188
+ * Canonical executable permission, computed by `canonicalAllow` — see that
189
+ * function for the rule. Never more permissive than `auditToolCall`.
190
+ */
191
+ allow: boolean;
163
192
  actionType: string;
164
193
  reason: string;
165
194
  confidence: number;
@@ -173,6 +202,8 @@ interface JudgeResult {
173
202
  * Whether the org's protection mode actually acts on a HOLD/BLOCK verdict.
174
203
  * `false` in Monitor mode (verdict is logged for observation, agent is NOT
175
204
  * jailed and the action is not held); `true` in Enforce mode.
205
+ * A plain boolean: an omitted field reads as `false`. `Decision.enforced`
206
+ * keeps "unknown" apart; gate on that, not on this.
176
207
  */
177
208
  enforced: boolean;
178
209
  /** Server-reported protection mode: `off`, `monitor`, or `enforce`. */
@@ -184,10 +215,17 @@ interface JudgeResult {
184
215
  * missing" — never infer the former from a null verdict alone.
185
216
  */
186
217
  status: string;
218
+ /**
219
+ * The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
220
+ * "HOLD", or "" when it neither refused nor is a known refusal shape.
221
+ */
222
+ refusal?: "" | "BLOCK" | "HOLD";
187
223
  }
188
224
  interface JudgmentStatus {
189
225
  status: JudgmentState;
190
226
  verdict: Verdict;
227
+ /** Same canonical executable permission as {@link JudgeResult.allow}. */
228
+ allow: boolean;
191
229
  reason: string;
192
230
  judgmentId: string;
193
231
  onChain?: boolean;
@@ -289,8 +327,32 @@ interface AtbashLogger {
289
327
  /** Options accepted by the `Atbash` constructor. */
290
328
  interface AtbashOptions {
291
329
  endpoint?: string;
330
+ /**
331
+ * Overall deadline for one `auditToolCall` decision, across every request
332
+ * it makes (org lookup, exists check, judge). Default 60 000 ms. When it
333
+ * passes the decision is an ERROR (denied unless `failClosed` is false).
334
+ * `timeoutMs` still bounds each single request.
335
+ */
336
+ decisionTimeoutMs?: number;
292
337
  timeoutMs?: number;
338
+ /**
339
+ * Full chain override — BRID + nodeUrls in one object. Wins over every
340
+ * other chain selector. Prefer this over paired `nodeUrls`/`blockchainRid`
341
+ * for anything but backwards compatibility.
342
+ */
343
+ chain?: ChainConfig;
344
+ /**
345
+ * Preset chain selector — `"private"` is the only value. Resolves to the
346
+ * matching `ChainConfig` via `chainForNetwork()`. Overridden by `chain`,
347
+ * overrides env `ATBASH_DEFAULT_CHAIN_NETWORK` and the config file.
348
+ */
349
+ network?: Network;
350
+ /**
351
+ * Explicit node URLs. Must be paired with `blockchainRid`. Passing one
352
+ * without the other throws — a BRID/nodes mismatch 404s every request.
353
+ */
293
354
  nodeUrls?: readonly string[];
355
+ /** Explicit BRID. Must be paired with `nodeUrls`. See {@link nodeUrls}. */
294
356
  blockchainRid?: string;
295
357
  /**
296
358
  * Default org name. When set, `judgeAction` / `auditToolCall` resolve
@@ -315,8 +377,27 @@ interface AtbashOptions {
315
377
  * `orgEncryptionPubKey` overrides this, same as `verifyPubKey`.
316
378
  */
317
379
  orgEncryptionPubKey?: string;
318
- /** When true (default), `auditToolCall` denies on any error. */
380
+ /**
381
+ * When true (default), `auditToolCall` denies on any error. When false, it
382
+ * allows only when Atbash could not be reached or could not answer: a
383
+ * transport failure, a timeout, a 5xx or a 408 (and a readable reply that
384
+ * neither grants nor refuses). Atbash's own refusals still deny: an
385
+ * unregistered agent, any other 4xx (no policy, billing suspended, quota
386
+ * exhausted or rate limited, unauthorized), and a 2xx reply that is not a
387
+ * JSON object. Direct `judgeAction` callers get a `JudgeRefusalError` for
388
+ * an unregistered agent or an unreadable reply.
389
+ */
319
390
  failClosed?: boolean;
391
+ /**
392
+ * When true (the default), an answer that carries no enforcement decision
393
+ * (the audit tier's `{status:"logged"}` with no verdict) is refused:
394
+ * `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
395
+ * `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
396
+ * are false. Set it to `false` only to run deliberately in log-only mode
397
+ * (security review 2026-09-30, H6: an unsigned marker switched enforcement
398
+ * off).
399
+ */
400
+ requireEnforcement?: boolean;
320
401
  /**
321
402
  * Verbose diagnostics. Off by default. When on, a failed judge call also
322
403
  * logs the response body, which is the difference between "judge API failed"
@@ -332,6 +413,15 @@ interface Decision {
332
413
  verdict: DecisionVerdict;
333
414
  reason?: string;
334
415
  toolCallId?: string;
416
+ /**
417
+ * Whether the organisation acts on a HOLD/BLOCK verdict; false under Monitor mode and per-agent Shadow Mode. Absent means unknown: a
418
+ * transport error, or a judge that stated no mode. Test `=== false`, never
419
+ * `!decision.enforced`, or every outage counts as a would-block.
420
+ * Informational only; `allow` remains the gate.
421
+ */
422
+ enforced?: boolean;
423
+ /** Server-reported protection mode (`off`, `monitor`, `enforce`); absent when unknown. */
424
+ enforcementMode?: string;
335
425
  }
336
426
  /** Input to `auditToolCall`. */
337
427
  interface ToolCallInput {
@@ -357,12 +447,20 @@ interface FromConfigOptions {
357
447
  keyPath?: string;
358
448
  /** Judge endpoint config — validated against the allowlist / self-hosted policy. */
359
449
  judge?: JudgeEndpointConfig;
450
+ /** See {@link AtbashOptions.chain}. */
451
+ chain?: ChainConfig;
452
+ /** See {@link AtbashOptions.network}. */
453
+ network?: Network;
360
454
  blockchainRid?: string;
361
455
  timeoutMs?: number;
362
456
  nodeUrls?: readonly string[];
363
457
  /** Default org name — see {@link AtbashOptions.orgName}. */
364
458
  orgName?: string;
365
459
  failClosed?: boolean;
460
+ /** See {@link AtbashOptions.requireEnforcement}. Default true. */
461
+ requireEnforcement?: boolean;
462
+ /** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
463
+ decisionTimeoutMs?: number;
366
464
  /** See {@link AtbashOptions.debug}. */
367
465
  debug?: boolean;
368
466
  logger?: AtbashLogger;
@@ -371,8 +469,6 @@ interface FromConfigOptions {
371
469
  interface JudgeOptions {
372
470
  toolName?: string;
373
471
  toolArgsJson?: string;
374
- provider?: string;
375
- model?: string;
376
472
  verifyPubKey?: string;
377
473
  /** Per-call override of {@link AtbashOptions.orgEncryptionPubKey}. */
378
474
  orgEncryptionPubKey?: string;
@@ -408,12 +504,6 @@ interface LogToolCallOptions {
408
504
  orgEncryptionPubKey?: string;
409
505
  }
410
506
 
411
- interface ChainConfig {
412
- readonly network: Network;
413
- readonly blockchainRid: string;
414
- readonly nodeUrls: readonly string[];
415
- }
416
-
417
507
  declare class Atbash {
418
508
  readonly auth: AgentAuth;
419
509
  readonly endpoint: string;
@@ -427,6 +517,10 @@ declare class Atbash {
427
517
  readonly orgEncryptionPubKey?: string;
428
518
  /** When true (default), `auditToolCall` denies on any error. */
429
519
  readonly failClosed: boolean;
520
+ /** When true (default), an unenforced (audit-tier) answer is refused. */
521
+ readonly requireEnforcement: boolean;
522
+ /** Overall deadline for one auditToolCall decision, in ms. */
523
+ readonly decisionTimeoutMs: number;
430
524
  /** Org key learned from the last agent-exists check, for this agent only. */
431
525
  private _orgKeyFromChain;
432
526
  private readonly debug;
@@ -437,6 +531,27 @@ declare class Atbash {
437
531
  * calls don't re-hit the dashboard. Cleared by `clearChainCache()`.
438
532
  */
439
533
  private readonly _chainCache;
534
+ /**
535
+ * The chain the constructor settled on. Used only where a lookup returns no
536
+ * answer — see {@link resolveChainFromMap}.
537
+ */
538
+ private readonly _defaultChain;
539
+ /**
540
+ * True when the caller named a chain outright — `chain`, `network`, or the
541
+ * paired `blockchainRid` + `nodeUrls`.
542
+ *
543
+ * Such a client is never re-pointed: not by the migration switch, and not by
544
+ * where an org turns out to live. Naming a chain is the caller saying "talk
545
+ * to this one", and silently routing elsewhere would make the argument a
546
+ * suggestion. A client that names nothing is the one that follows the org.
547
+ */
548
+ private readonly _explicitChain;
549
+ /**
550
+ * The fleet-wide chain switch, read once at construction. `resolve()` hits
551
+ * the config file on disk, so re-reading it per call would put a file read
552
+ * on every judge.
553
+ */
554
+ private readonly _forcedNetwork;
440
555
  /**
441
556
  * Short-TTL cache for `/api/ai/exists`. The `registered` field is
442
557
  * monotonic (once true, stays true), so most calls in a burst re-fetch
@@ -447,12 +562,6 @@ declare class Atbash {
447
562
  */
448
563
  private _agentExistsCache;
449
564
  private static readonly AGENT_EXISTS_TTL_MS;
450
- /**
451
- * Cached bearer token for risk-engine / insurance read calls. Built
452
- * lazily as a signed `log_tool_call` tx and refreshed every 4 min so
453
- * server-side replay protection windows never expire it mid-session.
454
- */
455
- private _authBearer;
456
565
  /** Guards `logEnvironmentOnce` — hosts construct several clients. */
457
566
  private static environmentLogged;
458
567
  constructor(privkey: string, options?: AtbashOptions);
@@ -475,6 +584,48 @@ declare class Atbash {
475
584
  * validated against the trusted allowlist / self-hosted policy; a
476
585
  * self-hosted endpoint's `verifyPubKey` becomes the client default.
477
586
  */
587
+ /**
588
+ * Decide which key this client signs as.
589
+ *
590
+ * Most specific wins: a key passed in code, then a key file the host named,
591
+ * then the environment, then the default key file. `config.json` is absent
592
+ * from that list on purpose — it is a settings file, writable by anything
593
+ * that can write the user's home and read by every Atbash integration on the
594
+ * machine, so letting it name the key meant one stray value silently
595
+ * re-pointed who each of them signed as.
596
+ *
597
+ * A named `keyPath` that does not exist throws rather than falling through.
598
+ * The caller said which file holds its identity; quietly signing as a
599
+ * different one is the outcome worth refusing.
600
+ *
601
+ * @param options - The `fromConfig` options, read for `agentKey` and `keyPath`.
602
+ * @returns The resolved agent identity.
603
+ * @throws When a named key file is missing, or no identity can be found.
604
+ */
605
+ private static resolveIdentity;
606
+ /**
607
+ * Add the one thing a machine with no key file needs to hear.
608
+ *
609
+ * This is the only path where the operator cannot see what happened: no key
610
+ * file, and the key they are looking for sits in `config.json`, which no
611
+ * longer selects the identity. A bare "key file not found" sends them hunting
612
+ * for a key they already have. Only this branch gets the hint — a caller who
613
+ * named a `keyPath` is told about that path, where a stranded key is noise.
614
+ *
615
+ * @param err - The "key file not found" error from the default lookup.
616
+ * @returns The same error, or one that also names the key in config.json.
617
+ */
618
+ private static explainStrandedKey;
619
+ /**
620
+ * Say so when `config.json` still holds an agent key.
621
+ *
622
+ * Nothing signs with it any more, and silence would leave an operator
623
+ * believing one identity is in force while another is. The pubkey is named,
624
+ * not the key, so the line is safe in a log.
625
+ *
626
+ * @param logger - The host logger, when one was supplied.
627
+ */
628
+ private static warnOnStrandedKey;
478
629
  static fromConfig(options?: FromConfigOptions): Atbash;
479
630
  get pubkey(): string;
480
631
  get privkey(): string;
@@ -494,6 +645,13 @@ declare class Atbash {
494
645
  * return the signed tx hex. The server broadcasts to chain.
495
646
  */
496
647
  logToolCall(action: string, context?: string, options?: LogToolCallOptions): Promise<LogToolCallResult>;
648
+ /**
649
+ * `raisePreflightErrors` (the judge path, like Python's
650
+ * `raise_preflight_errors`): a failed registration check throws instead of
651
+ * becoming `{ success: false }`, so the caller can tell Atbash's "no"
652
+ * (not registered, or a 4xx) from an outage.
653
+ */
654
+ private _logToolCall;
497
655
  /**
498
656
  * Sign log_tool_call + optionally judge_action, POST /api/v1/judge.
499
657
  *
@@ -502,6 +660,15 @@ declare class Atbash {
502
660
  */
503
661
  judgeAction(action: string, context?: string, options?: JudgeOptions): Promise<JudgeResult>;
504
662
  private _judgeAction;
663
+ /**
664
+ * Start a local hold on an enforced, on-chain BLOCK (the verdict that jails).
665
+ * Refuse any other result that lands while the agent is held, or whose
666
+ * request was already in flight when the block arrived: the server judged it
667
+ * on pre-jail state.
668
+ */
669
+ private applyJailHold;
670
+ /** The server's jailed refusal, produced locally while the agent is held. */
671
+ private localJailBlock;
505
672
  /**
506
673
  * High-level guard: redact secrets, submit for judgement, and collapse the
507
674
  * result into an allow/deny `Decision`. Fails closed by default — any error
@@ -509,6 +676,24 @@ declare class Atbash {
509
676
  * explicitly false.
510
677
  */
511
678
  auditToolCall(input: ToolCallInput): Promise<Decision>;
679
+ /**
680
+ * The verify key for one call (R4, governor tier-2 review). A per-call key
681
+ * may only narrow what the client accepts: on a client pinned to a judge
682
+ * key it must be that same key, never a replacement; on an unpinned client
683
+ * it adds the check.
684
+ */
685
+ private effectiveVerifyKey;
686
+ /**
687
+ * Collapse a judge result into a {@link Decision}. The most restrictive
688
+ * signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
689
+ * and the canonical `allow` are each able to deny, and only a result where
690
+ * all three agree may execute. A present `allow` / `decision.allow` that is
691
+ * not exactly `true` is the judge declining to grant permission, so it
692
+ * denies even under `failClosed: false` — that option relaxes *errors*, it
693
+ * never overrides a refusal.
694
+ */
695
+ private decide;
696
+ private decideInner;
512
697
  /**
513
698
  * One exit for every judge failure.
514
699
  *
@@ -518,8 +703,24 @@ declare class Atbash {
518
703
  * The response body follows only under `debug`, since it can echo the action.
519
704
  */
520
705
  private failJudge;
706
+ /**
707
+ * The canonical permission for a response body under this client's
708
+ * settings: {@link canonicalAllow}, minus the audit tier when
709
+ * `requireEnforcement` is on. Never more permissive than `auditToolCall`.
710
+ */
711
+ private executable;
521
712
  private fail;
522
- getJudgmentStatus(judgmentId: string, agentPubkey?: string): Promise<JudgmentStatus>;
713
+ /**
714
+ * Return the current status of a previously submitted judgment.
715
+ *
716
+ * `chainOpts` names which chain the judgment was signed against; the SDK
717
+ * sends it as the `brid` query param the server routes by. Without it the
718
+ * read uses the same chain `judgeAction` signs for (see `callBrid`): the
719
+ * client's org chain when it has an `orgName`, else the client default.
720
+ * A judgment signed with an explicit per-call `chainOpts` (or a per-call
721
+ * `orgName` on another chain) must be polled with that same `chainOpts`.
722
+ */
723
+ getJudgmentStatus(judgmentId: string, agentPubkey?: string, chainOpts?: ChainOpts): Promise<JudgmentStatus>;
523
724
  getToolCalls(maxCount: number): Promise<ToolCallRecord[]>;
524
725
  getOrgToolCalls(orgName: string, maxCount: number): Promise<ToolCallRecord[]>;
525
726
  getAgentToolCalls(agentPubkey: string, maxCount: number): Promise<ToolCallRecord[]>;
@@ -549,10 +750,9 @@ declare class Atbash {
549
750
  * Resolve which chain an org's actions should run against. Cached
550
751
  * per-client by orgName. Resolution order:
551
752
  * 1. `org_networks` map (authoritative).
552
- * 2. Per-chain subscription fallback — public + private records
553
- * are fetched in parallel, with `is_private_blockchain` and
554
- * `assigned_at` reconciling mixed states.
555
- * Defaults to the public chain when nothing else resolves.
753
+ * 2. Subscription fallback — the org's subscription row on the chain.
754
+ * A lookup that finds the org wins outright. Where it finds nothing (a
755
+ * brand-new org), the client's configured default decides.
556
756
  */
557
757
  resolveChainForOrg(orgName: string): Promise<ChainConfig>;
558
758
  /**
@@ -580,6 +780,13 @@ declare class Atbash {
580
780
  * chains; otherwise the client's default.
581
781
  */
582
782
  private bridFromChainOpts;
783
+ /**
784
+ * The chain one call's transactions are signed for: the per-call override
785
+ * when given, otherwise the client's org, otherwise the client default.
786
+ * log_tool_call, judge_action and the judgment-status read all use this, so
787
+ * a request never pairs transactions signed for two different chains.
788
+ */
789
+ private callBrid;
583
790
  /**
584
791
  * Resolve the dashboard chain used by agent metadata/policy reads.
585
792
  * Explicit per-call network overrides win; otherwise use the supplied org
@@ -600,6 +807,36 @@ declare class Atbash {
600
807
  private riskEngineGet;
601
808
  private riskEnginePost;
602
809
  private riskEngineRecords;
810
+ /**
811
+ * BRID for an org — one round-trip to the map, honoring the client's chain
812
+ * cache. A "brand-new org" (nothing anywhere names its chain) is not an
813
+ * error — `resolveChainForOrg` returns the client default for that case and
814
+ * this helper returns its BRID. A transport failure or non-200 from
815
+ * `/api/org-network` IS an error and propagates: the caller cannot fall
816
+ * back to the default chain on outage, because with multi-chain live that
817
+ * silently reads from the wrong chain. Matches the Python binding's
818
+ * `_brid_for_org` semantics.
819
+ */
820
+ private bridForOrg;
821
+ /**
822
+ * BRID for the client's configured default org, if it has one.
823
+ *
824
+ * Calls that carry no `orgName` argument are not chain-less: they still
825
+ * belong to `this.orgName`, and that org lives on exactly one chain. Routing
826
+ * them by the constructor's chain instead means a client configured
827
+ * `network: "private"` reads the private chain for an org that lives on
828
+ * public, and gets an empty answer rather than an error. So where an org is
829
+ * known the org decides the chain, and the constructor's chain is what is
830
+ * left when no org is known at all — the order `resolveAgentLookupNetwork`
831
+ * already applies to agent metadata reads, and the order the dashboard
832
+ * applies in `resolveChainForWallet`.
833
+ *
834
+ * Undefined when there is no default org, so callers keep falling back to
835
+ * the client default.
836
+ */
837
+ /** The switch's chain, unless this client named one of its own. */
838
+ private forcedNetwork;
839
+ private defaultOrgBrid;
603
840
  private raiseIfError;
604
841
  /** Wrap a failed HTTP *response* (non-2xx / non-200) as an AtbashAPIError. */
605
842
  private httpError;
@@ -620,20 +857,48 @@ declare class Atbash {
620
857
  static containsEvasionCharacters(text: string): boolean;
621
858
  }
622
859
 
860
+ declare const DEFAULTS: {
861
+ ceilingMs: number;
862
+ pollMs: number;
863
+ };
864
+ /** Whether `pubkey` is currently held. */
865
+ declare function isJailHeld(pubkey: string): boolean;
866
+ /** Forget every hold and stop its poll; tests and hosts that reset state. */
867
+ declare function releaseJailHolds(): void;
868
+ /** Override the ceiling or poll interval; an empty object restores defaults. */
869
+ declare function configureJailHold(next: Partial<typeof DEFAULTS>): void;
870
+
623
871
  declare const DEFAULT_ENDPOINT: string;
624
- declare const DEFAULT_CHROMIA_NODE_URLS: readonly string[];
625
- declare const DEFAULT_BLOCKCHAIN_RID: string;
626
872
 
627
873
  declare class AtbashAPIError extends Error {
628
874
  /** HTTP status code (or 0 if the request never completed). */
629
875
  readonly status: number;
630
876
  /** Raw response body text (may be empty). */
631
877
  readonly body: string;
632
- constructor(status: number, body: string, statusText?: string, endpoint?: string);
878
+ /**
879
+ * Request path this status came from, e.g. `/api/v1/judge` — absent when the
880
+ * request never reached a response (transport failures carry status 0).
881
+ *
882
+ * A status alone does not say what failed. Several routes answer one call, and
883
+ * more than one of them can 404: a caller reading only `404` cannot tell "this
884
+ * agent is not registered" from "the chain-routing lookup did not resolve", and
885
+ * has to guess from the body's prose — which a 404 raised before any handler
886
+ * ran does not carry. The path is the fact that settles it.
887
+ */
888
+ readonly path?: string;
889
+ constructor(status: number, body: string, statusText?: string, endpoint?: string, path?: string);
633
890
  }
634
891
  declare class SignatureVerificationError extends Error {
635
892
  constructor(message: string);
636
893
  }
894
+ /**
895
+ * Atbash answered, and the answer was not a grant: the agent is not
896
+ * registered, or the reply could not be read. Unlike an unreachable judge this
897
+ * always denies, whatever `failClosed` says.
898
+ */
899
+ declare class JudgeRefusalError extends Error {
900
+ constructor(message: string);
901
+ }
637
902
 
638
903
  /**
639
904
  * Thin typed fetch wrapper.
@@ -672,9 +937,25 @@ declare class HttpTransportError extends Error {
672
937
  });
673
938
  }
674
939
 
940
+ declare function canonicalAllow(data: Record<string, unknown>): boolean;
941
+
675
942
  /** Normalize wire shapes (verdict casing, status, pubkey) to canonical forms. */
676
943
 
677
944
  declare function normalizeVerdict(raw: unknown): Verdict;
945
+ /**
946
+ * Canonicalize `action_type` at the wire boundary, the way
947
+ * {@link normalizeVerdict} already canonicalizes `verdict`.
948
+ *
949
+ * `verdict` has been normalized here since the beginning and has never
950
+ * drifted between consumers. `action_type` was passed through raw, so every
951
+ * reader invented its own folding policy — `auditToolCall` compared exactly
952
+ * while `memory/scan.ts` trimmed and case-folded, and the same `" ALLOW "`
953
+ * was therefore an error on one path and permission on the other.
954
+ *
955
+ * Trim and case-fold only. Zero-width and homoglyph variants survive
956
+ * untouched, stay outside the known set, and still fail closed.
957
+ */
958
+ declare function normalizeActionType(raw: unknown): string;
678
959
  declare function normalizeStatus(raw: unknown): JudgmentState;
679
960
  /** Wire pubkey may be a hex string, a Buffer/Uint8Array, or `{ data: [...] }`. */
680
961
  declare function pubkeyToHex(val: unknown): string;
@@ -683,9 +964,20 @@ interface AtbashUserConfig {
683
964
  agentKey?: string;
684
965
  orgName?: string;
685
966
  judgeEndpoint?: string;
686
- blockchainRid?: string;
687
- provider?: string;
688
- providerModel?: string;
967
+ /**
968
+ * Response-signing pubkey of a self-hosted judge (66 hex). Setting it makes
969
+ * `fromConfig` use the self-hosted policy for `judgeEndpoint`, which is the
970
+ * only way a non-allowlisted judge host is accepted.
971
+ */
972
+ judgeVerifyPubKey?: string;
973
+ /**
974
+ * `"private"` pins every org to the private chain regardless of where the
975
+ * dashboard says it lives — the migration switch. Leave it unset for the
976
+ * normal mode, where each org's own chain decides. There is no `"public"`
977
+ * value; a caller that wants one specific chain passes `chain` or `network`
978
+ * at construction instead.
979
+ */
980
+ defaultChainNetwork?: Network;
689
981
  /** "1" / "true" turns on verbose diagnostics. See AtbashOptions.debug. */
690
982
  debug?: string;
691
983
  }
@@ -785,15 +1077,27 @@ declare function scanMemoryBatch(entries: MemoryEntry[], auth: AgentAuth, opts?:
785
1077
  interface CommitMemoryOptions {
786
1078
  /** Score in [1, 10]. Defaults to 5 (matches the Rell entity default). */
787
1079
  score?: number;
1080
+ /**
1081
+ * Which memory file this commit targets. Defaults to `""` — the
1082
+ * un-pathed slot, matching Rell's `file_path: text = ""` default.
1083
+ * Pass an explicit filename (e.g. `"AGENTS.md"`) to keep files
1084
+ * versioned independently on chain.
1085
+ */
1086
+ filePath?: string;
788
1087
  /** Org name — when set, the SDK resolves which chain the agent lives on. */
789
1088
  orgName?: string;
790
- /** Atbash service endpoint for org→chain lookup. */
1089
+ /** Atbash service endpoint — where memory reaches the chain, and the org→chain lookup. */
791
1090
  endpoint?: string;
1091
+ /** Self-hosted judge response-signing pubkey; required with a non-allowlisted `endpoint`. */
1092
+ verifyPubKey?: string;
792
1093
  chainOpts?: ChainOpts;
793
1094
  }
794
1095
  interface RollbackMemoryOptions {
795
1096
  orgName?: string;
1097
+ /** Atbash service endpoint — where memory reaches the chain, and the org→chain lookup. */
796
1098
  endpoint?: string;
1099
+ /** Self-hosted judge response-signing pubkey; required with a non-allowlisted `endpoint`. */
1100
+ verifyPubKey?: string;
797
1101
  chainOpts?: ChainOpts;
798
1102
  }
799
1103
  /**
@@ -815,6 +1119,7 @@ declare function commitMemoryVersion(plaintext: string, auth: AgentAuth, opts?:
815
1119
  */
816
1120
  interface AgentMemoryEntry {
817
1121
  id: number;
1122
+ filePath: string;
818
1123
  content: string;
819
1124
  decryptError?: string;
820
1125
  score: number;
@@ -826,6 +1131,7 @@ interface AgentMemoryEntry {
826
1131
  interface MemoryRollbackEvent {
827
1132
  fromId: number;
828
1133
  toId: number;
1134
+ filePath: string;
829
1135
  reason: string;
830
1136
  signer: string;
831
1137
  createdAt: number;
@@ -836,36 +1142,46 @@ interface MemoryRollbackEvent {
836
1142
  * response is a single integer, so this is safe to call on every
837
1143
  * memory-read hot path.
838
1144
  */
839
- declare function getActiveMemoryId(auth: AgentAuth, chainOpts?: ChainOpts): Promise<number | null>;
1145
+ declare function getActiveMemoryId(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<number | null>;
840
1146
  /**
841
1147
  * Recent active memory entries — subset of active versions filtered
842
1148
  * by the chain's `MEMORY_RECENT_WINDOW_MS`. For a time-unbounded view
843
- * of every currently active version, use `getAllAgentMemory`.
1149
+ * of every currently active version, use `getActiveAgentMemory`.
844
1150
  */
845
- declare function getActiveMemory(auth: AgentAuth, chainOpts?: ChainOpts): Promise<AgentMemoryEntry[]>;
1151
+ declare function getRecentAgentMemory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<AgentMemoryEntry[]>;
846
1152
  /**
847
1153
  * All currently-active memory entries with no time cutoff. Use this
848
1154
  * when you need every active version regardless of age.
849
1155
  */
850
- declare function getAllAgentMemory(auth: AgentAuth, chainOpts?: ChainOpts): Promise<AgentMemoryEntry[]>;
1156
+ declare function getActiveAgentMemory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<AgentMemoryEntry[]>;
1157
+ interface MemoryHistoryPage {
1158
+ /** Most versions to return, at most 200. Omit for the full history. */
1159
+ maxCount?: number;
1160
+ /** Return only versions with a lower id: the last id of the previous page. */
1161
+ beforeId?: number;
1162
+ }
851
1163
  /**
852
1164
  * Full version history — active + inactive, most recent first. Used
853
- * by rollback UX to choose a target version.
1165
+ * by rollback UX to choose a target version. Pass `page` to read it in
1166
+ * pages; this needs a Dashboard and chain that support paging.
854
1167
  */
855
- declare function getMemoryHistory(auth: AgentAuth, chainOpts?: ChainOpts): Promise<AgentMemoryEntry[]>;
1168
+ declare function getMemoryHistory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null, page?: MemoryHistoryPage): Promise<AgentMemoryEntry[]>;
856
1169
  /**
857
1170
  * Fetch a single memory entry by version id, including its current
858
- * `is_active` state.
1171
+ * `is_active` state. Version ids are agent-unique on chain (not
1172
+ * per-file), so `id` alone resolves the target row.
859
1173
  */
860
1174
  declare function getMemoryById(id: number, auth: AgentAuth, chainOpts?: ChainOpts): Promise<AgentMemoryEntry>;
861
1175
  /**
862
1176
  * Audit trail of rollback events for this agent, most recent first.
1177
+ * Scope by file with `filePath`; omit for a cross-file view.
863
1178
  */
864
- declare function getRollbackHistory(auth: AgentAuth, chainOpts?: ChainOpts): Promise<MemoryRollbackEvent[]>;
1179
+ declare function getRollbackHistory(auth: AgentAuth, chainOpts?: ChainOpts, filePath?: string | null): Promise<MemoryRollbackEvent[]>;
865
1180
  /**
866
1181
  * Roll back to a previously-committed memory version. The target
867
- * `toId` must exist and be currently inactive. `reason` is required
868
- * and is recorded on-chain in `memory_rollback_log`.
1182
+ * `toId` must exist and be currently inactive. The chain resolves the
1183
+ * target row's `file_path` from `toId` — no file path is passed in.
1184
+ * `reason` is required and is recorded on-chain in `memory_rollback_log`.
869
1185
  */
870
1186
  declare function rollbackMemory(toId: number, reason: string, auth: AgentAuth, opts?: RollbackMemoryOptions): Promise<void>;
871
1187
 
@@ -917,6 +1233,13 @@ interface ClassifyMemoryWriteOptions {
917
1233
  *
918
1234
  * Empty-content writes return `null`: they can't carry a poisoning
919
1235
  * payload, and the regular audit path still sees them.
1236
+ *
1237
+ * THROWS on host input it cannot read without side effects: a proxy, an accessor where a data
1238
+ * property is expected, a value with no safe projection, or one past the size bounds. Refusing is
1239
+ * deliberate - a silent `null` would report "not a memory write" for a call nobody could classify,
1240
+ * and memory protection would be skipped exactly where the input was strange. Callers that sit in a
1241
+ * host hook MUST catch it and turn it into a decision; `guardMemoryWrite` and
1242
+ * `MemoryGuardManager.handleBeforeToolCall` already do.
920
1243
  */
921
1244
  declare function classifyMemoryWrite(event: unknown, ctx: unknown, opts?: ClassifyMemoryWriteOptions): MemoryEntry | null;
922
1245
 
@@ -942,6 +1265,8 @@ interface GuardMemoryWriteInput extends ClassifyMemoryWriteOptions {
942
1265
  verifyPubKey?: string;
943
1266
  /** Org name for chain resolution (public vs org-private chain). */
944
1267
  orgName?: string;
1268
+ /** Explicit chain target when the host already resolved and verified it. */
1269
+ chainOpts?: ChainOpts;
945
1270
  /** LLM confidence threshold for yellow escalation. Default 0.6. */
946
1271
  threshold?: number;
947
1272
  /** When false, red verdicts log but don't block. Default true. */
@@ -950,8 +1275,17 @@ interface GuardMemoryWriteInput extends ClassifyMemoryWriteOptions {
950
1275
  debug?: boolean;
951
1276
  /** Optional logger for debug probe + persist-failure warnings. */
952
1277
  logger?: GuardLogger;
953
- /** Absolute path of the agent's managed memory file. Required for chain commits — omitted skips them. */
954
- memoryFilePath?: string;
1278
+ /** Agent home directory names to accept beyond the built-in list. */
1279
+ agentHomeDirs?: string[];
1280
+ /**
1281
+ * Root the caller keeps memory under, when it is not an agent home.
1282
+ *
1283
+ * Only pass a value the host stated outright. A root defaulted from the
1284
+ * process working directory would make whichever directory the host happened
1285
+ * to start in look like a memory store — which is how a stray MEMORY.md once
1286
+ * ended up inside a plugin checkout.
1287
+ */
1288
+ memoryRoot?: string;
955
1289
  }
956
1290
  /** Plugin-facing decision. Shape mirrors what plugins return from `before_tool_call`. */
957
1291
  interface GuardMemoryDecision {
@@ -966,25 +1300,57 @@ interface GuardMemoryWriteResult {
966
1300
  decision?: GuardMemoryDecision;
967
1301
  /** Present when handled and scan ran. Absent when scan threw. */
968
1302
  scanResult?: MemoryScanResult;
969
- /** Present when handled. True when a chain-commit was dispatched (fire-and-forget). */
1303
+ /** Present when handled. True only after the chain transaction confirms. */
970
1304
  committed?: boolean;
971
1305
  }
972
1306
  /**
973
- * Guarded memory-write flow. Returns `{ handled: false }` when the tool
974
- * call isn't a memory write, otherwise returns a `decision` the plugin
975
- * should return from its hook.
1307
+ * Guard a memory write, and record what was decided.
976
1308
  *
977
- * Verdict handling:
978
- * - `red` → not persisted to chain; decision blocks (unless `enforce: false`)
979
- * - `yellow` → persisted with LLM score; decision allows
980
- * - `green` → persisted with LLM score; decision allows
1309
+ * Wraps the decision rather than threading telemetry through its eight exits,
1310
+ * so a later return added inside cannot quietly stop being counted.
981
1311
  *
982
- * Chain commit is fire-and-forget: network errors are logged via
983
- * `logger.warn` but never surfaced to the caller. This mirrors the
984
- * existing plugin behaviour — chain is for audit/recovery, not gating.
1312
+ * Only a call this guard actually handled is recorded. A tool call that was
1313
+ * never a memory write is not a memory operation, and counting it would put
1314
+ * every unrelated edit into the number a memory migration reads.
1315
+ *
1316
+ * @param input - The tool call and the agent to judge it for.
1317
+ * @returns What the guard decided; unchanged by the recording.
985
1318
  */
986
1319
  declare function guardMemoryWrite(input: GuardMemoryWriteInput): Promise<GuardMemoryWriteResult>;
987
1320
 
1321
+ /**
1322
+ * The chain slot of a memory file, computed the same way everywhere.
1323
+ *
1324
+ * The Rust core finds the slot by stripping the agent's home from the file's
1325
+ * path, as an exact string prefix. The file path arrives resolved
1326
+ * (`path.resolve`), but the home did not: on Windows `HOME=/home/agent`
1327
+ * resolves the file to `C:\home\agent\...` and never matches, and a host that
1328
+ * spells the drive or the user folder differently (`c:\users\me` against
1329
+ * `C:\Users\Me`, both the same folder) did not match either. A file with no
1330
+ * slot is scanned but never recorded on chain, silently, so the on-chain
1331
+ * version record and its rollback/read checks were skipped for it.
1332
+ *
1333
+ * Here the home (and the memory root) is resolved like the file, and on a
1334
+ * case-insensitive file system it is passed to the core spelled the way the
1335
+ * file path spells it. The slot itself, the part below the agent home, keeps
1336
+ * the file's own spelling, so slots recorded before this change still match.
1337
+ *
1338
+ * Known limit: a folder with NTFS per-directory case sensitivity enabled
1339
+ * (`fsutil file setCaseSensitiveInfo`, or one created from WSL) can hold both
1340
+ * `Me` and `me`; there ASCII case does separate folders, and a sibling spelled
1341
+ * in another case would share the home's slots. Creating one needs write
1342
+ * access to the home's parent. A follow-up can confirm the same folder on disk
1343
+ * when the spellings differ.
1344
+ */
1345
+
1346
+ /** Path semantics: `node:path` here, `path.win32` / `path.posix` in tests. */
1347
+ type PathImpl = typeof nodePath;
1348
+ /**
1349
+ * The chain slot for `target`, or null when it gets no chain record. `p`
1350
+ * selects path semantics (tests pass `path.win32` to check Windows anywhere).
1351
+ */
1352
+ declare function memorySlotOf(target: string, home: string, root?: string, extraHomes?: string[], p?: PathImpl): string | null;
1353
+
988
1354
  /**
989
1355
  * Aligns a local `MemoryPointer` with chain's active memory version
990
1356
  * via a TTL-cached pointer check. Inside the TTL window: no-op.
@@ -1011,6 +1377,8 @@ interface SyncMemoryOptions {
1011
1377
  /** Skip the TTL gate and force a chain check this call. Default false. */
1012
1378
  force?: boolean;
1013
1379
  chainOpts?: ChainOpts;
1380
+ /** Exact chain slot for the managed file; omission keeps the cross-file API behavior. */
1381
+ filePath?: string;
1014
1382
  }
1015
1383
  /**
1016
1384
  * `drifted: false` — pointer is still valid; caller can keep serving the local copy.
@@ -1044,10 +1412,19 @@ declare class PointerStore {
1044
1412
  private cache;
1045
1413
  private loading;
1046
1414
  constructor(filePath: string);
1047
- /** Resolves the pointer for `agentPubkeyHex`, or a zero-pointer that will force a sync on first use. */
1048
- get(agentPubkeyHex: string): Promise<MemoryPointer>;
1049
- /** Persists an updated pointer. Failures are swallowed to a logger callback (if provided) so sync never blocks the caller. */
1050
- set(agentPubkeyHex: string, pointer: MemoryPointer, onError?: (err: Error) => void): Promise<void>;
1415
+ /**
1416
+ * The pointer for one agent's file, or a zero-pointer forcing a sync.
1417
+ *
1418
+ * @param filePath - The file's chain slot, i.e. its basename.
1419
+ */
1420
+ get(agentPubkeyHex: string, filePath: string): Promise<MemoryPointer>;
1421
+ /**
1422
+ * Persist one file's pointer, leaving the agent's other files alone.
1423
+ *
1424
+ * Write failures are reported to `onError` rather than thrown: a cache that
1425
+ * cannot be written must not take down the read it was speeding up.
1426
+ */
1427
+ set(agentPubkeyHex: string, filePath: string, pointer: MemoryPointer, onError?: (err: Error) => void): Promise<void>;
1051
1428
  private ensureLoaded;
1052
1429
  private loadOnce;
1053
1430
  private persist;
@@ -1059,6 +1436,8 @@ declare function defaultPointerPath(workspaceDir?: string): string;
1059
1436
  interface WrappedLogger {
1060
1437
  info(message: string, meta?: Record<string, unknown>): void;
1061
1438
  warn(message: string, meta?: Record<string, unknown>): void;
1439
+ /** Wait for queued writes and surface any lost log entry. */
1440
+ flush(): Promise<void>;
1062
1441
  }
1063
1442
  interface UpstreamLogger {
1064
1443
  info: (...args: unknown[]) => void;
@@ -1087,6 +1466,13 @@ interface ClassifyMemoryReadOptions {
1087
1466
  * Caller-supplied `patterns` are MERGED with the defaults (matches
1088
1467
  * Node's original behavior — extending in one plugin doesn't disable
1089
1468
  * standard coverage).
1469
+ *
1470
+ * THROWS on host input it cannot read without side effects: a proxy, an accessor where a data
1471
+ * property is expected, a value with no safe projection, or one past the size bounds. Refusing is
1472
+ * deliberate - a silent `null` would report "not a memory write" for a call nobody could classify,
1473
+ * and memory protection would be skipped exactly where the input was strange. Callers that sit in a
1474
+ * host hook MUST catch it and turn it into a decision; `guardMemoryWrite` and
1475
+ * `MemoryGuardManager.handleBeforeToolCall` already do.
1090
1476
  */
1091
1477
  declare function classifyMemoryRead(event: unknown, ctx: unknown, opts?: ClassifyMemoryReadOptions): boolean;
1092
1478
 
@@ -1111,10 +1497,10 @@ interface HookDecision {
1111
1497
  * failure, a rolled-back version), and a host must never re-judge its way past
1112
1498
  * one.
1113
1499
  *
1114
- * Absent or false means the guard reached no decision — it was inside its cache
1115
- * window, chain was unreachable, the scan never ran, the file it can vouch for
1116
- * is not the file being read, or it is in observe mode. Those calls are
1117
- * unaudited: fall through to your own judge exactly as for a `null` return.
1500
+ * Absent or false means the guard reached no decision — for example, the scan
1501
+ * never ran or this manager does not own the requested file. In enforce mode,
1502
+ * failed chain verification of the managed file is a block, never an unaudited
1503
+ * allow. In observe mode unverified calls remain unaudited.
1118
1504
  *
1119
1505
  * So the host rule is:
1120
1506
  * `if (d.block) deny; else if (d.audited) allow; else judge it yourself;`
@@ -1133,6 +1519,17 @@ interface MemoryGuardManagerOptions {
1133
1519
  workspaceDir: string;
1134
1520
  /** Absolute MEMORY.md path. Default `<workspaceDir>/MEMORY.md`. */
1135
1521
  memoryFilePath?: string;
1522
+ /**
1523
+ * Root this host keeps memory under, when it is not an agent home.
1524
+ *
1525
+ * Only set this from a value the host stated outright — never from
1526
+ * `process.cwd()`. A guessed root makes whichever directory the host started
1527
+ * in look like a memory store, which is how a stray MEMORY.md once ended up
1528
+ * inside a plugin checkout.
1529
+ */
1530
+ memoryRoot?: string;
1531
+ /** Agent home directory names to accept beyond the built-in list. */
1532
+ agentHomeDirs?: string[];
1136
1533
  /** Persistent pointer file. Default `<workspaceDir>/.atbash/memory-pointer.json`. */
1137
1534
  pointerFilePath?: string;
1138
1535
  /** File-backed log. Default `<workspaceDir>/.atbash/plugin.log`. */
@@ -1179,11 +1576,45 @@ declare class MemoryGuardManager {
1179
1576
  private readonly pointerStore;
1180
1577
  private readonly logger;
1181
1578
  private readonly memoryFilePath;
1579
+ /** Base for resolving the relative paths a host sends. */
1580
+ private readonly workspaceDir;
1581
+ /** Set by `verifyOneFile` when the file it checked was actually on chain. */
1582
+ private lastVerified;
1182
1583
  private readonly ttlMs;
1183
1584
  private readonly rollbackMinScore;
1184
1585
  private readonly enforce;
1185
1586
  private readonly agentPubkeyHex;
1587
+ /**
1588
+ * Memoized org→chain resolution. Reads have to hit the SAME chain
1589
+ * writes did, so an org-scoped guard must resolve `orgName` to
1590
+ * network exactly like `commitMemoryVersion` does. Without this
1591
+ * cache the read path would either (a) hit the SDK-default chain
1592
+ * every time — silently returning "no active memory on chain" when
1593
+ * writes landed on the org's actual chain, or (b) hammer
1594
+ * `/api/org-network` on every read. `undefined` means "not yet
1595
+ * resolved"; a resolved `null` means "no org / use raw chainOpts".
1596
+ */
1597
+ private _resolvedChainOpts;
1598
+ private _resolveChainInflight?;
1186
1599
  constructor(opts: MemoryGuardManagerOptions);
1600
+ /**
1601
+ * Carry this manager's host into `chainOpts`.
1602
+ *
1603
+ * Memory reads reach the chain through that host, and they are handed only
1604
+ * `chainOpts` — without this a self-hosted deployment would read from the
1605
+ * compiled-in default host while writing to its own.
1606
+ */
1607
+ private withEndpoint;
1608
+ /**
1609
+ * Resolve `orgName` → chain once, cache forever. `commitMemoryVersion`
1610
+ * already does this for writes; without the same call on the read
1611
+ * path, a client on the SDK's baked default chain reads from the wrong
1612
+ * chain and reports "no active memory" for an agent whose writes did
1613
+ * land — on the org's actual chain. An explicit `chainOpts.blockchainRid`
1614
+ * still wins (caller vouched for it); everything else honors the
1615
+ * dashboard's `org_networks` map.
1616
+ */
1617
+ private resolveChainOpts;
1187
1618
  /**
1188
1619
  * One-shot chain probe at plugin registration. Refreshes MEMORY.md
1189
1620
  * from chain when drifted and score passes threshold. Fire-and-forget
@@ -1191,15 +1622,27 @@ declare class MemoryGuardManager {
1191
1622
  */
1192
1623
  runBootProbe(): Promise<void>;
1193
1624
  /**
1194
- * Returns a `HookDecision` when the guard reached a decision about this event.
1195
- * Returns `null` when it did not — either the event isn't memory-related, or it
1196
- * is but the guard could not check it. In both cases the host falls through to
1197
- * its own audit.
1625
+ * Bring one chain file into step with its local counterpart.
1626
+ *
1627
+ * Only a file that already exists locally is touched. Restoring one that is
1628
+ * absent would mean choosing where to create it, and the root is a guess the
1629
+ * moment more than one could apply — the same guess that once wrote a
1630
+ * MEMORY.md into a plugin checkout.
1631
+ *
1632
+ * @returns What happened, for the single summary line the caller logs.
1633
+ */
1634
+ private syncOneAtBoot;
1635
+ /**
1636
+ * Where a chain slot's file lives locally, or null when we do not have it.
1198
1637
  *
1199
- * A returned decision carries `audited` (see `HookDecision`). Only
1200
- * `{ allow: true, audited: true }` means "checked and cleared"; anything else
1201
- * that allows is a call the host still needs to judge.
1638
+ * A stated root is tried first, then each agent home that actually exists in
1639
+ * the user's home directory — the same rule that produced the slot, run
1640
+ * backwards. Only an existing file is returned: creating one would mean
1641
+ * picking a root, and picking is guessing.
1202
1642
  */
1643
+ private locate;
1644
+ /** Roots a chain slot may be joined to, most specific first. */
1645
+ private memoryRoots;
1203
1646
  handleBeforeToolCall(event: unknown, ctx: unknown): Promise<HookDecision | null>;
1204
1647
  private mapGuardResult;
1205
1648
  /**
@@ -1217,9 +1660,70 @@ declare class MemoryGuardManager {
1217
1660
  * also covers events carrying two different path keys, where the classifier
1218
1661
  * and the host could otherwise disagree about which one is authoritative.
1219
1662
  */
1220
- private vouchesForTarget;
1663
+ /**
1664
+ * The memory files this tool call reads, as absolute paths.
1665
+ *
1666
+ * Relative paths resolve against the workspace, not `process.cwd()`. The host
1667
+ * sends paths relative to the agent's workspace; resolving them against the
1668
+ * process's own directory made the guard protect whichever directory the host
1669
+ * happened to start in, which on one machine was a plugin checkout.
1670
+ *
1671
+ * @returns Every named target, de-duplicated; empty when the call names none.
1672
+ */
1673
+ private readTargets;
1674
+ /**
1675
+ * Say what the read classifier saw and what it concluded.
1676
+ *
1677
+ * The write path has `emitDebugProbe`; this is its counterpart, so a tool
1678
+ * call can be followed across the fork instead of only after it. `false` here
1679
+ * is the normal answer for a write — it means the call goes to the write
1680
+ * classifier next, not that it was ignored.
1681
+ *
1682
+ * @param isMemoryRead - What `classifyMemoryRead` returned.
1683
+ */
1684
+ private emitReadProbe;
1685
+ /**
1686
+ * The chain slot for a path, or null when the file gets no chain record.
1687
+ *
1688
+ * One helper so the boot probe and every read agree: taking a basename in one
1689
+ * place and a relative path in the other would verify a file against a record
1690
+ * that is not its own.
1691
+ */
1692
+ private slotFor;
1693
+ /**
1694
+ * Verify every memory file this call reads, each against its own chain slot.
1695
+ *
1696
+ * A call naming several files is only cleared when all of them are, so one
1697
+ * unverifiable file cannot ride along with a verified one.
1698
+ *
1699
+ * @returns A block for the first file that fails, an audited allow when all
1700
+ * were verified, or null when none of them is on chain.
1701
+ */
1221
1702
  private handleMemoryRead;
1703
+ /**
1704
+ * Check one memory file against the chain slot of the same name.
1705
+ *
1706
+ * A file with no chain version is passed through rather than blocked: there
1707
+ * is no claim to protect, so refusing would only stop an agent reading memory
1708
+ * it has never committed. A file that IS on chain must match, or the read is
1709
+ * refused — that is the whole guarantee.
1710
+ *
1711
+ * @returns A decision when the read must not proceed, else null.
1712
+ */
1713
+ private verifyOneFile;
1714
+ /** Complete queued audit-log writes before a host shuts down or removes its workspace. */
1715
+ flushLogs(): Promise<void>;
1716
+ /**
1717
+ * A read this guard could not verify.
1718
+ *
1719
+ * Enforcing, that is a block. Observing, the host audits it as usual — but
1720
+ * the reason is said out loud either way, or an unverified read looks
1721
+ * identical to a clean one in the log.
1722
+ */
1723
+ private unverifiedRead;
1222
1724
  private writeMemoryAtomic;
1725
+ /** Replace `target` in one step, so a reader never sees a half-written file. */
1726
+ private writeFileAtomic;
1223
1727
  }
1224
1728
  declare function createMemoryGuardManager(opts: MemoryGuardManagerOptions): MemoryGuardManager;
1225
1729
 
@@ -1253,6 +1757,20 @@ interface TelemetryConfig {
1253
1757
  * `{ Authorization: "Bearer <hex>" }`). Required to actually export.
1254
1758
  */
1255
1759
  getAuthHeaders?: () => Record<string, string>;
1760
+ /**
1761
+ * Hashes the agent key before it is emitted on a wide event.
1762
+ *
1763
+ * Defaults to an unsalted SHA-256, which is de-identification rather than
1764
+ * anonymisation: agent pubkeys are public on chain and can be enumerated, so
1765
+ * the mapping is recoverable by anyone holding that list. Supply a salted
1766
+ * hasher to make it uncorrelatable, at the cost of joining across
1767
+ * deployments.
1768
+ *
1769
+ * Injected rather than imported so the telemetry modules need nothing from
1770
+ * the native addon. Left unset, the agent field is dropped from events
1771
+ * rather than falling back to the raw key.
1772
+ */
1773
+ hashAgentKey?: (pubkeyHex: string) => string;
1256
1774
  }
1257
1775
  declare function setupTelemetry(config: TelemetryConfig): void;
1258
1776
  /**
@@ -1275,6 +1793,111 @@ declare function flushTelemetry(): Promise<void>;
1275
1793
  */
1276
1794
  declare function shutdownTelemetry(): Promise<void>;
1277
1795
 
1796
+ /**
1797
+ * Wide events: one row per SDK operation, alongside the counters.
1798
+ *
1799
+ * The counters in `telemetry.ts` are aggregated inside this process before
1800
+ * export, so Honeycomb never receives a per-operation row. That is fine for
1801
+ * "is it up" and useless for everything else: a question nobody attached an
1802
+ * attribute for in advance cannot be answered later, because the data to
1803
+ * answer it never existed. "Which org sees the most failures", "what preceded
1804
+ * a fail-closed", "is the new release slower" are all that shape.
1805
+ *
1806
+ * So each operation also emits one event carrying its whole context. The
1807
+ * counters stay — they are the right tool for health boards and alerts, and
1808
+ * they are not sampled.
1809
+ *
1810
+ * TWO RULES SHAPE THIS MODULE.
1811
+ *
1812
+ * The payload is METADATA ONLY, and the allowlist below is the whole of it:
1813
+ * function name, outcome, latency, error class, error status and path, SDK
1814
+ * version, chain, and a hash of the agent key. Action text, tool arguments, policy text, organisation
1815
+ * names and keys never appear — not as a field that is emptied, but as a field
1816
+ * that does not exist, so there is nothing to forget to strip.
1817
+ *
1818
+ * The agent key is hashed, never sent raw, and the hasher is INJECTED rather
1819
+ * than imported: this module needs nothing from the native addon. With no
1820
+ * hasher supplied the field is DROPPED rather than falling back to the raw
1821
+ * key, so the guarantee does not depend on wiring somebody could forget.
1822
+ *
1823
+ * NOTE on what hashing does and does not buy. Agent pubkeys are public on
1824
+ * chain and can be enumerated, so an UNSALTED hash is reversible by anyone who
1825
+ * can read the chain: it is de-identification, not anonymisation. A salted
1826
+ * hasher makes it uncorrelatable and costs cross-deployment joins. Which one
1827
+ * is wanted is the caller's decision; this module only guarantees the raw key
1828
+ * never leaves.
1829
+ *
1830
+ * And the richer payload has its own switch. Counters carry a function name
1831
+ * and a source. These events carry agent and org identity off a customer's
1832
+ * machine, which is a different proposition for a security product, so
1833
+ * `ATBASH_TELEMETRY_EVENTS_DISABLED=1` turns them off while leaving the health
1834
+ * counters on. Both remain subject to the existing global opt-out.
1835
+ */
1836
+
1837
+ /** Attributes an event may carry. Anything not listed here cannot be emitted. */
1838
+ interface OperationEvent {
1839
+ /** SDK function, matching the counter's `function.name` so the two join. */
1840
+ functionName: string;
1841
+ durationMs: number;
1842
+ status: "success" | "error";
1843
+ /**
1844
+ * Hex pubkey of the agent the call was made as. Hashed before it is emitted,
1845
+ * and omitted entirely when no hasher is configured.
1846
+ */
1847
+ agentPubkey?: string;
1848
+ /**
1849
+ * Blockchain RID of the chain this call concerned.
1850
+ *
1851
+ * The RID rather than a network label: it is public infrastructure, it is
1852
+ * unambiguous, and it identifies the deployment without naming a customer.
1853
+ */
1854
+ chain?: string;
1855
+ /** Constructor name of the thrown error, e.g. `AtbashAPIError`. */
1856
+ errorClass?: string;
1857
+ /** HTTP status, or 0 when the request never completed. */
1858
+ errorStatus?: number;
1859
+ /**
1860
+ * Request path the status came from, e.g. `/api/v1/judge`.
1861
+ *
1862
+ * The class alone does not say what failed: several routes answer one call
1863
+ * and more than one can 404, so `AtbashAPIError` cannot distinguish "this
1864
+ * agent is not registered" from "the chain-routing lookup did not resolve".
1865
+ * The path settles it.
1866
+ *
1867
+ * Carries no query string — `requestPath` strips it at the point the error
1868
+ * is built, because a query string carries org names, tool call ids and the
1869
+ * agent pubkey. Structured and already sanitised, so this stays inside the
1870
+ * metadata rule where an error message would not.
1871
+ */
1872
+ errorPath?: string;
1873
+ /**
1874
+ * Short categorical result, when "success" is too coarse — e.g. a memory
1875
+ * write that scanned clean but never reached the chain.
1876
+ *
1877
+ * Categorical by contract: a value a caller chooses from a fixed vocabulary,
1878
+ * never free text and never anything derived from user content.
1879
+ */
1880
+ outcome?: string;
1881
+ }
1882
+ /** Hashes an agent key for emission. Supplied by `setupTelemetry`. */
1883
+ type AgentKeyHasher = (pubkeyHex: string) => string;
1884
+ declare function configureEvents(next: Logger | null, opts?: {
1885
+ version?: string;
1886
+ source?: string;
1887
+ hashAgentKey?: AgentKeyHasher;
1888
+ }): void;
1889
+ /** True when an event would actually be emitted. Exported for tests and callers that build context lazily. */
1890
+ declare function eventsAreOn(): boolean;
1891
+ /**
1892
+ * Emit one wide event for a completed operation.
1893
+ *
1894
+ * Never throws and never blocks: telemetry failing must not change what the
1895
+ * caller observes. Does nothing when events are off.
1896
+ *
1897
+ * @param e - The completed operation.
1898
+ */
1899
+ declare function recordEvent(e: OperationEvent): void;
1900
+
1278
1901
  /**
1279
1902
  * Rust core does the work; `browser/encrypted-toolcall.ts` mirrors it for the
1280
1903
  * browser and must produce the same columns.
@@ -1372,4 +1995,4 @@ declare function containsSecret(text: string): boolean;
1372
1995
  declare function createMemorySnapshot(entries: MemoryEntry[], takenAt: number): MemorySnapshot;
1373
1996
  declare function diffMemorySnapshots(before: MemorySnapshot, after: MemorySnapshot): MemoryDiffResult;
1374
1997
 
1375
- 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 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, 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, chooseKeyPath, claimHashHex, classifyMemoryRead, classifyMemoryWrite, columnAad, commitMemoryVersion, containsEvasionCharacters, containsSecret, createFileLogger, createMemoryGuardManager, createMemorySnapshot, decryptForOrg, decryptMemoryContent, defaultPluginLogPath, defaultPointerPath, deriveMemoryKey, derivePublicKey, diffMemorySnapshots, encryptForOrg, encryptMemoryContent, encryptedLength, flushTelemetry, generateKeypair, getActiveMemory, getActiveMemoryId, getAllAgentMemory, getConfigDir, getConfigPath, getMemoryById, getMemoryHistory, 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 };
1998
+ 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 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, 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, setupTelemetry, shutdownTelemetry, signEncryptedToolCall, signJudgeAction, signLogToolCall, syncLocalMemory, validateJudgeEndpoint, verifyJudgeResponseSignature, verifySignature };