@atbash/sdk 0.3.6 → 0.3.8

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/README.md CHANGED
@@ -21,8 +21,8 @@ import { loadAgent, judgeAction } from "@atbash/sdk";
21
21
  const agent = loadAgent(process.env.ATBASH_AGENT_PRIVKEY!);
22
22
 
23
23
  // 2. Submit an action for judgment, before executing it.
24
- // The SDK signs and broadcasts log_tool_call to the Chromia chain
25
- // (private key stays local), then requests a verdict from the judge API.
24
+ // The SDK signs the transaction locally and sends it to the judge API.
25
+ // Private key stays on your machine never sent over HTTP.
26
26
  const result = await judgeAction(
27
27
  "Transfer $50,000 to external wallet 0xabc",
28
28
  "Outbound AML check — new recipient, over threshold",
@@ -50,10 +50,9 @@ Before this works, the agent must be onboarded at [atbash.ai](https://atbash.ai/
50
50
 
51
51
  `judgeAction()` performs a two-step flow:
52
52
 
53
- 1. **Sign on-chain** — signs and broadcasts a `log_tool_call` transaction to the Chromia blockchain using the agent's private key. The key never leaves your machine.
54
- 2. **Request verdict** — sends the resulting `tool_call_id` and `agent_pubkey` to the Atbash judge API. No private key is transmitted over HTTP.
53
+ 1. **Sign locally** — signs the transaction using the agent's private key. The key never leaves your machine.
54
+ 2. **Request verdict** — sends the signed transaction, `tool_call_id`, and `agent_pubkey` to the Atbash judge API. The server broadcasts it to the Chromia blockchain and returns a verdict.
55
55
 
56
- If you need finer control, you can call `logToolCall()` and the judge API separately.
57
56
 
58
57
  ### Don't have an agent yet?
59
58
 
@@ -75,12 +74,9 @@ const agent = loadAgent(privKey);
75
74
 
76
75
  ### Secret storage
77
76
 
78
- The private key is used to sign on-chain transactions locally. Treat it like any other long-lived credential:
79
-
80
- - Load it from an environment variable (`ATBASH_AGENT_PRIVKEY`) or a secret manager (AWS Secrets Manager, HashiCorp Vault, 1Password, etc.) — never hardcode it.
81
- - Never commit `.env` files containing the key. Add them to `.gitignore`.
82
- - If a key leaks, revoke the agent and create a new one in the [Atbash dashboard](https://atbash.ai/risk-engine/agents).
83
- - The SDK is **server-side only** — the key must never ship to a browser bundle.
77
+ - Load the private key from an environment variable (`ATBASH_AGENT_KEY`) or a secret manager never hardcode it.
78
+ - Never commit `.env` files containing the key.
79
+ - If a key leaks, stop using it and create a new agent in the [dashboard](https://atbash.ai/risk-engine/agents).
84
80
 
85
81
  ## Verdicts
86
82
 
@@ -107,7 +103,7 @@ judgeAction(
107
103
  ): Promise<JudgeResult>
108
104
  ```
109
105
 
110
- Submit an action for judgment before execution. Signs `log_tool_call` on-chain, then requests a verdict. Returns the verdict, reason, confidence, provider, latency, and tool call ID.
106
+ Submit an action for judgment before execution. Signs the transaction locally and sends it to the judge API for a verdict.
111
107
 
112
108
  ```ts
113
109
  interface AgentAuth {
@@ -154,12 +150,12 @@ logToolCall(
154
150
  ): Promise<LogToolCallResult>
155
151
  ```
156
152
 
157
- Sign `log_tool_call` on the Chromia chain. Returns `{ success, toolCallId, error? }`. Use this if you need to separate the on-chain logging step from the verdict request.
153
+ Sign the transaction locally. Returns `{ success, toolCallId, signedHex?, error? }`. Use this if you need to separate the signing step from the verdict request.
158
154
 
159
155
  ### Poll judgment status
160
156
 
161
157
  ```ts
162
- getJudgmentStatus(judgmentId: string, opts?: ClientOpts): Promise<JudgmentStatus>
158
+ getJudgmentStatus(judgmentId: string, agentPubkey: string, opts?: ClientOpts): Promise<JudgmentStatus>
163
159
  ```
164
160
 
165
161
  Check whether a held action has been approved or rejected by an operator.
@@ -194,15 +190,15 @@ Functions that sign transactions and write to the Chromia blockchain.
194
190
 
195
191
  | Function | Use case |
196
192
  |----------|----------|
197
- | `judgeAction(action, context, auth, opts?)` | Sign `log_tool_call` on-chain + request a verdict from the judge API |
198
- | `logToolCall(action, context, auth, ...)` | Sign and broadcast `log_tool_call` to chain without requesting a verdict |
193
+ | `judgeAction(action, context, auth, opts?)` | Sign locally + request a verdict from the judge API |
194
+ | `logToolCall(action, context, auth, ...)` | Sign the transaction locally without requesting a verdict |
199
195
 
200
196
  ### Queries
201
197
 
202
198
  | Function | Use case |
203
199
  |----------|----------|
204
200
  | `checkAgentExists(pubkey, opts?)` | Check if an agent is onboarded before signing |
205
- | `getJudgmentStatus(judgmentId, opts?)` | Poll whether a held action has been approved or rejected |
201
+ | `getJudgmentStatus(judgmentId, agentPubkey, opts?)` | Poll whether a held action has been approved or rejected |
206
202
  | `getToolCalls(maxCount)` | List recent tool calls across all agents |
207
203
  | `getOrgToolCalls(orgName, maxCount)` | List tool calls for a specific org |
208
204
  | `getAgentToolCalls(pubkey, maxCount)` | List tool calls for a specific agent |
@@ -217,24 +213,78 @@ Functions that sign transactions and write to the Chromia blockchain.
217
213
 
218
214
  ## Configuration
219
215
 
220
- The SDK reads no environment variables and has no global state. Pass configuration explicitly:
216
+ You can pass configuration inline or use the built-in config module that reads from `~/.config/atbash/config.json`.
217
+
218
+ ### Inline
221
219
 
222
220
  ```ts
223
- // Custom endpoint
224
221
  const result = await judgeAction(action, context, auth, {
225
222
  endpoint: "https://your-instance.example.com",
223
+ provider: "openai",
224
+ model: "gpt-4o",
226
225
  });
226
+ ```
227
227
 
228
- // Custom provider (API keys are saved in the dashboard, not passed here)
229
- const result = await judgeAction(action, context, auth, {
228
+ ### Persistent config
229
+
230
+ Save configuration once — the SDK resolves values with priority: **flag > env var > config file**.
231
+
232
+ ```ts
233
+ import { saveUserConfig, resolve, loadAgent, judgeAction } from "@atbash/sdk";
234
+
235
+ // Save once
236
+ saveUserConfig({
237
+ agentKey: "9cd07a...",
238
+ orgName: "my_org",
230
239
  provider: "openai",
231
- model: "gpt-4o",
232
240
  });
233
241
 
242
+ // Then use resolve() anywhere
243
+ const agent = loadAgent(resolve("agentKey"));
244
+ const result = await judgeAction("Transfer $500", "finance", agent, {
245
+ provider: resolve("provider"), // omit to use the on-chain ATBASH judge
246
+ });
234
247
  ```
235
248
 
249
+ Config file location: `~/.config/atbash/config.json`
250
+
251
+ | Function | Purpose |
252
+ |----------|---------|
253
+ | `saveUserConfig(config)` | Write config to disk |
254
+ | `loadUserConfig()` | Read config from disk |
255
+ | `resolve(key, flagValue?)` | Resolve a value: flag > env > file > `""` |
256
+ | `getConfigPath()` | Returns the config file path |
257
+
258
+ | Config key | Env var |
259
+ |------------|--------|
260
+ | `agentKey` | `ATBASH_AGENT_KEY` |
261
+ | `orgName` | `ATBASH_ORG_NAME` |
262
+ | `judgeEndpoint` | `ATBASH_ENDPOINT` |
263
+ | `blockchainRid` | `ATBASH_BLOCKCHAIN_RID` |
264
+ | `provider` | `ATBASH_PROVIDER` |
265
+ | `providerModel` | `ATBASH_PROVIDER_MODEL` |
266
+
236
267
  > **Advanced:** The SDK connects to the default Atbash Chromia chain. To use a different chain, pass `chainOpts` with custom `nodeUrls` and `blockchainRid` in `JudgeOptions`.
237
268
 
269
+ ## Secret redaction
270
+
271
+ Before each `auditToolCall` signs anything, the SDK scans `args` and `context` for secret-shaped values (API keys, tokens, JWTs, PEM blocks, etc.) and replaces matches with `[REDACTED:<kind>]`. Redaction happens **before signing**, so secrets never reach the signed bytes, the request body, the on-chain log, or the prompt sent to the AI provider.
272
+
273
+ When the redactor fires, you'll see a warning via the configured `logger`:
274
+
275
+ ```
276
+ [atbash] redacted secrets before judge call { tool: "exec", count: 2, kinds: ["anthropic", "generic_token"] }
277
+ ```
278
+
279
+ Common `kinds`:
280
+
281
+ - `anthropic`, `openai`, `github`, `google`, `aws_access_key`, `stripe`, `slack`, `jwt`, `private_key_pem` — high-confidence vendor patterns; if you see these, a real secret was almost certainly in your input
282
+ - `context_secret` — a value next to a label like `api_key=`, `token:`, `password=`, etc.
283
+ - `generic_token` — long random-looking strings (32+ alphanumeric chars). Catches unknown-vendor secrets, but can also match UUIDs, content hashes, and other opaque identifiers. The judge sees `[REDACTED:generic_token]` instead of the original; for verdict purposes the shape of the action matters more than the exact ID, so this is generally safe — but worth knowing if you see it unexpectedly.
284
+ - `base64` — long base64-encoded values; can match legitimate image/file data
285
+
286
+ Redaction is silent at the consumer level — the SDK's caller still has the original arguments. Only what's sent to the judge (and persisted on chain via the verdict log) is scrubbed.
287
+
238
288
  ## Integration patterns
239
289
 
240
290
  ### Pre-execution gate
@@ -253,7 +303,7 @@ async function safeExecute(action: string, context: string, execute: () => Promi
253
303
  ```ts
254
304
  async function waitForApproval(toolCallId: string): Promise<string> {
255
305
  while (true) {
256
- const status = await getJudgmentStatus(toolCallId);
306
+ const status = await getJudgmentStatus(toolCallId, auth.pubkey);
257
307
  if (status.status === "answered") return status.verdict;
258
308
  if (status.status === "error") throw new Error(status.reason);
259
309
  await new Promise((r) => setTimeout(r, 5000));
package/dist/index.cjs CHANGED
@@ -34,11 +34,14 @@ __export(index_exports, {
34
34
  DEFAULT_CHROMIA_NODE_URLS: () => DEFAULT_CHROMIA_NODE_URLS,
35
35
  DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
36
36
  checkAgentExists: () => checkAgentExists,
37
+ createAtbashClient: () => createAtbashClient,
37
38
  derivePublicKey: () => derivePublicKey,
38
39
  generateKeyPair: () => generateKeyPair,
39
40
  getAgentDetail: () => getAgentDetail,
40
41
  getAgentPolicy: () => getAgentPolicy,
41
42
  getAgentToolCalls: () => getAgentToolCalls,
43
+ getConfigDir: () => getConfigDir,
44
+ getConfigPath: () => getConfigPath,
42
45
  getHeldActionReviews: () => getHeldActionReviews,
43
46
  getJudgmentStatus: () => getJudgmentStatus,
44
47
  getOrgTierInfo: () => getOrgTierInfo,
@@ -51,15 +54,50 @@ __export(index_exports, {
51
54
  isValidPrivateKey: () => isValidPrivateKey,
52
55
  judgeAction: () => judgeAction,
53
56
  loadAgent: () => loadAgent,
57
+ loadAgentFromFile: () => loadAgentFromFile,
58
+ loadUserConfig: () => loadUserConfig,
54
59
  logToolCall: () => logToolCall,
55
- toPubkeyHex: () => toPubkeyHex
60
+ resolve: () => resolve,
61
+ resolveKeyPath: () => resolveKeyPath,
62
+ saveUserConfig: () => saveUserConfig,
63
+ toPubkeyHex: () => toPubkeyHex,
64
+ validateJudgeEndpoint: () => validateJudgeEndpoint,
65
+ verifyJudgeResponseSignature: () => verifyJudgeResponseSignature
56
66
  });
57
67
  module.exports = __toCommonJS(index_exports);
58
68
 
59
69
  // src/client.ts
60
70
  var import_crypto = require("crypto");
71
+ var import_postchain_client2 = __toESM(require("postchain-client"), 1);
72
+
73
+ // src/signature.ts
61
74
  var import_postchain_client = __toESM(require("postchain-client"), 1);
62
- var { createClient, encryption, newSignatureProvider } = import_postchain_client.default;
75
+ var { encryption } = import_postchain_client.default;
76
+ function verifyJudgeResponseSignature(bodyBytes, signatureHex, pubKeyHex) {
77
+ if (!signatureHex) {
78
+ return { ok: false, reason: "missing X-Atbash-Signature header" };
79
+ }
80
+ const sigClean = signatureHex.trim().toLowerCase().replace(/^0x/, "");
81
+ if (!/^[0-9a-f]+$/.test(sigClean) || sigClean.length < 64 || sigClean.length > 256) {
82
+ return { ok: false, reason: "malformed signature header" };
83
+ }
84
+ let isValid = false;
85
+ try {
86
+ const digest = encryption.sha256(Buffer.from(bodyBytes));
87
+ const pubKeyBytes = Buffer.from(pubKeyHex.replace(/^0x/, ""), "hex");
88
+ const sigBytes = Buffer.from(sigClean, "hex");
89
+ isValid = encryption.checkDigestSignature(digest, pubKeyBytes, sigBytes);
90
+ } catch (err) {
91
+ const message = String(
92
+ err?.message ?? err ?? ""
93
+ );
94
+ return { ok: false, reason: `signature verification threw: ${message}` };
95
+ }
96
+ return isValid ? { ok: true } : { ok: false, reason: "signature does not verify against configured verifyPubKey" };
97
+ }
98
+
99
+ // src/client.ts
100
+ var { createClient, encryption: encryption2, newSignatureProvider } = import_postchain_client2.default;
63
101
  var DEFAULT_ENDPOINT = "https://atbash.ai";
64
102
  var DEFAULT_CHROMIA_NODE_URLS = [
65
103
  "https://node6.testnet.chromia.com:7740",
@@ -115,7 +153,7 @@ async function buildSignedTx(opName, args, auth, chainOpts) {
115
153
  const blockchainRid = chainOpts?.blockchainRid ?? DEFAULT_BLOCKCHAIN_RID;
116
154
  const client = await createClient({ nodeUrlPool: nodeUrls, blockchainRid });
117
155
  const privKeyBuf = Buffer.from(auth.privkey, "hex");
118
- const keyPair = encryption.makeKeyPair(privKeyBuf);
156
+ const keyPair = encryption2.makeKeyPair(privKeyBuf);
119
157
  const sigProvider = newSignatureProvider({
120
158
  privKey: keyPair.privKey,
121
159
  pubKey: keyPair.pubKey
@@ -226,6 +264,31 @@ async function getJson(url, opts) {
226
264
  }
227
265
  return resp.json();
228
266
  }
267
+ async function postJudgeRequest(url, body, opts) {
268
+ if (!opts?.verifyPubKey) {
269
+ return postJson(url, body, opts);
270
+ }
271
+ const resp = await fetch(url, {
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify(body),
275
+ signal: opts?.timeout ? AbortSignal.timeout(opts.timeout) : void 0
276
+ });
277
+ if (!resp.ok) {
278
+ const text = await resp.text().catch(() => "");
279
+ throw enrichError(resp.status, text, resp.statusText, opts);
280
+ }
281
+ const buf = new Uint8Array(await resp.arrayBuffer());
282
+ const verdict = verifyJudgeResponseSignature(
283
+ buf,
284
+ resp.headers.get("X-Atbash-Signature"),
285
+ opts.verifyPubKey
286
+ );
287
+ if (!verdict.ok) {
288
+ throw new Error(`signature verification failed: ${verdict.reason}`);
289
+ }
290
+ return JSON.parse(new TextDecoder().decode(buf));
291
+ }
229
292
  async function judgeAction(action, context = "", auth, opts) {
230
293
  if (!action || !action.trim()) {
231
294
  throw new Error("action is required and cannot be empty.");
@@ -263,7 +326,7 @@ async function judgeAction(action, context = "", auth, opts) {
263
326
  ...opts?.toolName && { tool_name: opts.toolName },
264
327
  ...opts?.model && { model: opts.model }
265
328
  };
266
- const data = await postJson(url, body, opts);
329
+ const data = await postJudgeRequest(url, body, opts);
267
330
  return {
268
331
  verdict: normalizeVerdict(data.verdict),
269
332
  action_type: String(data.action_type || ""),
@@ -275,8 +338,8 @@ async function judgeAction(action, context = "", auth, opts) {
275
338
  on_chain: Boolean(data.on_chain)
276
339
  };
277
340
  }
278
- async function getJudgmentStatus(judgmentId, opts) {
279
- const url = `${baseUrl(opts)}/api/v1/judge?tool_call_id=${encodeURIComponent(judgmentId)}`;
341
+ async function getJudgmentStatus(judgmentId, agentPubkey, opts) {
342
+ const url = `${baseUrl(opts)}/api/v1/judge?tool_call_id=${encodeURIComponent(judgmentId)}&agent_pubkey=${encodeURIComponent(agentPubkey)}`;
280
343
  const data = await getJson(url, opts);
281
344
  return {
282
345
  status: normalizeStatus(data.status),
@@ -390,17 +453,355 @@ async function getSafetyStats(opts) {
390
453
  const result = await getJson(url, opts);
391
454
  return result?.data || result;
392
455
  }
456
+
457
+ // src/config.ts
458
+ var ALLOWED_JUDGE_HOSTS = /* @__PURE__ */ new Set([
459
+ "atbash.ai",
460
+ "www.atbash.ai"
461
+ ]);
462
+ function validateJudgeEndpoint(judge) {
463
+ const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
464
+ const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
465
+ let parsed;
466
+ try {
467
+ parsed = new URL(candidate);
468
+ } catch {
469
+ throw new Error(
470
+ `[atbash] invalid judge endpoint URL: ${candidate}. Refusing to load \u2014 fix the URL or omit it to use the default (${DEFAULT_ENDPOINT}).`
471
+ );
472
+ }
473
+ if (parsed.protocol !== "https:") {
474
+ throw new Error(
475
+ `[atbash] judge endpoint must use https:// (got "${parsed.protocol}"). Refusing to load \u2014 plaintext endpoints leak verdicts and enable trivial MITM bypass.`
476
+ );
477
+ }
478
+ if (parsed.username || parsed.password) {
479
+ throw new Error(
480
+ `[atbash] judge endpoint must not contain credentials (user:pass@host). Refusing to load \u2014 credentials embedded in URLs leak to logs and process listings.`
481
+ );
482
+ }
483
+ const normalisedUrl = parsed.origin;
484
+ if (policy === "self-hosted") {
485
+ const verifyPubKey = judge?.verifyPubKey;
486
+ const key = verifyPubKey?.trim().toLowerCase();
487
+ if (!key || !/^[0-9a-f]{66}$/.test(key)) {
488
+ throw new Error(
489
+ `[atbash] judge endpoint policy "self-hosted" requires verifyPubKey to be a 66-hex-char compressed secp256k1 pubkey. Refusing to load \u2014 self-hosted judges must produce signed responses so the SDK can detect a malicious or compromised judge.`
490
+ );
491
+ }
492
+ return { url: normalisedUrl, policy, verifyPubKey: key };
493
+ }
494
+ if (!ALLOWED_JUDGE_HOSTS.has(parsed.hostname.toLowerCase())) {
495
+ throw new Error(
496
+ `[atbash] judge endpoint hostname "${parsed.hostname}" is not in the trusted allowlist. Allowed: ${[...ALLOWED_JUDGE_HOSTS].join(", ")}. To use a self-hosted judge, set BOTH policy="self-hosted" AND verifyPubKey to the 66-hex pubkey of your judge's response-signing key. Refusing to load \u2014 silent endpoint redirection is a known attack vector (F-003).`
497
+ );
498
+ }
499
+ return { url: normalisedUrl, policy, verifyPubKey: null };
500
+ }
501
+
502
+ // src/key-loader.ts
503
+ var import_node_fs = require("fs");
504
+ var import_node_os = require("os");
505
+ var import_node_path = require("path");
506
+ var DEFAULT_KEY_PATH_REL = ".config/atbash/guard-client-key";
507
+ function resolveKeyPath(input) {
508
+ if (input) return expandHome(input);
509
+ const home = process.env.HOME || (0, import_node_os.homedir)() || "";
510
+ return (0, import_node_path.join)(home, DEFAULT_KEY_PATH_REL);
511
+ }
512
+ function expandHome(p) {
513
+ if (!p.startsWith("~/")) return p;
514
+ const home = process.env.HOME || (0, import_node_os.homedir)() || "";
515
+ return (0, import_node_path.join)(home, p.slice(2));
516
+ }
517
+ function readKeyFile(keyPath) {
518
+ const content = String((0, import_node_fs.readFileSync)(keyPath, "utf8") || "").trim();
519
+ let privKey = "";
520
+ let pubKey = "";
521
+ if (content.startsWith("{")) {
522
+ const creds = JSON.parse(content);
523
+ privKey = String(
524
+ creds.privKey || creds.privkey || creds.privateKey || ""
525
+ ).trim();
526
+ pubKey = String(
527
+ creds.pubKey || creds.pubkey || creds.publicKey || ""
528
+ ).trim();
529
+ } else {
530
+ const lines = content.split(/\r?\n/);
531
+ for (const line of lines) {
532
+ if (line.startsWith("privkey=")) privKey = line.slice("privkey=".length).trim();
533
+ if (line.startsWith("pubkey=")) pubKey = line.slice("pubkey=".length).trim();
534
+ }
535
+ }
536
+ if (!privKey || !pubKey) {
537
+ throw new Error(`atbash key file missing priv/pub key fields: ${keyPath}`);
538
+ }
539
+ privKey = privKey.replace(/^0x/, "");
540
+ return { privKey, pubKey };
541
+ }
542
+ function loadAgentFromFile(keyPath) {
543
+ const resolved = resolveKeyPath(keyPath);
544
+ const { privKey } = readKeyFile(resolved);
545
+ return loadAgent(privKey);
546
+ }
547
+
548
+ // src/redact-secrets.ts
549
+ var PATTERNS = [
550
+ { kind: "anthropic", re: /\bsk-ant-[A-Za-z0-9_-]{20,}/g },
551
+ { kind: "openai_project", re: /\bsk-proj-[A-Za-z0-9_-]{20,}/g },
552
+ { kind: "openai", re: /\bsk-[A-Za-z0-9]{20,}/g },
553
+ { kind: "github", re: /\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{30,}/g },
554
+ { kind: "google", re: /\bAIza[0-9A-Za-z_-]{35}/g },
555
+ { kind: "google_oauth", re: /\bya29\.[0-9A-Za-z_-]{20,}/g },
556
+ {
557
+ kind: "aws_access_key",
558
+ re: /\b(?:AKIA|ASIA|AGPA|AROA|ANPA|ANVA|ASCA|AIDA|AIPA)[0-9A-Z]{16}\b/g
559
+ },
560
+ { kind: "stripe", re: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g },
561
+ { kind: "slack", re: /\bxox[abprseo]-[A-Za-z0-9-]{10,}/g },
562
+ {
563
+ kind: "slack_webhook",
564
+ re: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9]+\/B[A-Za-z0-9]+\/[A-Za-z0-9]{20,}/g
565
+ },
566
+ { kind: "sendgrid", re: /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g },
567
+ { kind: "twilio_sid", re: /\bAC[0-9a-fA-F]{32}\b/g },
568
+ { kind: "mailgun", re: /\bkey-[0-9a-f]{32}\b/g },
569
+ { kind: "npm_token", re: /\bnpm_[A-Za-z0-9]{36,}\b/g },
570
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
571
+ {
572
+ kind: "private_key_pem",
573
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
574
+ },
575
+ {
576
+ kind: "aws_secret_key",
577
+ re: /(?:aws[_-]?secret|secret[_-]?access[_-]?key)["'\s:=]{1,10}[A-Za-z0-9/+=]{40}/gi
578
+ },
579
+ {
580
+ kind: "generic_token",
581
+ re: /\b(?=[A-Za-z0-9_-]*[0-9])(?=[A-Za-z0-9_-]*[A-Za-z])(?![0-9a-fA-F]+$)[A-Za-z0-9_-]{32,}\b/g
582
+ },
583
+ {
584
+ kind: "base64",
585
+ re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*[+/])(?=[A-Za-z0-9+/]*[0-9])(?=[A-Za-z0-9+/]*[A-Za-z])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g
586
+ },
587
+ {
588
+ kind: "context_secret",
589
+ re: /(?<![A-Za-z0-9_])(?:api[_-]?key|api[_-]?secret|access[_-]?token|refresh[_-]?token|auth[_-]?token|client[_-]?secret|password|passwd|pwd|secret|token|credential|private[_-]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9+/=._-]{12,})(?=["'\s,;)\]}>]|$)/gi,
590
+ groupOnly: true
591
+ },
592
+ {
593
+ kind: "bearer",
594
+ re: /(?<![A-Za-z0-9_])Bearer\s+([A-Za-z0-9._-]{20,})\b/gi,
595
+ groupOnly: true
596
+ }
597
+ ];
598
+ function redactSecrets(input) {
599
+ if (!input) return { redacted: input ?? "", found: [] };
600
+ const found = [];
601
+ let working = input;
602
+ for (const { kind, re, groupOnly } of PATTERNS) {
603
+ if (groupOnly) {
604
+ working = working.replace(re, (full, value) => {
605
+ if (typeof value !== "string" || !value) return full;
606
+ found.push({ kind, length: value.length });
607
+ return full.replace(value, `[REDACTED:${kind}]`);
608
+ });
609
+ } else {
610
+ working = working.replace(re, (m) => {
611
+ found.push({ kind, length: m.length });
612
+ return `[REDACTED:${kind}]`;
613
+ });
614
+ }
615
+ }
616
+ return { redacted: working, found };
617
+ }
618
+
619
+ // src/factory.ts
620
+ function createAtbashClient(config = {}) {
621
+ const validated = validateJudgeEndpoint(config.judge);
622
+ const failClosed = config.failClosed !== false;
623
+ const logger = config.logger ?? {};
624
+ const inlineKeyPair = config.keyPair;
625
+ const keyPath = inlineKeyPair ? null : config.keyPath;
626
+ if (validated.url !== DEFAULT_ENDPOINT) {
627
+ logger.warn?.("[atbash] running on non-default judge endpoint", {
628
+ endpoint: validated.url,
629
+ policy: validated.policy,
630
+ verifying: validated.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
631
+ });
632
+ }
633
+ let cachedAgent = inlineKeyPair ? loadAgent(inlineKeyPair.privKey) : null;
634
+ function loadAgentOnce() {
635
+ if (cachedAgent) return cachedAgent;
636
+ cachedAgent = loadAgentFromFile(keyPath ?? void 0);
637
+ return cachedAgent;
638
+ }
639
+ function fail(reason, toolCallId) {
640
+ return { allow: !failClosed, verdict: "ERROR", reason, toolCallId };
641
+ }
642
+ return {
643
+ async auditToolCall(input) {
644
+ let agent;
645
+ try {
646
+ agent = loadAgentOnce();
647
+ } catch (err) {
648
+ const message = String(err?.message ?? err ?? "");
649
+ logger.warn?.("[atbash] failed to load key pair, blocking for safety", {
650
+ error: message
651
+ });
652
+ return fail("key load failed, blocking for safety");
653
+ }
654
+ const toolName = input.toolName || "unknown";
655
+ const argsRedaction = redactSecrets(stringifyArgs(input.args));
656
+ const ctxRedaction = redactSecrets(input.context ?? toolName);
657
+ const argsJson = argsRedaction.redacted;
658
+ const actionText = truncate(argsJson);
659
+ const contextText = ctxRedaction.redacted;
660
+ const totalRedactions = argsRedaction.found.length + ctxRedaction.found.length;
661
+ if (totalRedactions > 0) {
662
+ const kinds = [.../* @__PURE__ */ new Set([
663
+ ...argsRedaction.found.map((f) => f.kind),
664
+ ...ctxRedaction.found.map((f) => f.kind)
665
+ ])];
666
+ logger.warn?.("[atbash] redacted secrets before judge call", {
667
+ tool: toolName,
668
+ count: totalRedactions,
669
+ kinds
670
+ });
671
+ }
672
+ try {
673
+ logger.info?.("[atbash] judge API called", { tool: toolName });
674
+ const result = await judgeAction(actionText, contextText, agent, {
675
+ endpoint: validated.url,
676
+ verifyPubKey: validated.verifyPubKey ?? void 0,
677
+ toolName,
678
+ toolArgsJson: argsJson,
679
+ chainOpts: {
680
+ nodeUrls: config.nodeUrls,
681
+ blockchainRid: config.blockchainRid
682
+ }
683
+ });
684
+ if (result.verdict === "No verdict") {
685
+ return {
686
+ allow: true,
687
+ verdict: "ALLOW",
688
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
689
+ toolCallId: result.tool_call_id
690
+ };
691
+ }
692
+ const action = result.action_type;
693
+ if (action === "block") {
694
+ return {
695
+ allow: false,
696
+ verdict: "BLOCK",
697
+ reason: result.reason,
698
+ toolCallId: result.tool_call_id
699
+ };
700
+ }
701
+ if (action === "hold_for_user_confirm") {
702
+ return {
703
+ allow: false,
704
+ verdict: "HOLD",
705
+ reason: result.reason || "held for human confirmation",
706
+ toolCallId: result.tool_call_id
707
+ };
708
+ }
709
+ if (action === "allow") {
710
+ const surfacedVerdict = result.verdict === "ALLOW" || result.verdict === "HOLD" || result.verdict === "BLOCK" ? result.verdict : "ALLOW";
711
+ return {
712
+ allow: true,
713
+ verdict: surfacedVerdict,
714
+ reason: result.reason,
715
+ toolCallId: result.tool_call_id
716
+ };
717
+ }
718
+ return fail("unrecognized action_type from judge", result.tool_call_id);
719
+ } catch (err) {
720
+ const message = String(err?.message ?? err ?? "");
721
+ logger.warn?.("[atbash] judge API failed", { reason: message });
722
+ return fail(message);
723
+ }
724
+ }
725
+ };
726
+ }
727
+ function stringifyArgs(args) {
728
+ if (args == null) return "";
729
+ if (typeof args === "string") return args;
730
+ try {
731
+ return JSON.stringify(args);
732
+ } catch {
733
+ return String(args);
734
+ }
735
+ }
736
+ var MAX_ACTION_LEN = 4e3;
737
+ function truncate(text) {
738
+ if (text.length <= MAX_ACTION_LEN) return text;
739
+ return text.slice(0, MAX_ACTION_LEN) + "\u2026";
740
+ }
741
+
742
+ // src/user-config.ts
743
+ var import_node_fs2 = require("fs");
744
+ var import_node_os2 = require("os");
745
+ var import_node_path2 = require("path");
746
+ var ENV_MAP = {
747
+ agentKey: "ATBASH_AGENT_KEY",
748
+ orgName: "ATBASH_ORG_NAME",
749
+ judgeEndpoint: "ATBASH_ENDPOINT",
750
+ blockchainRid: "ATBASH_BLOCKCHAIN_RID",
751
+ provider: "ATBASH_PROVIDER",
752
+ providerModel: "ATBASH_PROVIDER_MODEL"
753
+ };
754
+ function getConfigDir() {
755
+ const home = process.env.HOME || (0, import_node_os2.homedir)() || "";
756
+ return (0, import_node_path2.join)(home, ".config", "atbash");
757
+ }
758
+ function getConfigPath() {
759
+ return (0, import_node_path2.join)(getConfigDir(), "config.json");
760
+ }
761
+ function loadUserConfig() {
762
+ try {
763
+ const p = getConfigPath();
764
+ if (!(0, import_node_fs2.existsSync)(p)) return {};
765
+ const raw = (0, import_node_fs2.readFileSync)(p, "utf-8").trim();
766
+ if (!raw) return {};
767
+ return JSON.parse(raw);
768
+ } catch (err) {
769
+ console.error("Failed to load config file", err);
770
+ return {};
771
+ }
772
+ }
773
+ function saveUserConfig(config) {
774
+ const dir = getConfigDir();
775
+ if (!(0, import_node_fs2.existsSync)(dir)) {
776
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
777
+ }
778
+ (0, import_node_fs2.writeFileSync)(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
779
+ }
780
+ function resolve(key, flagValue) {
781
+ if (flagValue) return flagValue;
782
+ const envName = ENV_MAP[key];
783
+ if (envName) {
784
+ const envVal = process.env[envName];
785
+ if (envVal) return envVal;
786
+ }
787
+ const fileVal = loadUserConfig()[key];
788
+ if (fileVal != null) return String(fileVal);
789
+ return "";
790
+ }
393
791
  // Annotate the CommonJS export names for ESM import in node:
394
792
  0 && (module.exports = {
395
793
  DEFAULT_BLOCKCHAIN_RID,
396
794
  DEFAULT_CHROMIA_NODE_URLS,
397
795
  DEFAULT_ENDPOINT,
398
796
  checkAgentExists,
797
+ createAtbashClient,
399
798
  derivePublicKey,
400
799
  generateKeyPair,
401
800
  getAgentDetail,
402
801
  getAgentPolicy,
403
802
  getAgentToolCalls,
803
+ getConfigDir,
804
+ getConfigPath,
404
805
  getHeldActionReviews,
405
806
  getJudgmentStatus,
406
807
  getOrgTierInfo,
@@ -413,6 +814,13 @@ async function getSafetyStats(opts) {
413
814
  isValidPrivateKey,
414
815
  judgeAction,
415
816
  loadAgent,
817
+ loadAgentFromFile,
818
+ loadUserConfig,
416
819
  logToolCall,
417
- toPubkeyHex
820
+ resolve,
821
+ resolveKeyPath,
822
+ saveUserConfig,
823
+ toPubkeyHex,
824
+ validateJudgeEndpoint,
825
+ verifyJudgeResponseSignature
418
826
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  type Verdict = "ALLOW" | "HOLD" | "BLOCK" | "No verdict";
2
- type Provider = "atbash" | "openai" | "google" | "microsoft" | "custom" | (string & {});
2
+ type Provider = "openai" | "google" | "microsoft" | "custom" | (string & {});
3
3
  type Tier = "audit" | "audit_plus" | "enforcement" | (string & {});
4
4
  type ActionType = "allow" | "hold_for_user_confirm" | "block" | (string & {});
5
5
  type PubkeyValue = string | Buffer | {
@@ -40,6 +40,7 @@ interface JudgeOptions extends ClientOpts {
40
40
  toolName?: string;
41
41
  toolArgsJson?: string;
42
42
  chainOpts?: ChainOpts;
43
+ verifyPubKey?: string;
43
44
  }
44
45
  interface JudgmentStatus {
45
46
  status: JudgmentStatusState;
@@ -106,6 +107,46 @@ interface AgentPolicy {
106
107
  is_custom: boolean;
107
108
  default_policy: string;
108
109
  }
110
+ type DecisionVerdict = "ALLOW" | "HOLD" | "BLOCK" | "ERROR";
111
+ interface Decision {
112
+ allow: boolean;
113
+ verdict: DecisionVerdict;
114
+ reason?: string;
115
+ toolCallId?: string;
116
+ }
117
+ interface ToolCallInput {
118
+ toolName: string;
119
+ args?: unknown;
120
+ context?: string;
121
+ }
122
+ type JudgeEndpointConfig = {
123
+ policy?: "default";
124
+ endpoint?: string;
125
+ } | {
126
+ policy: "self-hosted";
127
+ endpoint: string;
128
+ verifyPubKey: string;
129
+ };
130
+ interface ValidatedEndpoint {
131
+ url: string;
132
+ policy: "default" | "self-hosted";
133
+ verifyPubKey: string | null;
134
+ }
135
+ interface AtbashClientConfig {
136
+ judge?: JudgeEndpointConfig;
137
+ nodeUrls?: string[];
138
+ blockchainRid?: string;
139
+ keyPath?: string;
140
+ keyPair?: {
141
+ privKey: string;
142
+ pubKey: string;
143
+ };
144
+ failClosed?: boolean;
145
+ logger?: {
146
+ info?(...a: unknown[]): void;
147
+ warn?(...a: unknown[]): void;
148
+ };
149
+ }
109
150
 
110
151
  declare const DEFAULT_ENDPOINT = "https://atbash.ai";
111
152
  declare const DEFAULT_CHROMIA_NODE_URLS: string[];
@@ -135,7 +176,7 @@ declare function logToolCall(action: string, context: string, auth: AgentAuth, c
135
176
  toolArgsJson?: string;
136
177
  }, clientOpts?: ClientOpts): Promise<LogToolCallResult>;
137
178
  declare function judgeAction(action: string, context: string | undefined, auth: AgentAuth, opts?: JudgeOptions): Promise<JudgeResult>;
138
- declare function getJudgmentStatus(judgmentId: string, opts?: ClientOpts): Promise<JudgmentStatus>;
179
+ declare function getJudgmentStatus(judgmentId: string, agentPubkey: string, opts?: ClientOpts): Promise<JudgmentStatus>;
139
180
  declare function getToolCalls(maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
140
181
  declare function getOrgToolCalls(orgName: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
141
182
  declare function getAgentToolCalls(agentPubkey: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
@@ -148,4 +189,33 @@ declare function getAgentDetail(agentPubkey: string, opts?: ClientOpts): Promise
148
189
  declare function getAgentPolicy(agentPubkey: string, opts?: ClientOpts): Promise<AgentPolicy>;
149
190
  declare function getSafetyStats(opts?: ClientOpts): Promise<Record<string, unknown>>;
150
191
 
151
- export { type ActionType, type AgentAuth, type AgentPolicy, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type HeldAction, type HeldActionReview, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallRecord, type Verdict, checkAgentExists, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, logToolCall, toPubkeyHex };
192
+ interface AtbashClient {
193
+ auditToolCall(input: ToolCallInput): Promise<Decision>;
194
+ }
195
+ declare function createAtbashClient(config?: AtbashClientConfig): AtbashClient;
196
+
197
+ declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
198
+
199
+ declare function resolveKeyPath(input?: string): string;
200
+ declare function loadAgentFromFile(keyPath?: string): AgentAuth;
201
+
202
+ declare function verifyJudgeResponseSignature(bodyBytes: Uint8Array, signatureHex: string | null, pubKeyHex: string): {
203
+ ok: boolean;
204
+ reason?: string;
205
+ };
206
+
207
+ interface AtbashUserConfig {
208
+ agentKey?: string;
209
+ orgName?: string;
210
+ judgeEndpoint?: string;
211
+ blockchainRid?: string;
212
+ provider?: string;
213
+ providerModel?: string;
214
+ }
215
+ declare function getConfigDir(): string;
216
+ declare function getConfigPath(): string;
217
+ declare function loadUserConfig(): AtbashUserConfig;
218
+ declare function saveUserConfig(config: AtbashUserConfig): void;
219
+ declare function resolve(key: keyof AtbashUserConfig, flagValue?: string): string;
220
+
221
+ export { type ActionType, type AgentAuth, type AgentPolicy, type AtbashClient, type AtbashClientConfig, type AtbashUserConfig, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type Decision, type DecisionVerdict, type HeldAction, type HeldActionReview, type JudgeEndpointConfig, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, checkAgentExists, createAtbashClient, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getConfigDir, getConfigPath, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, loadAgentFromFile, loadUserConfig, logToolCall, resolve, resolveKeyPath, saveUserConfig, toPubkeyHex, validateJudgeEndpoint, verifyJudgeResponseSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  type Verdict = "ALLOW" | "HOLD" | "BLOCK" | "No verdict";
2
- type Provider = "atbash" | "openai" | "google" | "microsoft" | "custom" | (string & {});
2
+ type Provider = "openai" | "google" | "microsoft" | "custom" | (string & {});
3
3
  type Tier = "audit" | "audit_plus" | "enforcement" | (string & {});
4
4
  type ActionType = "allow" | "hold_for_user_confirm" | "block" | (string & {});
5
5
  type PubkeyValue = string | Buffer | {
@@ -40,6 +40,7 @@ interface JudgeOptions extends ClientOpts {
40
40
  toolName?: string;
41
41
  toolArgsJson?: string;
42
42
  chainOpts?: ChainOpts;
43
+ verifyPubKey?: string;
43
44
  }
44
45
  interface JudgmentStatus {
45
46
  status: JudgmentStatusState;
@@ -106,6 +107,46 @@ interface AgentPolicy {
106
107
  is_custom: boolean;
107
108
  default_policy: string;
108
109
  }
110
+ type DecisionVerdict = "ALLOW" | "HOLD" | "BLOCK" | "ERROR";
111
+ interface Decision {
112
+ allow: boolean;
113
+ verdict: DecisionVerdict;
114
+ reason?: string;
115
+ toolCallId?: string;
116
+ }
117
+ interface ToolCallInput {
118
+ toolName: string;
119
+ args?: unknown;
120
+ context?: string;
121
+ }
122
+ type JudgeEndpointConfig = {
123
+ policy?: "default";
124
+ endpoint?: string;
125
+ } | {
126
+ policy: "self-hosted";
127
+ endpoint: string;
128
+ verifyPubKey: string;
129
+ };
130
+ interface ValidatedEndpoint {
131
+ url: string;
132
+ policy: "default" | "self-hosted";
133
+ verifyPubKey: string | null;
134
+ }
135
+ interface AtbashClientConfig {
136
+ judge?: JudgeEndpointConfig;
137
+ nodeUrls?: string[];
138
+ blockchainRid?: string;
139
+ keyPath?: string;
140
+ keyPair?: {
141
+ privKey: string;
142
+ pubKey: string;
143
+ };
144
+ failClosed?: boolean;
145
+ logger?: {
146
+ info?(...a: unknown[]): void;
147
+ warn?(...a: unknown[]): void;
148
+ };
149
+ }
109
150
 
110
151
  declare const DEFAULT_ENDPOINT = "https://atbash.ai";
111
152
  declare const DEFAULT_CHROMIA_NODE_URLS: string[];
@@ -135,7 +176,7 @@ declare function logToolCall(action: string, context: string, auth: AgentAuth, c
135
176
  toolArgsJson?: string;
136
177
  }, clientOpts?: ClientOpts): Promise<LogToolCallResult>;
137
178
  declare function judgeAction(action: string, context: string | undefined, auth: AgentAuth, opts?: JudgeOptions): Promise<JudgeResult>;
138
- declare function getJudgmentStatus(judgmentId: string, opts?: ClientOpts): Promise<JudgmentStatus>;
179
+ declare function getJudgmentStatus(judgmentId: string, agentPubkey: string, opts?: ClientOpts): Promise<JudgmentStatus>;
139
180
  declare function getToolCalls(maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
140
181
  declare function getOrgToolCalls(orgName: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
141
182
  declare function getAgentToolCalls(agentPubkey: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>;
@@ -148,4 +189,33 @@ declare function getAgentDetail(agentPubkey: string, opts?: ClientOpts): Promise
148
189
  declare function getAgentPolicy(agentPubkey: string, opts?: ClientOpts): Promise<AgentPolicy>;
149
190
  declare function getSafetyStats(opts?: ClientOpts): Promise<Record<string, unknown>>;
150
191
 
151
- export { type ActionType, type AgentAuth, type AgentPolicy, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type HeldAction, type HeldActionReview, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallRecord, type Verdict, checkAgentExists, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, logToolCall, toPubkeyHex };
192
+ interface AtbashClient {
193
+ auditToolCall(input: ToolCallInput): Promise<Decision>;
194
+ }
195
+ declare function createAtbashClient(config?: AtbashClientConfig): AtbashClient;
196
+
197
+ declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
198
+
199
+ declare function resolveKeyPath(input?: string): string;
200
+ declare function loadAgentFromFile(keyPath?: string): AgentAuth;
201
+
202
+ declare function verifyJudgeResponseSignature(bodyBytes: Uint8Array, signatureHex: string | null, pubKeyHex: string): {
203
+ ok: boolean;
204
+ reason?: string;
205
+ };
206
+
207
+ interface AtbashUserConfig {
208
+ agentKey?: string;
209
+ orgName?: string;
210
+ judgeEndpoint?: string;
211
+ blockchainRid?: string;
212
+ provider?: string;
213
+ providerModel?: string;
214
+ }
215
+ declare function getConfigDir(): string;
216
+ declare function getConfigPath(): string;
217
+ declare function loadUserConfig(): AtbashUserConfig;
218
+ declare function saveUserConfig(config: AtbashUserConfig): void;
219
+ declare function resolve(key: keyof AtbashUserConfig, flagValue?: string): string;
220
+
221
+ export { type ActionType, type AgentAuth, type AgentPolicy, type AtbashClient, type AtbashClientConfig, type AtbashUserConfig, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type Decision, type DecisionVerdict, type HeldAction, type HeldActionReview, type JudgeEndpointConfig, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, checkAgentExists, createAtbashClient, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getConfigDir, getConfigPath, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, loadAgentFromFile, loadUserConfig, logToolCall, resolve, resolveKeyPath, saveUserConfig, toPubkeyHex, validateJudgeEndpoint, verifyJudgeResponseSignature };
package/dist/index.js CHANGED
@@ -1,7 +1,35 @@
1
1
  // src/client.ts
2
2
  import { createECDH, randomBytes } from "crypto";
3
+ import postchain2 from "postchain-client";
4
+
5
+ // src/signature.ts
3
6
  import postchain from "postchain-client";
4
- var { createClient, encryption, newSignatureProvider } = postchain;
7
+ var { encryption } = postchain;
8
+ function verifyJudgeResponseSignature(bodyBytes, signatureHex, pubKeyHex) {
9
+ if (!signatureHex) {
10
+ return { ok: false, reason: "missing X-Atbash-Signature header" };
11
+ }
12
+ const sigClean = signatureHex.trim().toLowerCase().replace(/^0x/, "");
13
+ if (!/^[0-9a-f]+$/.test(sigClean) || sigClean.length < 64 || sigClean.length > 256) {
14
+ return { ok: false, reason: "malformed signature header" };
15
+ }
16
+ let isValid = false;
17
+ try {
18
+ const digest = encryption.sha256(Buffer.from(bodyBytes));
19
+ const pubKeyBytes = Buffer.from(pubKeyHex.replace(/^0x/, ""), "hex");
20
+ const sigBytes = Buffer.from(sigClean, "hex");
21
+ isValid = encryption.checkDigestSignature(digest, pubKeyBytes, sigBytes);
22
+ } catch (err) {
23
+ const message = String(
24
+ err?.message ?? err ?? ""
25
+ );
26
+ return { ok: false, reason: `signature verification threw: ${message}` };
27
+ }
28
+ return isValid ? { ok: true } : { ok: false, reason: "signature does not verify against configured verifyPubKey" };
29
+ }
30
+
31
+ // src/client.ts
32
+ var { createClient, encryption: encryption2, newSignatureProvider } = postchain2;
5
33
  var DEFAULT_ENDPOINT = "https://atbash.ai";
6
34
  var DEFAULT_CHROMIA_NODE_URLS = [
7
35
  "https://node6.testnet.chromia.com:7740",
@@ -57,7 +85,7 @@ async function buildSignedTx(opName, args, auth, chainOpts) {
57
85
  const blockchainRid = chainOpts?.blockchainRid ?? DEFAULT_BLOCKCHAIN_RID;
58
86
  const client = await createClient({ nodeUrlPool: nodeUrls, blockchainRid });
59
87
  const privKeyBuf = Buffer.from(auth.privkey, "hex");
60
- const keyPair = encryption.makeKeyPair(privKeyBuf);
88
+ const keyPair = encryption2.makeKeyPair(privKeyBuf);
61
89
  const sigProvider = newSignatureProvider({
62
90
  privKey: keyPair.privKey,
63
91
  pubKey: keyPair.pubKey
@@ -168,6 +196,31 @@ async function getJson(url, opts) {
168
196
  }
169
197
  return resp.json();
170
198
  }
199
+ async function postJudgeRequest(url, body, opts) {
200
+ if (!opts?.verifyPubKey) {
201
+ return postJson(url, body, opts);
202
+ }
203
+ const resp = await fetch(url, {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json" },
206
+ body: JSON.stringify(body),
207
+ signal: opts?.timeout ? AbortSignal.timeout(opts.timeout) : void 0
208
+ });
209
+ if (!resp.ok) {
210
+ const text = await resp.text().catch(() => "");
211
+ throw enrichError(resp.status, text, resp.statusText, opts);
212
+ }
213
+ const buf = new Uint8Array(await resp.arrayBuffer());
214
+ const verdict = verifyJudgeResponseSignature(
215
+ buf,
216
+ resp.headers.get("X-Atbash-Signature"),
217
+ opts.verifyPubKey
218
+ );
219
+ if (!verdict.ok) {
220
+ throw new Error(`signature verification failed: ${verdict.reason}`);
221
+ }
222
+ return JSON.parse(new TextDecoder().decode(buf));
223
+ }
171
224
  async function judgeAction(action, context = "", auth, opts) {
172
225
  if (!action || !action.trim()) {
173
226
  throw new Error("action is required and cannot be empty.");
@@ -205,7 +258,7 @@ async function judgeAction(action, context = "", auth, opts) {
205
258
  ...opts?.toolName && { tool_name: opts.toolName },
206
259
  ...opts?.model && { model: opts.model }
207
260
  };
208
- const data = await postJson(url, body, opts);
261
+ const data = await postJudgeRequest(url, body, opts);
209
262
  return {
210
263
  verdict: normalizeVerdict(data.verdict),
211
264
  action_type: String(data.action_type || ""),
@@ -217,8 +270,8 @@ async function judgeAction(action, context = "", auth, opts) {
217
270
  on_chain: Boolean(data.on_chain)
218
271
  };
219
272
  }
220
- async function getJudgmentStatus(judgmentId, opts) {
221
- const url = `${baseUrl(opts)}/api/v1/judge?tool_call_id=${encodeURIComponent(judgmentId)}`;
273
+ async function getJudgmentStatus(judgmentId, agentPubkey, opts) {
274
+ const url = `${baseUrl(opts)}/api/v1/judge?tool_call_id=${encodeURIComponent(judgmentId)}&agent_pubkey=${encodeURIComponent(agentPubkey)}`;
222
275
  const data = await getJson(url, opts);
223
276
  return {
224
277
  status: normalizeStatus(data.status),
@@ -332,16 +385,354 @@ async function getSafetyStats(opts) {
332
385
  const result = await getJson(url, opts);
333
386
  return result?.data || result;
334
387
  }
388
+
389
+ // src/config.ts
390
+ var ALLOWED_JUDGE_HOSTS = /* @__PURE__ */ new Set([
391
+ "atbash.ai",
392
+ "www.atbash.ai"
393
+ ]);
394
+ function validateJudgeEndpoint(judge) {
395
+ const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
396
+ const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
397
+ let parsed;
398
+ try {
399
+ parsed = new URL(candidate);
400
+ } catch {
401
+ throw new Error(
402
+ `[atbash] invalid judge endpoint URL: ${candidate}. Refusing to load \u2014 fix the URL or omit it to use the default (${DEFAULT_ENDPOINT}).`
403
+ );
404
+ }
405
+ if (parsed.protocol !== "https:") {
406
+ throw new Error(
407
+ `[atbash] judge endpoint must use https:// (got "${parsed.protocol}"). Refusing to load \u2014 plaintext endpoints leak verdicts and enable trivial MITM bypass.`
408
+ );
409
+ }
410
+ if (parsed.username || parsed.password) {
411
+ throw new Error(
412
+ `[atbash] judge endpoint must not contain credentials (user:pass@host). Refusing to load \u2014 credentials embedded in URLs leak to logs and process listings.`
413
+ );
414
+ }
415
+ const normalisedUrl = parsed.origin;
416
+ if (policy === "self-hosted") {
417
+ const verifyPubKey = judge?.verifyPubKey;
418
+ const key = verifyPubKey?.trim().toLowerCase();
419
+ if (!key || !/^[0-9a-f]{66}$/.test(key)) {
420
+ throw new Error(
421
+ `[atbash] judge endpoint policy "self-hosted" requires verifyPubKey to be a 66-hex-char compressed secp256k1 pubkey. Refusing to load \u2014 self-hosted judges must produce signed responses so the SDK can detect a malicious or compromised judge.`
422
+ );
423
+ }
424
+ return { url: normalisedUrl, policy, verifyPubKey: key };
425
+ }
426
+ if (!ALLOWED_JUDGE_HOSTS.has(parsed.hostname.toLowerCase())) {
427
+ throw new Error(
428
+ `[atbash] judge endpoint hostname "${parsed.hostname}" is not in the trusted allowlist. Allowed: ${[...ALLOWED_JUDGE_HOSTS].join(", ")}. To use a self-hosted judge, set BOTH policy="self-hosted" AND verifyPubKey to the 66-hex pubkey of your judge's response-signing key. Refusing to load \u2014 silent endpoint redirection is a known attack vector (F-003).`
429
+ );
430
+ }
431
+ return { url: normalisedUrl, policy, verifyPubKey: null };
432
+ }
433
+
434
+ // src/key-loader.ts
435
+ import { readFileSync } from "fs";
436
+ import { homedir } from "os";
437
+ import { join } from "path";
438
+ var DEFAULT_KEY_PATH_REL = ".config/atbash/guard-client-key";
439
+ function resolveKeyPath(input) {
440
+ if (input) return expandHome(input);
441
+ const home = process.env.HOME || homedir() || "";
442
+ return join(home, DEFAULT_KEY_PATH_REL);
443
+ }
444
+ function expandHome(p) {
445
+ if (!p.startsWith("~/")) return p;
446
+ const home = process.env.HOME || homedir() || "";
447
+ return join(home, p.slice(2));
448
+ }
449
+ function readKeyFile(keyPath) {
450
+ const content = String(readFileSync(keyPath, "utf8") || "").trim();
451
+ let privKey = "";
452
+ let pubKey = "";
453
+ if (content.startsWith("{")) {
454
+ const creds = JSON.parse(content);
455
+ privKey = String(
456
+ creds.privKey || creds.privkey || creds.privateKey || ""
457
+ ).trim();
458
+ pubKey = String(
459
+ creds.pubKey || creds.pubkey || creds.publicKey || ""
460
+ ).trim();
461
+ } else {
462
+ const lines = content.split(/\r?\n/);
463
+ for (const line of lines) {
464
+ if (line.startsWith("privkey=")) privKey = line.slice("privkey=".length).trim();
465
+ if (line.startsWith("pubkey=")) pubKey = line.slice("pubkey=".length).trim();
466
+ }
467
+ }
468
+ if (!privKey || !pubKey) {
469
+ throw new Error(`atbash key file missing priv/pub key fields: ${keyPath}`);
470
+ }
471
+ privKey = privKey.replace(/^0x/, "");
472
+ return { privKey, pubKey };
473
+ }
474
+ function loadAgentFromFile(keyPath) {
475
+ const resolved = resolveKeyPath(keyPath);
476
+ const { privKey } = readKeyFile(resolved);
477
+ return loadAgent(privKey);
478
+ }
479
+
480
+ // src/redact-secrets.ts
481
+ var PATTERNS = [
482
+ { kind: "anthropic", re: /\bsk-ant-[A-Za-z0-9_-]{20,}/g },
483
+ { kind: "openai_project", re: /\bsk-proj-[A-Za-z0-9_-]{20,}/g },
484
+ { kind: "openai", re: /\bsk-[A-Za-z0-9]{20,}/g },
485
+ { kind: "github", re: /\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{30,}/g },
486
+ { kind: "google", re: /\bAIza[0-9A-Za-z_-]{35}/g },
487
+ { kind: "google_oauth", re: /\bya29\.[0-9A-Za-z_-]{20,}/g },
488
+ {
489
+ kind: "aws_access_key",
490
+ re: /\b(?:AKIA|ASIA|AGPA|AROA|ANPA|ANVA|ASCA|AIDA|AIPA)[0-9A-Z]{16}\b/g
491
+ },
492
+ { kind: "stripe", re: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g },
493
+ { kind: "slack", re: /\bxox[abprseo]-[A-Za-z0-9-]{10,}/g },
494
+ {
495
+ kind: "slack_webhook",
496
+ re: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9]+\/B[A-Za-z0-9]+\/[A-Za-z0-9]{20,}/g
497
+ },
498
+ { kind: "sendgrid", re: /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g },
499
+ { kind: "twilio_sid", re: /\bAC[0-9a-fA-F]{32}\b/g },
500
+ { kind: "mailgun", re: /\bkey-[0-9a-f]{32}\b/g },
501
+ { kind: "npm_token", re: /\bnpm_[A-Za-z0-9]{36,}\b/g },
502
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
503
+ {
504
+ kind: "private_key_pem",
505
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
506
+ },
507
+ {
508
+ kind: "aws_secret_key",
509
+ re: /(?:aws[_-]?secret|secret[_-]?access[_-]?key)["'\s:=]{1,10}[A-Za-z0-9/+=]{40}/gi
510
+ },
511
+ {
512
+ kind: "generic_token",
513
+ re: /\b(?=[A-Za-z0-9_-]*[0-9])(?=[A-Za-z0-9_-]*[A-Za-z])(?![0-9a-fA-F]+$)[A-Za-z0-9_-]{32,}\b/g
514
+ },
515
+ {
516
+ kind: "base64",
517
+ re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*[+/])(?=[A-Za-z0-9+/]*[0-9])(?=[A-Za-z0-9+/]*[A-Za-z])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g
518
+ },
519
+ {
520
+ kind: "context_secret",
521
+ re: /(?<![A-Za-z0-9_])(?:api[_-]?key|api[_-]?secret|access[_-]?token|refresh[_-]?token|auth[_-]?token|client[_-]?secret|password|passwd|pwd|secret|token|credential|private[_-]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9+/=._-]{12,})(?=["'\s,;)\]}>]|$)/gi,
522
+ groupOnly: true
523
+ },
524
+ {
525
+ kind: "bearer",
526
+ re: /(?<![A-Za-z0-9_])Bearer\s+([A-Za-z0-9._-]{20,})\b/gi,
527
+ groupOnly: true
528
+ }
529
+ ];
530
+ function redactSecrets(input) {
531
+ if (!input) return { redacted: input ?? "", found: [] };
532
+ const found = [];
533
+ let working = input;
534
+ for (const { kind, re, groupOnly } of PATTERNS) {
535
+ if (groupOnly) {
536
+ working = working.replace(re, (full, value) => {
537
+ if (typeof value !== "string" || !value) return full;
538
+ found.push({ kind, length: value.length });
539
+ return full.replace(value, `[REDACTED:${kind}]`);
540
+ });
541
+ } else {
542
+ working = working.replace(re, (m) => {
543
+ found.push({ kind, length: m.length });
544
+ return `[REDACTED:${kind}]`;
545
+ });
546
+ }
547
+ }
548
+ return { redacted: working, found };
549
+ }
550
+
551
+ // src/factory.ts
552
+ function createAtbashClient(config = {}) {
553
+ const validated = validateJudgeEndpoint(config.judge);
554
+ const failClosed = config.failClosed !== false;
555
+ const logger = config.logger ?? {};
556
+ const inlineKeyPair = config.keyPair;
557
+ const keyPath = inlineKeyPair ? null : config.keyPath;
558
+ if (validated.url !== DEFAULT_ENDPOINT) {
559
+ logger.warn?.("[atbash] running on non-default judge endpoint", {
560
+ endpoint: validated.url,
561
+ policy: validated.policy,
562
+ verifying: validated.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
563
+ });
564
+ }
565
+ let cachedAgent = inlineKeyPair ? loadAgent(inlineKeyPair.privKey) : null;
566
+ function loadAgentOnce() {
567
+ if (cachedAgent) return cachedAgent;
568
+ cachedAgent = loadAgentFromFile(keyPath ?? void 0);
569
+ return cachedAgent;
570
+ }
571
+ function fail(reason, toolCallId) {
572
+ return { allow: !failClosed, verdict: "ERROR", reason, toolCallId };
573
+ }
574
+ return {
575
+ async auditToolCall(input) {
576
+ let agent;
577
+ try {
578
+ agent = loadAgentOnce();
579
+ } catch (err) {
580
+ const message = String(err?.message ?? err ?? "");
581
+ logger.warn?.("[atbash] failed to load key pair, blocking for safety", {
582
+ error: message
583
+ });
584
+ return fail("key load failed, blocking for safety");
585
+ }
586
+ const toolName = input.toolName || "unknown";
587
+ const argsRedaction = redactSecrets(stringifyArgs(input.args));
588
+ const ctxRedaction = redactSecrets(input.context ?? toolName);
589
+ const argsJson = argsRedaction.redacted;
590
+ const actionText = truncate(argsJson);
591
+ const contextText = ctxRedaction.redacted;
592
+ const totalRedactions = argsRedaction.found.length + ctxRedaction.found.length;
593
+ if (totalRedactions > 0) {
594
+ const kinds = [.../* @__PURE__ */ new Set([
595
+ ...argsRedaction.found.map((f) => f.kind),
596
+ ...ctxRedaction.found.map((f) => f.kind)
597
+ ])];
598
+ logger.warn?.("[atbash] redacted secrets before judge call", {
599
+ tool: toolName,
600
+ count: totalRedactions,
601
+ kinds
602
+ });
603
+ }
604
+ try {
605
+ logger.info?.("[atbash] judge API called", { tool: toolName });
606
+ const result = await judgeAction(actionText, contextText, agent, {
607
+ endpoint: validated.url,
608
+ verifyPubKey: validated.verifyPubKey ?? void 0,
609
+ toolName,
610
+ toolArgsJson: argsJson,
611
+ chainOpts: {
612
+ nodeUrls: config.nodeUrls,
613
+ blockchainRid: config.blockchainRid
614
+ }
615
+ });
616
+ if (result.verdict === "No verdict") {
617
+ return {
618
+ allow: true,
619
+ verdict: "ALLOW",
620
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
621
+ toolCallId: result.tool_call_id
622
+ };
623
+ }
624
+ const action = result.action_type;
625
+ if (action === "block") {
626
+ return {
627
+ allow: false,
628
+ verdict: "BLOCK",
629
+ reason: result.reason,
630
+ toolCallId: result.tool_call_id
631
+ };
632
+ }
633
+ if (action === "hold_for_user_confirm") {
634
+ return {
635
+ allow: false,
636
+ verdict: "HOLD",
637
+ reason: result.reason || "held for human confirmation",
638
+ toolCallId: result.tool_call_id
639
+ };
640
+ }
641
+ if (action === "allow") {
642
+ const surfacedVerdict = result.verdict === "ALLOW" || result.verdict === "HOLD" || result.verdict === "BLOCK" ? result.verdict : "ALLOW";
643
+ return {
644
+ allow: true,
645
+ verdict: surfacedVerdict,
646
+ reason: result.reason,
647
+ toolCallId: result.tool_call_id
648
+ };
649
+ }
650
+ return fail("unrecognized action_type from judge", result.tool_call_id);
651
+ } catch (err) {
652
+ const message = String(err?.message ?? err ?? "");
653
+ logger.warn?.("[atbash] judge API failed", { reason: message });
654
+ return fail(message);
655
+ }
656
+ }
657
+ };
658
+ }
659
+ function stringifyArgs(args) {
660
+ if (args == null) return "";
661
+ if (typeof args === "string") return args;
662
+ try {
663
+ return JSON.stringify(args);
664
+ } catch {
665
+ return String(args);
666
+ }
667
+ }
668
+ var MAX_ACTION_LEN = 4e3;
669
+ function truncate(text) {
670
+ if (text.length <= MAX_ACTION_LEN) return text;
671
+ return text.slice(0, MAX_ACTION_LEN) + "\u2026";
672
+ }
673
+
674
+ // src/user-config.ts
675
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync } from "fs";
676
+ import { homedir as homedir2 } from "os";
677
+ import { join as join2 } from "path";
678
+ var ENV_MAP = {
679
+ agentKey: "ATBASH_AGENT_KEY",
680
+ orgName: "ATBASH_ORG_NAME",
681
+ judgeEndpoint: "ATBASH_ENDPOINT",
682
+ blockchainRid: "ATBASH_BLOCKCHAIN_RID",
683
+ provider: "ATBASH_PROVIDER",
684
+ providerModel: "ATBASH_PROVIDER_MODEL"
685
+ };
686
+ function getConfigDir() {
687
+ const home = process.env.HOME || homedir2() || "";
688
+ return join2(home, ".config", "atbash");
689
+ }
690
+ function getConfigPath() {
691
+ return join2(getConfigDir(), "config.json");
692
+ }
693
+ function loadUserConfig() {
694
+ try {
695
+ const p = getConfigPath();
696
+ if (!existsSync(p)) return {};
697
+ const raw = readFileSync2(p, "utf-8").trim();
698
+ if (!raw) return {};
699
+ return JSON.parse(raw);
700
+ } catch (err) {
701
+ console.error("Failed to load config file", err);
702
+ return {};
703
+ }
704
+ }
705
+ function saveUserConfig(config) {
706
+ const dir = getConfigDir();
707
+ if (!existsSync(dir)) {
708
+ mkdirSync(dir, { recursive: true });
709
+ }
710
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
711
+ }
712
+ function resolve(key, flagValue) {
713
+ if (flagValue) return flagValue;
714
+ const envName = ENV_MAP[key];
715
+ if (envName) {
716
+ const envVal = process.env[envName];
717
+ if (envVal) return envVal;
718
+ }
719
+ const fileVal = loadUserConfig()[key];
720
+ if (fileVal != null) return String(fileVal);
721
+ return "";
722
+ }
335
723
  export {
336
724
  DEFAULT_BLOCKCHAIN_RID,
337
725
  DEFAULT_CHROMIA_NODE_URLS,
338
726
  DEFAULT_ENDPOINT,
339
727
  checkAgentExists,
728
+ createAtbashClient,
340
729
  derivePublicKey,
341
730
  generateKeyPair,
342
731
  getAgentDetail,
343
732
  getAgentPolicy,
344
733
  getAgentToolCalls,
734
+ getConfigDir,
735
+ getConfigPath,
345
736
  getHeldActionReviews,
346
737
  getJudgmentStatus,
347
738
  getOrgTierInfo,
@@ -354,6 +745,13 @@ export {
354
745
  isValidPrivateKey,
355
746
  judgeAction,
356
747
  loadAgent,
748
+ loadAgentFromFile,
749
+ loadUserConfig,
357
750
  logToolCall,
358
- toPubkeyHex
751
+ resolve,
752
+ resolveKeyPath,
753
+ saveUserConfig,
754
+ toPubkeyHex,
755
+ validateJudgeEndpoint,
756
+ verifyJudgeResponseSignature
359
757
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/sdk",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Atbash SDK — control boundary before the last irreversible step in an agent workflow",
5
5
  "homepage": "https://atbash.ai",
6
6
  "author": "Atbash",