@atbash/sdk 0.16.6-dev.0 → 0.17.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.mts +53 -0
- package/dist/browser.mjs +252 -95
- package/dist/index.d.mts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +258 -96
- package/dist/index.mjs +258 -96
- package/package.json +6 -6
package/dist/browser.d.mts
CHANGED
|
@@ -198,6 +198,11 @@ interface JudgeResult {
|
|
|
198
198
|
* missing" — never infer the former from a null verdict alone.
|
|
199
199
|
*/
|
|
200
200
|
status: string;
|
|
201
|
+
/**
|
|
202
|
+
* The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
|
|
203
|
+
* "HOLD", or "" when it neither refused nor is a known refusal shape.
|
|
204
|
+
*/
|
|
205
|
+
refusal?: "" | "BLOCK" | "HOLD";
|
|
201
206
|
}
|
|
202
207
|
interface JudgmentStatus {
|
|
203
208
|
status: JudgmentState;
|
|
@@ -305,6 +310,13 @@ interface AtbashLogger {
|
|
|
305
310
|
/** Options accepted by the `Atbash` constructor. */
|
|
306
311
|
interface AtbashOptions {
|
|
307
312
|
endpoint?: string;
|
|
313
|
+
/**
|
|
314
|
+
* Overall deadline for one `auditToolCall` decision, across every request
|
|
315
|
+
* it makes (org lookup, exists check, judge). Default 60 000 ms. When it
|
|
316
|
+
* passes the decision is an ERROR (denied unless `failClosed` is false).
|
|
317
|
+
* `timeoutMs` still bounds each single request.
|
|
318
|
+
*/
|
|
319
|
+
decisionTimeoutMs?: number;
|
|
308
320
|
timeoutMs?: number;
|
|
309
321
|
/**
|
|
310
322
|
* Full chain override — BRID + nodeUrls in one object. Wins over every
|
|
@@ -350,6 +362,16 @@ interface AtbashOptions {
|
|
|
350
362
|
orgEncryptionPubKey?: string;
|
|
351
363
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
352
364
|
failClosed?: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* When true (the default), an answer that carries no enforcement decision
|
|
367
|
+
* (the audit tier's `{status:"logged"}` with no verdict) is refused:
|
|
368
|
+
* `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
|
|
369
|
+
* `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
|
|
370
|
+
* are false. Set it to `false` only to run deliberately in log-only mode
|
|
371
|
+
* (security review 2026-09-30, H6: an unsigned marker switched enforcement
|
|
372
|
+
* off).
|
|
373
|
+
*/
|
|
374
|
+
requireEnforcement?: boolean;
|
|
353
375
|
/**
|
|
354
376
|
* Verbose diagnostics. Off by default. When on, a failed judge call also
|
|
355
377
|
* logs the response body, which is the difference between "judge API failed"
|
|
@@ -400,6 +422,10 @@ interface FromConfigOptions {
|
|
|
400
422
|
/** Default org name — see {@link AtbashOptions.orgName}. */
|
|
401
423
|
orgName?: string;
|
|
402
424
|
failClosed?: boolean;
|
|
425
|
+
/** See {@link AtbashOptions.requireEnforcement}. Default true. */
|
|
426
|
+
requireEnforcement?: boolean;
|
|
427
|
+
/** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
|
|
428
|
+
decisionTimeoutMs?: number;
|
|
403
429
|
/** See {@link AtbashOptions.debug}. */
|
|
404
430
|
debug?: boolean;
|
|
405
431
|
logger?: AtbashLogger;
|
|
@@ -456,6 +482,10 @@ declare class Atbash {
|
|
|
456
482
|
readonly orgEncryptionPubKey?: string;
|
|
457
483
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
458
484
|
readonly failClosed: boolean;
|
|
485
|
+
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
486
|
+
readonly requireEnforcement: boolean;
|
|
487
|
+
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
488
|
+
readonly decisionTimeoutMs: number;
|
|
459
489
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
460
490
|
private _orgKeyFromChain;
|
|
461
491
|
private readonly debug;
|
|
@@ -559,6 +589,23 @@ declare class Atbash {
|
|
|
559
589
|
* explicitly false.
|
|
560
590
|
*/
|
|
561
591
|
auditToolCall(input: ToolCallInput): Promise<Decision>;
|
|
592
|
+
/**
|
|
593
|
+
* The verify key for one call (R4, governor tier-2 review). A per-call key
|
|
594
|
+
* may only narrow what the client accepts: on a client pinned to a judge
|
|
595
|
+
* key it must be that same key, never a replacement; on an unpinned client
|
|
596
|
+
* it adds the check.
|
|
597
|
+
*/
|
|
598
|
+
private effectiveVerifyKey;
|
|
599
|
+
/**
|
|
600
|
+
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
601
|
+
* signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
|
|
602
|
+
* and the canonical `allow` are each able to deny, and only a result where
|
|
603
|
+
* all three agree may execute. A present `allow` / `decision.allow` that is
|
|
604
|
+
* not exactly `true` is the judge declining to grant permission, so it
|
|
605
|
+
* denies even under `failClosed: false` — that option relaxes *errors*, it
|
|
606
|
+
* never overrides a refusal.
|
|
607
|
+
*/
|
|
608
|
+
private decide;
|
|
562
609
|
/**
|
|
563
610
|
* One exit for every judge failure.
|
|
564
611
|
*
|
|
@@ -568,6 +615,12 @@ declare class Atbash {
|
|
|
568
615
|
* The response body follows only under `debug`, since it can echo the action.
|
|
569
616
|
*/
|
|
570
617
|
private failJudge;
|
|
618
|
+
/**
|
|
619
|
+
* The canonical permission for a response body under this client's
|
|
620
|
+
* settings: {@link canonicalAllow}, minus the audit tier when
|
|
621
|
+
* `requireEnforcement` is on. Never more permissive than `auditToolCall`.
|
|
622
|
+
*/
|
|
623
|
+
private executable;
|
|
571
624
|
private fail;
|
|
572
625
|
/**
|
|
573
626
|
* Return the current status of a previously submitted judgment.
|
package/dist/browser.mjs
CHANGED
|
@@ -44590,7 +44590,7 @@ init_define_ATBASH_CHROMIA_NODE_URLS();
|
|
|
44590
44590
|
init_define_ATBASH_PRIVATE_NODE_URLS();
|
|
44591
44591
|
function normalizeVerdict(raw2) {
|
|
44592
44592
|
if (raw2 === null || raw2 === void 0) return "No verdict";
|
|
44593
|
-
const v = String(raw2).toUpperCase();
|
|
44593
|
+
const v = String(raw2).trim().toUpperCase();
|
|
44594
44594
|
if (v === "ALLOW" || v === "GREEN") return "ALLOW";
|
|
44595
44595
|
if (v === "HOLD" || v === "YELLOW") return "HOLD";
|
|
44596
44596
|
if (v === "BLOCK" || v === "RED") return "BLOCK";
|
|
@@ -44627,17 +44627,58 @@ function allowSignalsPermit(data) {
|
|
|
44627
44627
|
}
|
|
44628
44628
|
return signals.length === 0 || signals.every((value) => value === true);
|
|
44629
44629
|
}
|
|
44630
|
+
function explicitRefusal(data) {
|
|
44631
|
+
let hold = false;
|
|
44632
|
+
for (const source of decisionSources(data)) {
|
|
44633
|
+
const verdict = verdictOf(source);
|
|
44634
|
+
if (verdict === "BLOCK" || verdict === "RED") return "BLOCK";
|
|
44635
|
+
if (verdict === "HOLD" || verdict === "YELLOW") hold = true;
|
|
44636
|
+
const actionType = actionTypeOf(source);
|
|
44637
|
+
if (actionType === "block") return "BLOCK";
|
|
44638
|
+
if (actionType === "hold_for_user_confirm") hold = true;
|
|
44639
|
+
}
|
|
44640
|
+
if (!allowSignalsPermit(data)) return "BLOCK";
|
|
44641
|
+
return hold ? "HOLD" : "";
|
|
44642
|
+
}
|
|
44643
|
+
function decisionSources(data) {
|
|
44644
|
+
return [data, isRecord(data.decision) ? data.decision : {}];
|
|
44645
|
+
}
|
|
44646
|
+
function verdictOf(source) {
|
|
44647
|
+
return typeof source.verdict === "string" ? source.verdict.trim().toUpperCase() : "";
|
|
44648
|
+
}
|
|
44649
|
+
function actionTypeRank(actionType) {
|
|
44650
|
+
if (actionType === "block") return 4;
|
|
44651
|
+
if (actionType === "hold_for_user_confirm") return 3;
|
|
44652
|
+
if (actionType === "") return 1;
|
|
44653
|
+
if (actionType === "allow") return 0;
|
|
44654
|
+
return 2;
|
|
44655
|
+
}
|
|
44656
|
+
function actionTypeOf(source) {
|
|
44657
|
+
let winner = "";
|
|
44658
|
+
for (const key3 of ["action_type", "actionType"]) {
|
|
44659
|
+
const raw2 = source[key3];
|
|
44660
|
+
if (raw2 === void 0 || raw2 === null) continue;
|
|
44661
|
+
const actionType = normalizeActionType(raw2);
|
|
44662
|
+
if (actionTypeRank(actionType) > actionTypeRank(winner)) winner = actionType;
|
|
44663
|
+
}
|
|
44664
|
+
return winner;
|
|
44665
|
+
}
|
|
44630
44666
|
function wireActionType(data) {
|
|
44631
|
-
|
|
44632
|
-
|
|
44633
|
-
|
|
44667
|
+
for (const source of decisionSources(data)) {
|
|
44668
|
+
const actionType = actionTypeOf(source);
|
|
44669
|
+
if (actionType !== "") return actionType;
|
|
44670
|
+
}
|
|
44671
|
+
return "";
|
|
44634
44672
|
}
|
|
44635
44673
|
function isAuditTier(data) {
|
|
44636
44674
|
const absentVerdict = data.verdict === null || data.verdict === void 0;
|
|
44637
44675
|
return absentVerdict && data.status === "logged";
|
|
44638
44676
|
}
|
|
44677
|
+
function isAuditTierBody(data) {
|
|
44678
|
+
return isAuditTier(data);
|
|
44679
|
+
}
|
|
44639
44680
|
function canonicalAllow(data) {
|
|
44640
|
-
if (
|
|
44681
|
+
if (explicitRefusal(data) !== "") return false;
|
|
44641
44682
|
const actionType = wireActionType(data);
|
|
44642
44683
|
if (actionType && actionType !== "allow") return false;
|
|
44643
44684
|
if (isAuditTier(data)) return true;
|
|
@@ -44720,6 +44761,10 @@ var Atbash = class _Atbash {
|
|
|
44720
44761
|
orgEncryptionPubKey;
|
|
44721
44762
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
44722
44763
|
failClosed;
|
|
44764
|
+
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
44765
|
+
requireEnforcement;
|
|
44766
|
+
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
44767
|
+
decisionTimeoutMs;
|
|
44723
44768
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
44724
44769
|
_orgKeyFromChain = null;
|
|
44725
44770
|
debug;
|
|
@@ -44793,6 +44838,8 @@ var Atbash = class _Atbash {
|
|
|
44793
44838
|
this.verifyPubKey = validated.verifyPubKey ?? void 0;
|
|
44794
44839
|
this.orgEncryptionPubKey = options.orgEncryptionPubKey;
|
|
44795
44840
|
this.failClosed = options.failClosed !== false;
|
|
44841
|
+
this.requireEnforcement = options.requireEnforcement !== false;
|
|
44842
|
+
this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
|
|
44796
44843
|
this.debug = options.debug === true;
|
|
44797
44844
|
this.logger = options.logger ?? {};
|
|
44798
44845
|
this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
|
|
@@ -44883,6 +44930,8 @@ var Atbash = class _Atbash {
|
|
|
44883
44930
|
orgName: options.orgName,
|
|
44884
44931
|
verifyPubKey: validated.verifyPubKey ?? void 0,
|
|
44885
44932
|
failClosed: options.failClosed,
|
|
44933
|
+
requireEnforcement: options.requireEnforcement,
|
|
44934
|
+
decisionTimeoutMs: options.decisionTimeoutMs,
|
|
44886
44935
|
// ATBASH_DEBUG lets an operator turn diagnostics on without editing a
|
|
44887
44936
|
// host's plugin config, which is usually the harder half.
|
|
44888
44937
|
debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
|
|
@@ -45081,34 +45130,16 @@ var Atbash = class _Atbash {
|
|
|
45081
45130
|
}
|
|
45082
45131
|
if (!resp.ok) throw await this.httpError(resp);
|
|
45083
45132
|
const bodyBytes = Buffer.from(await resp.arrayBuffer());
|
|
45084
|
-
const verifyPubKey = options.verifyPubKey
|
|
45133
|
+
const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
|
|
45085
45134
|
if (verifyPubKey !== void 0) {
|
|
45086
|
-
|
|
45087
|
-
if (!sig) {
|
|
45088
|
-
throw new SignatureVerificationError(
|
|
45089
|
-
"missing X-Atbash-Signature header"
|
|
45090
|
-
);
|
|
45091
|
-
}
|
|
45092
|
-
let ok;
|
|
45093
|
-
try {
|
|
45094
|
-
ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
|
|
45095
|
-
} catch (err) {
|
|
45096
|
-
throw new SignatureVerificationError(
|
|
45097
|
-
`signature verification threw: ${errorMessage(err)}`
|
|
45098
|
-
);
|
|
45099
|
-
}
|
|
45100
|
-
if (!ok) {
|
|
45101
|
-
throw new SignatureVerificationError(
|
|
45102
|
-
"signature does not verify against configured verifyPubKey"
|
|
45103
|
-
);
|
|
45104
|
-
}
|
|
45135
|
+
verifyResponseSignature(bodyBytes, resp, verifyPubKey);
|
|
45105
45136
|
}
|
|
45106
45137
|
const data = parseJson(bodyBytes);
|
|
45107
45138
|
const rawScore = data.score;
|
|
45108
45139
|
const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
|
|
45109
45140
|
return {
|
|
45110
45141
|
verdict: normalizeVerdict(data.verdict),
|
|
45111
|
-
allow:
|
|
45142
|
+
allow: this.executable(data),
|
|
45112
45143
|
actionType: normalizeActionType(data.action_type),
|
|
45113
45144
|
reason: String(data.reason ?? ""),
|
|
45114
45145
|
confidence: Number(data.confidence ?? 0),
|
|
@@ -45119,7 +45150,8 @@ var Atbash = class _Atbash {
|
|
|
45119
45150
|
enforced: Boolean(data.enforced),
|
|
45120
45151
|
enforcementMode: String(data.enforcement_mode ?? ""),
|
|
45121
45152
|
status: String(data.status ?? ""),
|
|
45122
|
-
score
|
|
45153
|
+
score,
|
|
45154
|
+
refusal: explicitRefusal(data)
|
|
45123
45155
|
};
|
|
45124
45156
|
}
|
|
45125
45157
|
/* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
|
|
@@ -45152,77 +45184,125 @@ var Atbash = class _Atbash {
|
|
|
45152
45184
|
}
|
|
45153
45185
|
try {
|
|
45154
45186
|
this.logger.info?.("[atbash] judge API called", { tool: toolName });
|
|
45155
|
-
const result = await
|
|
45156
|
-
|
|
45157
|
-
|
|
45158
|
-
|
|
45159
|
-
|
|
45160
|
-
|
|
45161
|
-
|
|
45162
|
-
|
|
45163
|
-
|
|
45164
|
-
|
|
45165
|
-
|
|
45166
|
-
|
|
45167
|
-
|
|
45168
|
-
|
|
45169
|
-
}
|
|
45170
|
-
return {
|
|
45171
|
-
allow: true,
|
|
45172
|
-
verdict: "ALLOW",
|
|
45173
|
-
reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
|
|
45174
|
-
toolCallId: result.toolCallId
|
|
45175
|
-
};
|
|
45176
|
-
}
|
|
45177
|
-
const action = result.actionType;
|
|
45178
|
-
if (action === "block") {
|
|
45187
|
+
const result = await withDeadline(
|
|
45188
|
+
this.judgeAction(actionText, contextText, {
|
|
45189
|
+
toolName,
|
|
45190
|
+
toolArgsJson: argsJson,
|
|
45191
|
+
orgName: this.orgName,
|
|
45192
|
+
// Passed through unredacted — caller vouches for the contents.
|
|
45193
|
+
resolved: input.resolved
|
|
45194
|
+
}),
|
|
45195
|
+
this.decisionTimeoutMs
|
|
45196
|
+
);
|
|
45197
|
+
return this.decide(result);
|
|
45198
|
+
} catch (err) {
|
|
45199
|
+
if (err instanceof SignatureVerificationError) {
|
|
45200
|
+
this.failJudge(errorMessage(err), err);
|
|
45179
45201
|
return {
|
|
45180
45202
|
allow: false,
|
|
45181
|
-
verdict: "
|
|
45182
|
-
reason:
|
|
45183
|
-
toolCallId: result.toolCallId
|
|
45203
|
+
verdict: "ERROR",
|
|
45204
|
+
reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
|
|
45184
45205
|
};
|
|
45185
45206
|
}
|
|
45186
|
-
|
|
45187
|
-
|
|
45188
|
-
|
|
45189
|
-
|
|
45190
|
-
|
|
45191
|
-
|
|
45192
|
-
|
|
45207
|
+
return this.failJudge(errorMessage(err), err);
|
|
45208
|
+
}
|
|
45209
|
+
}
|
|
45210
|
+
/**
|
|
45211
|
+
* The verify key for one call (R4, governor tier-2 review). A per-call key
|
|
45212
|
+
* may only narrow what the client accepts: on a client pinned to a judge
|
|
45213
|
+
* key it must be that same key, never a replacement; on an unpinned client
|
|
45214
|
+
* it adds the check.
|
|
45215
|
+
*/
|
|
45216
|
+
effectiveVerifyKey(perCall) {
|
|
45217
|
+
if (perCall === void 0) return this.verifyPubKey;
|
|
45218
|
+
const key3 = perCall.trim().toLowerCase();
|
|
45219
|
+
if (this.verifyPubKey !== void 0 && key3 !== this.verifyPubKey) {
|
|
45220
|
+
throw new SignatureVerificationError(
|
|
45221
|
+
"a per-call verifyPubKey cannot replace the judge key this client is pinned to"
|
|
45222
|
+
);
|
|
45223
|
+
}
|
|
45224
|
+
return key3;
|
|
45225
|
+
}
|
|
45226
|
+
/**
|
|
45227
|
+
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
45228
|
+
* signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
|
|
45229
|
+
* and the canonical `allow` are each able to deny, and only a result where
|
|
45230
|
+
* all three agree may execute. A present `allow` / `decision.allow` that is
|
|
45231
|
+
* not exactly `true` is the judge declining to grant permission, so it
|
|
45232
|
+
* denies even under `failClosed: false` — that option relaxes *errors*, it
|
|
45233
|
+
* never overrides a refusal.
|
|
45234
|
+
*/
|
|
45235
|
+
decide(result) {
|
|
45236
|
+
const toolCallId = result.toolCallId;
|
|
45237
|
+
const deny = (verdict, reason) => ({
|
|
45238
|
+
allow: false,
|
|
45239
|
+
verdict,
|
|
45240
|
+
reason,
|
|
45241
|
+
toolCallId
|
|
45242
|
+
});
|
|
45243
|
+
const notGranted = "the judge response did not grant permission";
|
|
45244
|
+
const held = deny("HOLD", result.reason || "held for human confirmation");
|
|
45245
|
+
const blocked = deny("BLOCK", result.reason || notGranted);
|
|
45246
|
+
if (result.refusal === "BLOCK") return blocked;
|
|
45247
|
+
if (result.verdict === "No verdict") {
|
|
45248
|
+
if (result.actionType === "block") {
|
|
45249
|
+
return blocked;
|
|
45250
|
+
}
|
|
45251
|
+
if (result.actionType === "hold_for_user_confirm") {
|
|
45252
|
+
return held;
|
|
45253
|
+
}
|
|
45254
|
+
if (result.refusal === "HOLD") return held;
|
|
45255
|
+
if (result.status !== "logged") {
|
|
45256
|
+
return this.failJudge(
|
|
45257
|
+
`judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
|
|
45258
|
+
void 0,
|
|
45259
|
+
toolCallId
|
|
45260
|
+
);
|
|
45193
45261
|
}
|
|
45194
|
-
if (
|
|
45195
|
-
|
|
45196
|
-
|
|
45197
|
-
|
|
45198
|
-
|
|
45199
|
-
reason: result.reason,
|
|
45200
|
-
toolCallId: result.toolCallId
|
|
45201
|
-
};
|
|
45202
|
-
}
|
|
45203
|
-
if (result.verdict === "BLOCK") {
|
|
45204
|
-
return {
|
|
45205
|
-
allow: false,
|
|
45206
|
-
verdict: "BLOCK",
|
|
45207
|
-
reason: result.reason,
|
|
45208
|
-
toolCallId: result.toolCallId
|
|
45209
|
-
};
|
|
45210
|
-
}
|
|
45262
|
+
if (this.requireEnforcement) {
|
|
45263
|
+
this.logger.warn?.(
|
|
45264
|
+
"[atbash] judge did not enforce this action (audit tier) and requireEnforcement is on: denied",
|
|
45265
|
+
{ toolCallId }
|
|
45266
|
+
);
|
|
45211
45267
|
return {
|
|
45212
|
-
allow:
|
|
45213
|
-
verdict: "
|
|
45214
|
-
reason:
|
|
45215
|
-
toolCallId
|
|
45268
|
+
allow: false,
|
|
45269
|
+
verdict: "ERROR",
|
|
45270
|
+
reason: "Atbash logged this action but did not check it (audit-tier plan), and this client requires a checked decision. Upgrade the plan, or set requireEnforcement: false for log-only mode.",
|
|
45271
|
+
toolCallId
|
|
45216
45272
|
};
|
|
45217
45273
|
}
|
|
45218
|
-
return
|
|
45219
|
-
|
|
45220
|
-
|
|
45221
|
-
|
|
45222
|
-
|
|
45223
|
-
|
|
45224
|
-
|
|
45274
|
+
if (result.allow !== true) return deny("BLOCK", notGranted);
|
|
45275
|
+
return {
|
|
45276
|
+
allow: true,
|
|
45277
|
+
verdict: "ALLOW",
|
|
45278
|
+
reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
|
|
45279
|
+
toolCallId
|
|
45280
|
+
};
|
|
45281
|
+
}
|
|
45282
|
+
const action = result.actionType;
|
|
45283
|
+
if (action === "block") {
|
|
45284
|
+
return deny("BLOCK", result.reason || "Blocked by your organisation's Atbash policy.");
|
|
45285
|
+
}
|
|
45286
|
+
if (action === "hold_for_user_confirm") {
|
|
45287
|
+
return held;
|
|
45225
45288
|
}
|
|
45289
|
+
if (result.refusal === "HOLD") return held;
|
|
45290
|
+
if (action === "allow") {
|
|
45291
|
+
if (result.verdict === "HOLD") return held;
|
|
45292
|
+
if (result.verdict === "BLOCK") return blocked;
|
|
45293
|
+
if (result.allow !== true) return deny("BLOCK", notGranted);
|
|
45294
|
+
return {
|
|
45295
|
+
allow: true,
|
|
45296
|
+
verdict: "ALLOW",
|
|
45297
|
+
reason: result.reason,
|
|
45298
|
+
toolCallId
|
|
45299
|
+
};
|
|
45300
|
+
}
|
|
45301
|
+
return this.failJudge(
|
|
45302
|
+
"unrecognized action_type from judge",
|
|
45303
|
+
void 0,
|
|
45304
|
+
toolCallId
|
|
45305
|
+
);
|
|
45226
45306
|
}
|
|
45227
45307
|
/**
|
|
45228
45308
|
* One exit for every judge failure.
|
|
@@ -45234,18 +45314,30 @@ var Atbash = class _Atbash {
|
|
|
45234
45314
|
*/
|
|
45235
45315
|
failJudge(reason, cause, toolCallId) {
|
|
45236
45316
|
const api2 = cause instanceof AtbashAPIError ? cause : null;
|
|
45317
|
+
const userReason = userFacingReason(reason, api2);
|
|
45237
45318
|
const status = api2 ? ` status=${api2.status || "no-response"}` : "";
|
|
45319
|
+
const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
|
|
45238
45320
|
const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
|
|
45239
45321
|
this.logger.warn?.(
|
|
45240
|
-
`[atbash] judge API failed \u2014${status} reason=${truncate(
|
|
45322
|
+
`[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
|
|
45241
45323
|
{
|
|
45242
|
-
reason,
|
|
45243
|
-
...api2 ? { status: api2.status
|
|
45324
|
+
reason: userReason,
|
|
45325
|
+
...api2 ? { status: api2.status } : {},
|
|
45326
|
+
...this.debug && api2 ? { body: api2.body, detail: reason } : {},
|
|
45244
45327
|
endpoint: this.endpoint,
|
|
45245
45328
|
...toolCallId ? { toolCallId } : {}
|
|
45246
45329
|
}
|
|
45247
45330
|
);
|
|
45248
|
-
return this.fail(
|
|
45331
|
+
return this.fail(userReason, toolCallId);
|
|
45332
|
+
}
|
|
45333
|
+
/**
|
|
45334
|
+
* The canonical permission for a response body under this client's
|
|
45335
|
+
* settings: {@link canonicalAllow}, minus the audit tier when
|
|
45336
|
+
* `requireEnforcement` is on. Never more permissive than `auditToolCall`.
|
|
45337
|
+
*/
|
|
45338
|
+
executable(data) {
|
|
45339
|
+
if (!canonicalAllow(data)) return false;
|
|
45340
|
+
return !(this.requireEnforcement && isAuditTierBody(data));
|
|
45249
45341
|
}
|
|
45250
45342
|
fail(reason, toolCallId) {
|
|
45251
45343
|
return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
|
|
@@ -45271,11 +45363,16 @@ var Atbash = class _Atbash {
|
|
|
45271
45363
|
this.authHeaders(brid)
|
|
45272
45364
|
);
|
|
45273
45365
|
await this.raiseIfError(resp);
|
|
45274
|
-
const
|
|
45366
|
+
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
45367
|
+
if (this.verifyPubKey !== void 0) {
|
|
45368
|
+
verifyResponseSignature(bytes, resp, this.verifyPubKey);
|
|
45369
|
+
}
|
|
45370
|
+
const parsed = tryParseJson(bytes.toString("utf-8"));
|
|
45371
|
+
const data = isRecord2(parsed) ? parsed : {};
|
|
45275
45372
|
return {
|
|
45276
45373
|
status: normalizeStatus(data.status),
|
|
45277
45374
|
verdict: normalizeVerdict(data.verdict),
|
|
45278
|
-
allow:
|
|
45375
|
+
allow: this.executable(data),
|
|
45279
45376
|
reason: String(data.reason ?? ""),
|
|
45280
45377
|
judgmentId: String(data.judgmentId ?? judgmentId),
|
|
45281
45378
|
onChain: optBool(data.onChain),
|
|
@@ -45750,6 +45847,66 @@ var Atbash = class _Atbash {
|
|
|
45750
45847
|
return native.containsEvasionCharacters(text);
|
|
45751
45848
|
}
|
|
45752
45849
|
};
|
|
45850
|
+
function userFacingReason(reason, api2) {
|
|
45851
|
+
if (api2 && api2.status > 0) {
|
|
45852
|
+
const where = ` (HTTP ${api2.status}${api2.path ? ` from ${api2.path}` : ""})`;
|
|
45853
|
+
const b = api2.body.toLowerCase();
|
|
45854
|
+
if (b.includes("agent not registered")) {
|
|
45855
|
+
return `Agent not registered with Atbash: onboard it in the Atbash dashboard${where}.`;
|
|
45856
|
+
}
|
|
45857
|
+
if (b.includes("jailed")) {
|
|
45858
|
+
return `This agent is jailed; unjail it in the Atbash dashboard${where}.`;
|
|
45859
|
+
}
|
|
45860
|
+
if (b.includes("agent has no policy") || b.includes("no policy configured")) {
|
|
45861
|
+
return `This agent has no policy; attach one in the Atbash dashboard${where}.`;
|
|
45862
|
+
}
|
|
45863
|
+
if (b.includes("audit tier") || b.includes("verdict disabled") || b.includes("verdict not supported")) {
|
|
45864
|
+
return `The organisation's plan does not include checked decisions${where}.`;
|
|
45865
|
+
}
|
|
45866
|
+
return `The Atbash judge answered with an error${where}, so the action was not approved.`;
|
|
45867
|
+
}
|
|
45868
|
+
const text = api2 ? api2.body || reason : reason;
|
|
45869
|
+
return truncate(text.replace(/https?:\/\/[^\s)]+/g, "the judge"), 300);
|
|
45870
|
+
}
|
|
45871
|
+
var DEFAULT_DECISION_TIMEOUT_MS = 6e4;
|
|
45872
|
+
async function withDeadline(p, ms) {
|
|
45873
|
+
let timer;
|
|
45874
|
+
const deadline = new Promise((_, reject) => {
|
|
45875
|
+
timer = setTimeout(
|
|
45876
|
+
() => reject(
|
|
45877
|
+
new Error(
|
|
45878
|
+
`The judge did not give a decision within ${Math.round(ms / 1e3)} s, so the action was not approved. Check the network, or raise decisionTimeoutMs.`
|
|
45879
|
+
)
|
|
45880
|
+
),
|
|
45881
|
+
ms
|
|
45882
|
+
);
|
|
45883
|
+
});
|
|
45884
|
+
p.catch(() => void 0);
|
|
45885
|
+
try {
|
|
45886
|
+
return await Promise.race([p, deadline]);
|
|
45887
|
+
} finally {
|
|
45888
|
+
clearTimeout(timer);
|
|
45889
|
+
}
|
|
45890
|
+
}
|
|
45891
|
+
function verifyResponseSignature(bodyBytes, resp, verifyPubKey) {
|
|
45892
|
+
const sig = resp.headers.get("X-Atbash-Signature");
|
|
45893
|
+
if (!sig) {
|
|
45894
|
+
throw new SignatureVerificationError("missing X-Atbash-Signature header");
|
|
45895
|
+
}
|
|
45896
|
+
let ok;
|
|
45897
|
+
try {
|
|
45898
|
+
ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
|
|
45899
|
+
} catch (err) {
|
|
45900
|
+
throw new SignatureVerificationError(
|
|
45901
|
+
`signature verification threw: ${errorMessage(err)}`
|
|
45902
|
+
);
|
|
45903
|
+
}
|
|
45904
|
+
if (!ok) {
|
|
45905
|
+
throw new SignatureVerificationError(
|
|
45906
|
+
"signature does not verify against configured verifyPubKey"
|
|
45907
|
+
);
|
|
45908
|
+
}
|
|
45909
|
+
}
|
|
45753
45910
|
function baseToolCall(raw2) {
|
|
45754
45911
|
return {
|
|
45755
45912
|
toolCallId: String(raw2.tool_call_id ?? ""),
|
package/dist/index.d.mts
CHANGED
|
@@ -198,6 +198,11 @@ interface JudgeResult {
|
|
|
198
198
|
* missing" — never infer the former from a null verdict alone.
|
|
199
199
|
*/
|
|
200
200
|
status: string;
|
|
201
|
+
/**
|
|
202
|
+
* The judge's explicit refusal in the raw body (`explicitRefusal`): "BLOCK",
|
|
203
|
+
* "HOLD", or "" when it neither refused nor is a known refusal shape.
|
|
204
|
+
*/
|
|
205
|
+
refusal?: "" | "BLOCK" | "HOLD";
|
|
201
206
|
}
|
|
202
207
|
interface JudgmentStatus {
|
|
203
208
|
status: JudgmentState;
|
|
@@ -305,6 +310,13 @@ interface AtbashLogger {
|
|
|
305
310
|
/** Options accepted by the `Atbash` constructor. */
|
|
306
311
|
interface AtbashOptions {
|
|
307
312
|
endpoint?: string;
|
|
313
|
+
/**
|
|
314
|
+
* Overall deadline for one `auditToolCall` decision, across every request
|
|
315
|
+
* it makes (org lookup, exists check, judge). Default 60 000 ms. When it
|
|
316
|
+
* passes the decision is an ERROR (denied unless `failClosed` is false).
|
|
317
|
+
* `timeoutMs` still bounds each single request.
|
|
318
|
+
*/
|
|
319
|
+
decisionTimeoutMs?: number;
|
|
308
320
|
timeoutMs?: number;
|
|
309
321
|
/**
|
|
310
322
|
* Full chain override — BRID + nodeUrls in one object. Wins over every
|
|
@@ -350,6 +362,16 @@ interface AtbashOptions {
|
|
|
350
362
|
orgEncryptionPubKey?: string;
|
|
351
363
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
352
364
|
failClosed?: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* When true (the default), an answer that carries no enforcement decision
|
|
367
|
+
* (the audit tier's `{status:"logged"}` with no verdict) is refused:
|
|
368
|
+
* `auditToolCall` returns `{allow:false, verdict:"ERROR"}` whatever
|
|
369
|
+
* `failClosed` says, and `judgeAction().allow` / `getJudgmentStatus().allow`
|
|
370
|
+
* are false. Set it to `false` only to run deliberately in log-only mode
|
|
371
|
+
* (security review 2026-09-30, H6: an unsigned marker switched enforcement
|
|
372
|
+
* off).
|
|
373
|
+
*/
|
|
374
|
+
requireEnforcement?: boolean;
|
|
353
375
|
/**
|
|
354
376
|
* Verbose diagnostics. Off by default. When on, a failed judge call also
|
|
355
377
|
* logs the response body, which is the difference between "judge API failed"
|
|
@@ -400,6 +422,10 @@ interface FromConfigOptions {
|
|
|
400
422
|
/** Default org name — see {@link AtbashOptions.orgName}. */
|
|
401
423
|
orgName?: string;
|
|
402
424
|
failClosed?: boolean;
|
|
425
|
+
/** See {@link AtbashOptions.requireEnforcement}. Default true. */
|
|
426
|
+
requireEnforcement?: boolean;
|
|
427
|
+
/** See {@link AtbashOptions.decisionTimeoutMs}. Default 60 000 ms. */
|
|
428
|
+
decisionTimeoutMs?: number;
|
|
403
429
|
/** See {@link AtbashOptions.debug}. */
|
|
404
430
|
debug?: boolean;
|
|
405
431
|
logger?: AtbashLogger;
|
|
@@ -456,6 +482,10 @@ declare class Atbash {
|
|
|
456
482
|
readonly orgEncryptionPubKey?: string;
|
|
457
483
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
458
484
|
readonly failClosed: boolean;
|
|
485
|
+
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
486
|
+
readonly requireEnforcement: boolean;
|
|
487
|
+
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
488
|
+
readonly decisionTimeoutMs: number;
|
|
459
489
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
460
490
|
private _orgKeyFromChain;
|
|
461
491
|
private readonly debug;
|
|
@@ -559,6 +589,23 @@ declare class Atbash {
|
|
|
559
589
|
* explicitly false.
|
|
560
590
|
*/
|
|
561
591
|
auditToolCall(input: ToolCallInput): Promise<Decision>;
|
|
592
|
+
/**
|
|
593
|
+
* The verify key for one call (R4, governor tier-2 review). A per-call key
|
|
594
|
+
* may only narrow what the client accepts: on a client pinned to a judge
|
|
595
|
+
* key it must be that same key, never a replacement; on an unpinned client
|
|
596
|
+
* it adds the check.
|
|
597
|
+
*/
|
|
598
|
+
private effectiveVerifyKey;
|
|
599
|
+
/**
|
|
600
|
+
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
601
|
+
* signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
|
|
602
|
+
* and the canonical `allow` are each able to deny, and only a result where
|
|
603
|
+
* all three agree may execute. A present `allow` / `decision.allow` that is
|
|
604
|
+
* not exactly `true` is the judge declining to grant permission, so it
|
|
605
|
+
* denies even under `failClosed: false` — that option relaxes *errors*, it
|
|
606
|
+
* never overrides a refusal.
|
|
607
|
+
*/
|
|
608
|
+
private decide;
|
|
562
609
|
/**
|
|
563
610
|
* One exit for every judge failure.
|
|
564
611
|
*
|
|
@@ -568,6 +615,12 @@ declare class Atbash {
|
|
|
568
615
|
* The response body follows only under `debug`, since it can echo the action.
|
|
569
616
|
*/
|
|
570
617
|
private failJudge;
|
|
618
|
+
/**
|
|
619
|
+
* The canonical permission for a response body under this client's
|
|
620
|
+
* settings: {@link canonicalAllow}, minus the audit tier when
|
|
621
|
+
* `requireEnforcement` is on. Never more permissive than `auditToolCall`.
|
|
622
|
+
*/
|
|
623
|
+
private executable;
|
|
571
624
|
private fail;
|
|
572
625
|
/**
|
|
573
626
|
* Return the current status of a previously submitted judgment.
|