@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 +73 -23
- package/dist/index.cjs +415 -7
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +404 -6
- package/package.json +1 -1
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
|
|
25
|
-
//
|
|
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
|
|
54
|
-
2. **Request verdict** — sends the
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
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
|
|
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
|
|
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
|
|
198
|
-
| `logToolCall(action, context, auth, ...)` | Sign
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
751
|
+
resolve,
|
|
752
|
+
resolveKeyPath,
|
|
753
|
+
saveUserConfig,
|
|
754
|
+
toPubkeyHex,
|
|
755
|
+
validateJudgeEndpoint,
|
|
756
|
+
verifyJudgeResponseSignature
|
|
359
757
|
};
|