@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/index.d.ts
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/index.js
CHANGED
|
@@ -3301,7 +3301,7 @@ function loadAgentFromFile(keyPath) {
|
|
|
3301
3301
|
// src-ts/normalize.ts
|
|
3302
3302
|
function normalizeVerdict(raw2) {
|
|
3303
3303
|
if (raw2 === null || raw2 === void 0) return "No verdict";
|
|
3304
|
-
const v = String(raw2).toUpperCase();
|
|
3304
|
+
const v = String(raw2).trim().toUpperCase();
|
|
3305
3305
|
if (v === "ALLOW" || v === "GREEN") return "ALLOW";
|
|
3306
3306
|
if (v === "HOLD" || v === "YELLOW") return "HOLD";
|
|
3307
3307
|
if (v === "BLOCK" || v === "RED") return "BLOCK";
|
|
@@ -3338,17 +3338,58 @@ function allowSignalsPermit(data) {
|
|
|
3338
3338
|
}
|
|
3339
3339
|
return signals.length === 0 || signals.every((value) => value === true);
|
|
3340
3340
|
}
|
|
3341
|
+
function explicitRefusal(data) {
|
|
3342
|
+
let hold = false;
|
|
3343
|
+
for (const source of decisionSources(data)) {
|
|
3344
|
+
const verdict = verdictOf(source);
|
|
3345
|
+
if (verdict === "BLOCK" || verdict === "RED") return "BLOCK";
|
|
3346
|
+
if (verdict === "HOLD" || verdict === "YELLOW") hold = true;
|
|
3347
|
+
const actionType = actionTypeOf(source);
|
|
3348
|
+
if (actionType === "block") return "BLOCK";
|
|
3349
|
+
if (actionType === "hold_for_user_confirm") hold = true;
|
|
3350
|
+
}
|
|
3351
|
+
if (!allowSignalsPermit(data)) return "BLOCK";
|
|
3352
|
+
return hold ? "HOLD" : "";
|
|
3353
|
+
}
|
|
3354
|
+
function decisionSources(data) {
|
|
3355
|
+
return [data, isRecord(data.decision) ? data.decision : {}];
|
|
3356
|
+
}
|
|
3357
|
+
function verdictOf(source) {
|
|
3358
|
+
return typeof source.verdict === "string" ? source.verdict.trim().toUpperCase() : "";
|
|
3359
|
+
}
|
|
3360
|
+
function actionTypeRank(actionType) {
|
|
3361
|
+
if (actionType === "block") return 4;
|
|
3362
|
+
if (actionType === "hold_for_user_confirm") return 3;
|
|
3363
|
+
if (actionType === "") return 1;
|
|
3364
|
+
if (actionType === "allow") return 0;
|
|
3365
|
+
return 2;
|
|
3366
|
+
}
|
|
3367
|
+
function actionTypeOf(source) {
|
|
3368
|
+
let winner = "";
|
|
3369
|
+
for (const key3 of ["action_type", "actionType"]) {
|
|
3370
|
+
const raw2 = source[key3];
|
|
3371
|
+
if (raw2 === void 0 || raw2 === null) continue;
|
|
3372
|
+
const actionType = normalizeActionType(raw2);
|
|
3373
|
+
if (actionTypeRank(actionType) > actionTypeRank(winner)) winner = actionType;
|
|
3374
|
+
}
|
|
3375
|
+
return winner;
|
|
3376
|
+
}
|
|
3341
3377
|
function wireActionType(data) {
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3378
|
+
for (const source of decisionSources(data)) {
|
|
3379
|
+
const actionType = actionTypeOf(source);
|
|
3380
|
+
if (actionType !== "") return actionType;
|
|
3381
|
+
}
|
|
3382
|
+
return "";
|
|
3345
3383
|
}
|
|
3346
3384
|
function isAuditTier(data) {
|
|
3347
3385
|
const absentVerdict = data.verdict === null || data.verdict === void 0;
|
|
3348
3386
|
return absentVerdict && data.status === "logged";
|
|
3349
3387
|
}
|
|
3388
|
+
function isAuditTierBody(data) {
|
|
3389
|
+
return isAuditTier(data);
|
|
3390
|
+
}
|
|
3350
3391
|
function canonicalAllow(data) {
|
|
3351
|
-
if (
|
|
3392
|
+
if (explicitRefusal(data) !== "") return false;
|
|
3352
3393
|
const actionType = wireActionType(data);
|
|
3353
3394
|
if (actionType && actionType !== "allow") return false;
|
|
3354
3395
|
if (isAuditTier(data)) return true;
|
|
@@ -3578,6 +3619,10 @@ var Atbash = class _Atbash {
|
|
|
3578
3619
|
orgEncryptionPubKey;
|
|
3579
3620
|
/** When true (default), `auditToolCall` denies on any error. */
|
|
3580
3621
|
failClosed;
|
|
3622
|
+
/** When true (default), an unenforced (audit-tier) answer is refused. */
|
|
3623
|
+
requireEnforcement;
|
|
3624
|
+
/** Overall deadline for one auditToolCall decision, in ms. */
|
|
3625
|
+
decisionTimeoutMs;
|
|
3581
3626
|
/** Org key learned from the last agent-exists check, for this agent only. */
|
|
3582
3627
|
_orgKeyFromChain = null;
|
|
3583
3628
|
debug;
|
|
@@ -3651,6 +3696,8 @@ var Atbash = class _Atbash {
|
|
|
3651
3696
|
this.verifyPubKey = validated.verifyPubKey ?? void 0;
|
|
3652
3697
|
this.orgEncryptionPubKey = options.orgEncryptionPubKey;
|
|
3653
3698
|
this.failClosed = options.failClosed !== false;
|
|
3699
|
+
this.requireEnforcement = options.requireEnforcement !== false;
|
|
3700
|
+
this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
|
|
3654
3701
|
this.debug = options.debug === true;
|
|
3655
3702
|
this.logger = options.logger ?? {};
|
|
3656
3703
|
this.http = new HttpClient(this.endpoint, options.timeoutMs ?? 12e4);
|
|
@@ -3741,6 +3788,8 @@ var Atbash = class _Atbash {
|
|
|
3741
3788
|
orgName: options.orgName,
|
|
3742
3789
|
verifyPubKey: validated.verifyPubKey ?? void 0,
|
|
3743
3790
|
failClosed: options.failClosed,
|
|
3791
|
+
requireEnforcement: options.requireEnforcement,
|
|
3792
|
+
decisionTimeoutMs: options.decisionTimeoutMs,
|
|
3744
3793
|
// ATBASH_DEBUG lets an operator turn diagnostics on without editing a
|
|
3745
3794
|
// host's plugin config, which is usually the harder half.
|
|
3746
3795
|
debug: options.debug ?? /^(1|true|yes)$/i.test(resolve("debug")),
|
|
@@ -3939,34 +3988,16 @@ var Atbash = class _Atbash {
|
|
|
3939
3988
|
}
|
|
3940
3989
|
if (!resp.ok) throw await this.httpError(resp);
|
|
3941
3990
|
const bodyBytes = Buffer.from(await resp.arrayBuffer());
|
|
3942
|
-
const verifyPubKey = options.verifyPubKey
|
|
3991
|
+
const verifyPubKey = this.effectiveVerifyKey(options.verifyPubKey);
|
|
3943
3992
|
if (verifyPubKey !== void 0) {
|
|
3944
|
-
|
|
3945
|
-
if (!sig) {
|
|
3946
|
-
throw new SignatureVerificationError(
|
|
3947
|
-
"missing X-Atbash-Signature header"
|
|
3948
|
-
);
|
|
3949
|
-
}
|
|
3950
|
-
let ok;
|
|
3951
|
-
try {
|
|
3952
|
-
ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
|
|
3953
|
-
} catch (err) {
|
|
3954
|
-
throw new SignatureVerificationError(
|
|
3955
|
-
`signature verification threw: ${errorMessage(err)}`
|
|
3956
|
-
);
|
|
3957
|
-
}
|
|
3958
|
-
if (!ok) {
|
|
3959
|
-
throw new SignatureVerificationError(
|
|
3960
|
-
"signature does not verify against configured verifyPubKey"
|
|
3961
|
-
);
|
|
3962
|
-
}
|
|
3993
|
+
verifyResponseSignature(bodyBytes, resp, verifyPubKey);
|
|
3963
3994
|
}
|
|
3964
3995
|
const data = parseJson(bodyBytes);
|
|
3965
3996
|
const rawScore = data.score;
|
|
3966
3997
|
const score = typeof rawScore === "number" && Number.isInteger(rawScore) && rawScore >= 1 && rawScore <= 10 ? rawScore : void 0;
|
|
3967
3998
|
return {
|
|
3968
3999
|
verdict: normalizeVerdict(data.verdict),
|
|
3969
|
-
allow:
|
|
4000
|
+
allow: this.executable(data),
|
|
3970
4001
|
actionType: normalizeActionType(data.action_type),
|
|
3971
4002
|
reason: String(data.reason ?? ""),
|
|
3972
4003
|
confidence: Number(data.confidence ?? 0),
|
|
@@ -3977,7 +4008,8 @@ var Atbash = class _Atbash {
|
|
|
3977
4008
|
enforced: Boolean(data.enforced),
|
|
3978
4009
|
enforcementMode: String(data.enforcement_mode ?? ""),
|
|
3979
4010
|
status: String(data.status ?? ""),
|
|
3980
|
-
score
|
|
4011
|
+
score,
|
|
4012
|
+
refusal: explicitRefusal(data)
|
|
3981
4013
|
};
|
|
3982
4014
|
}
|
|
3983
4015
|
/* ── audit_tool_call (redact → judge → decision) ───────────────────────── */
|
|
@@ -4010,77 +4042,125 @@ var Atbash = class _Atbash {
|
|
|
4010
4042
|
}
|
|
4011
4043
|
try {
|
|
4012
4044
|
this.logger.info?.("[atbash] judge API called", { tool: toolName });
|
|
4013
|
-
const result = await
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
}
|
|
4028
|
-
return {
|
|
4029
|
-
allow: true,
|
|
4030
|
-
verdict: "ALLOW",
|
|
4031
|
-
reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
|
|
4032
|
-
toolCallId: result.toolCallId
|
|
4033
|
-
};
|
|
4034
|
-
}
|
|
4035
|
-
const action = result.actionType;
|
|
4036
|
-
if (action === "block") {
|
|
4045
|
+
const result = await withDeadline(
|
|
4046
|
+
this.judgeAction(actionText, contextText, {
|
|
4047
|
+
toolName,
|
|
4048
|
+
toolArgsJson: argsJson,
|
|
4049
|
+
orgName: this.orgName,
|
|
4050
|
+
// Passed through unredacted — caller vouches for the contents.
|
|
4051
|
+
resolved: input.resolved
|
|
4052
|
+
}),
|
|
4053
|
+
this.decisionTimeoutMs
|
|
4054
|
+
);
|
|
4055
|
+
return this.decide(result);
|
|
4056
|
+
} catch (err) {
|
|
4057
|
+
if (err instanceof SignatureVerificationError) {
|
|
4058
|
+
this.failJudge(errorMessage(err), err);
|
|
4037
4059
|
return {
|
|
4038
4060
|
allow: false,
|
|
4039
|
-
verdict: "
|
|
4040
|
-
reason:
|
|
4041
|
-
toolCallId: result.toolCallId
|
|
4061
|
+
verdict: "ERROR",
|
|
4062
|
+
reason: `The judge's reply failed its signature check (${errorMessage(err)}), so it was not trusted.`
|
|
4042
4063
|
};
|
|
4043
4064
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4065
|
+
return this.failJudge(errorMessage(err), err);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* The verify key for one call (R4, governor tier-2 review). A per-call key
|
|
4070
|
+
* may only narrow what the client accepts: on a client pinned to a judge
|
|
4071
|
+
* key it must be that same key, never a replacement; on an unpinned client
|
|
4072
|
+
* it adds the check.
|
|
4073
|
+
*/
|
|
4074
|
+
effectiveVerifyKey(perCall) {
|
|
4075
|
+
if (perCall === void 0) return this.verifyPubKey;
|
|
4076
|
+
const key3 = perCall.trim().toLowerCase();
|
|
4077
|
+
if (this.verifyPubKey !== void 0 && key3 !== this.verifyPubKey) {
|
|
4078
|
+
throw new SignatureVerificationError(
|
|
4079
|
+
"a per-call verifyPubKey cannot replace the judge key this client is pinned to"
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4082
|
+
return key3;
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Collapse a judge result into a {@link Decision}. The most restrictive
|
|
4086
|
+
* signal wins (security review 2026-09-30, H1): `action_type`, `verdict`
|
|
4087
|
+
* and the canonical `allow` are each able to deny, and only a result where
|
|
4088
|
+
* all three agree may execute. A present `allow` / `decision.allow` that is
|
|
4089
|
+
* not exactly `true` is the judge declining to grant permission, so it
|
|
4090
|
+
* denies even under `failClosed: false` — that option relaxes *errors*, it
|
|
4091
|
+
* never overrides a refusal.
|
|
4092
|
+
*/
|
|
4093
|
+
decide(result) {
|
|
4094
|
+
const toolCallId = result.toolCallId;
|
|
4095
|
+
const deny = (verdict, reason) => ({
|
|
4096
|
+
allow: false,
|
|
4097
|
+
verdict,
|
|
4098
|
+
reason,
|
|
4099
|
+
toolCallId
|
|
4100
|
+
});
|
|
4101
|
+
const notGranted = "the judge response did not grant permission";
|
|
4102
|
+
const held = deny("HOLD", result.reason || "held for human confirmation");
|
|
4103
|
+
const blocked = deny("BLOCK", result.reason || notGranted);
|
|
4104
|
+
if (result.refusal === "BLOCK") return blocked;
|
|
4105
|
+
if (result.verdict === "No verdict") {
|
|
4106
|
+
if (result.actionType === "block") {
|
|
4107
|
+
return blocked;
|
|
4108
|
+
}
|
|
4109
|
+
if (result.actionType === "hold_for_user_confirm") {
|
|
4110
|
+
return held;
|
|
4111
|
+
}
|
|
4112
|
+
if (result.refusal === "HOLD") return held;
|
|
4113
|
+
if (result.status !== "logged") {
|
|
4114
|
+
return this.failJudge(
|
|
4115
|
+
`judge returned no verdict without an audit-tier marker (status: ${result.status || "absent"})`,
|
|
4116
|
+
void 0,
|
|
4117
|
+
toolCallId
|
|
4118
|
+
);
|
|
4051
4119
|
}
|
|
4052
|
-
if (
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
reason: result.reason,
|
|
4058
|
-
toolCallId: result.toolCallId
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4061
|
-
if (result.verdict === "BLOCK") {
|
|
4062
|
-
return {
|
|
4063
|
-
allow: false,
|
|
4064
|
-
verdict: "BLOCK",
|
|
4065
|
-
reason: result.reason,
|
|
4066
|
-
toolCallId: result.toolCallId
|
|
4067
|
-
};
|
|
4068
|
-
}
|
|
4120
|
+
if (this.requireEnforcement) {
|
|
4121
|
+
this.logger.warn?.(
|
|
4122
|
+
"[atbash] judge did not enforce this action (audit tier) and requireEnforcement is on: denied",
|
|
4123
|
+
{ toolCallId }
|
|
4124
|
+
);
|
|
4069
4125
|
return {
|
|
4070
|
-
allow:
|
|
4071
|
-
verdict: "
|
|
4072
|
-
reason:
|
|
4073
|
-
toolCallId
|
|
4126
|
+
allow: false,
|
|
4127
|
+
verdict: "ERROR",
|
|
4128
|
+
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.",
|
|
4129
|
+
toolCallId
|
|
4074
4130
|
};
|
|
4075
4131
|
}
|
|
4076
|
-
return
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4132
|
+
if (result.allow !== true) return deny("BLOCK", notGranted);
|
|
4133
|
+
return {
|
|
4134
|
+
allow: true,
|
|
4135
|
+
verdict: "ALLOW",
|
|
4136
|
+
reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
|
|
4137
|
+
toolCallId
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
const action = result.actionType;
|
|
4141
|
+
if (action === "block") {
|
|
4142
|
+
return deny("BLOCK", result.reason || "Blocked by your organisation's Atbash policy.");
|
|
4143
|
+
}
|
|
4144
|
+
if (action === "hold_for_user_confirm") {
|
|
4145
|
+
return held;
|
|
4146
|
+
}
|
|
4147
|
+
if (result.refusal === "HOLD") return held;
|
|
4148
|
+
if (action === "allow") {
|
|
4149
|
+
if (result.verdict === "HOLD") return held;
|
|
4150
|
+
if (result.verdict === "BLOCK") return blocked;
|
|
4151
|
+
if (result.allow !== true) return deny("BLOCK", notGranted);
|
|
4152
|
+
return {
|
|
4153
|
+
allow: true,
|
|
4154
|
+
verdict: "ALLOW",
|
|
4155
|
+
reason: result.reason,
|
|
4156
|
+
toolCallId
|
|
4157
|
+
};
|
|
4083
4158
|
}
|
|
4159
|
+
return this.failJudge(
|
|
4160
|
+
"unrecognized action_type from judge",
|
|
4161
|
+
void 0,
|
|
4162
|
+
toolCallId
|
|
4163
|
+
);
|
|
4084
4164
|
}
|
|
4085
4165
|
/**
|
|
4086
4166
|
* One exit for every judge failure.
|
|
@@ -4092,18 +4172,30 @@ var Atbash = class _Atbash {
|
|
|
4092
4172
|
*/
|
|
4093
4173
|
failJudge(reason, cause, toolCallId) {
|
|
4094
4174
|
const api2 = cause instanceof AtbashAPIError ? cause : null;
|
|
4175
|
+
const userReason = userFacingReason(reason, api2);
|
|
4095
4176
|
const status = api2 ? ` status=${api2.status || "no-response"}` : "";
|
|
4177
|
+
const detail = this.debug ? ` detail=${truncate(reason, 500)}` : "";
|
|
4096
4178
|
const body = this.debug && api2?.body ? ` body=${truncate(api2.body, 500)}` : "";
|
|
4097
4179
|
this.logger.warn?.(
|
|
4098
|
-
`[atbash] judge API failed \u2014${status} reason=${truncate(
|
|
4180
|
+
`[atbash] judge API failed \u2014${status} reason=${truncate(userReason, 300)}${detail}${body}`,
|
|
4099
4181
|
{
|
|
4100
|
-
reason,
|
|
4101
|
-
...api2 ? { status: api2.status
|
|
4182
|
+
reason: userReason,
|
|
4183
|
+
...api2 ? { status: api2.status } : {},
|
|
4184
|
+
...this.debug && api2 ? { body: api2.body, detail: reason } : {},
|
|
4102
4185
|
endpoint: this.endpoint,
|
|
4103
4186
|
...toolCallId ? { toolCallId } : {}
|
|
4104
4187
|
}
|
|
4105
4188
|
);
|
|
4106
|
-
return this.fail(
|
|
4189
|
+
return this.fail(userReason, toolCallId);
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* The canonical permission for a response body under this client's
|
|
4193
|
+
* settings: {@link canonicalAllow}, minus the audit tier when
|
|
4194
|
+
* `requireEnforcement` is on. Never more permissive than `auditToolCall`.
|
|
4195
|
+
*/
|
|
4196
|
+
executable(data) {
|
|
4197
|
+
if (!canonicalAllow(data)) return false;
|
|
4198
|
+
return !(this.requireEnforcement && isAuditTierBody(data));
|
|
4107
4199
|
}
|
|
4108
4200
|
fail(reason, toolCallId) {
|
|
4109
4201
|
return { allow: !this.failClosed, verdict: "ERROR", reason, toolCallId };
|
|
@@ -4129,11 +4221,16 @@ var Atbash = class _Atbash {
|
|
|
4129
4221
|
this.authHeaders(brid)
|
|
4130
4222
|
);
|
|
4131
4223
|
await this.raiseIfError(resp);
|
|
4132
|
-
const
|
|
4224
|
+
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
4225
|
+
if (this.verifyPubKey !== void 0) {
|
|
4226
|
+
verifyResponseSignature(bytes, resp, this.verifyPubKey);
|
|
4227
|
+
}
|
|
4228
|
+
const parsed = tryParseJson(bytes.toString("utf-8"));
|
|
4229
|
+
const data = isRecord2(parsed) ? parsed : {};
|
|
4133
4230
|
return {
|
|
4134
4231
|
status: normalizeStatus(data.status),
|
|
4135
4232
|
verdict: normalizeVerdict(data.verdict),
|
|
4136
|
-
allow:
|
|
4233
|
+
allow: this.executable(data),
|
|
4137
4234
|
reason: String(data.reason ?? ""),
|
|
4138
4235
|
judgmentId: String(data.judgmentId ?? judgmentId),
|
|
4139
4236
|
onChain: optBool(data.onChain),
|
|
@@ -4608,6 +4705,66 @@ var Atbash = class _Atbash {
|
|
|
4608
4705
|
return native.containsEvasionCharacters(text);
|
|
4609
4706
|
}
|
|
4610
4707
|
};
|
|
4708
|
+
function userFacingReason(reason, api2) {
|
|
4709
|
+
if (api2 && api2.status > 0) {
|
|
4710
|
+
const where = ` (HTTP ${api2.status}${api2.path ? ` from ${api2.path}` : ""})`;
|
|
4711
|
+
const b = api2.body.toLowerCase();
|
|
4712
|
+
if (b.includes("agent not registered")) {
|
|
4713
|
+
return `Agent not registered with Atbash: onboard it in the Atbash dashboard${where}.`;
|
|
4714
|
+
}
|
|
4715
|
+
if (b.includes("jailed")) {
|
|
4716
|
+
return `This agent is jailed; unjail it in the Atbash dashboard${where}.`;
|
|
4717
|
+
}
|
|
4718
|
+
if (b.includes("agent has no policy") || b.includes("no policy configured")) {
|
|
4719
|
+
return `This agent has no policy; attach one in the Atbash dashboard${where}.`;
|
|
4720
|
+
}
|
|
4721
|
+
if (b.includes("audit tier") || b.includes("verdict disabled") || b.includes("verdict not supported")) {
|
|
4722
|
+
return `The organisation's plan does not include checked decisions${where}.`;
|
|
4723
|
+
}
|
|
4724
|
+
return `The Atbash judge answered with an error${where}, so the action was not approved.`;
|
|
4725
|
+
}
|
|
4726
|
+
const text = api2 ? api2.body || reason : reason;
|
|
4727
|
+
return truncate(text.replace(/https?:\/\/[^\s)]+/g, "the judge"), 300);
|
|
4728
|
+
}
|
|
4729
|
+
var DEFAULT_DECISION_TIMEOUT_MS = 6e4;
|
|
4730
|
+
async function withDeadline(p, ms) {
|
|
4731
|
+
let timer;
|
|
4732
|
+
const deadline = new Promise((_, reject) => {
|
|
4733
|
+
timer = setTimeout(
|
|
4734
|
+
() => reject(
|
|
4735
|
+
new Error(
|
|
4736
|
+
`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.`
|
|
4737
|
+
)
|
|
4738
|
+
),
|
|
4739
|
+
ms
|
|
4740
|
+
);
|
|
4741
|
+
});
|
|
4742
|
+
p.catch(() => void 0);
|
|
4743
|
+
try {
|
|
4744
|
+
return await Promise.race([p, deadline]);
|
|
4745
|
+
} finally {
|
|
4746
|
+
clearTimeout(timer);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
function verifyResponseSignature(bodyBytes, resp, verifyPubKey) {
|
|
4750
|
+
const sig = resp.headers.get("X-Atbash-Signature");
|
|
4751
|
+
if (!sig) {
|
|
4752
|
+
throw new SignatureVerificationError("missing X-Atbash-Signature header");
|
|
4753
|
+
}
|
|
4754
|
+
let ok;
|
|
4755
|
+
try {
|
|
4756
|
+
ok = native.verifySignature(bodyBytes, sig, verifyPubKey);
|
|
4757
|
+
} catch (err) {
|
|
4758
|
+
throw new SignatureVerificationError(
|
|
4759
|
+
`signature verification threw: ${errorMessage(err)}`
|
|
4760
|
+
);
|
|
4761
|
+
}
|
|
4762
|
+
if (!ok) {
|
|
4763
|
+
throw new SignatureVerificationError(
|
|
4764
|
+
"signature does not verify against configured verifyPubKey"
|
|
4765
|
+
);
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4611
4768
|
function baseToolCall(raw2) {
|
|
4612
4769
|
return {
|
|
4613
4770
|
toolCallId: String(raw2.tool_call_id ?? ""),
|
|
@@ -4821,7 +4978,11 @@ async function scanMemory(entry, auth, opts) {
|
|
|
4821
4978
|
const atbash = new Atbash(auth.privkey, {
|
|
4822
4979
|
endpoint: opts?.endpoint,
|
|
4823
4980
|
verifyPubKey: opts?.verifyPubKey,
|
|
4824
|
-
orgName: opts?.orgName
|
|
4981
|
+
orgName: opts?.orgName,
|
|
4982
|
+
// Memory scans keep their existing audit-tier semantics (an audit-tier
|
|
4983
|
+
// scan is not a defect the scanner invents). Whether an unenforced scan
|
|
4984
|
+
// may commit memory is the memory-guard PR's decision, not H6's.
|
|
4985
|
+
requireEnforcement: false
|
|
4825
4986
|
});
|
|
4826
4987
|
const result = await atbash.judgeAction(request.prompt, request.context, {
|
|
4827
4988
|
toolName: opts?.toolName ?? "memory_write",
|
|
@@ -4857,6 +5018,7 @@ async function scanMemory(entry, auth, opts) {
|
|
|
4857
5018
|
verdict = "yellow";
|
|
4858
5019
|
}
|
|
4859
5020
|
}
|
|
5021
|
+
if (verdict === "green" && result.allow !== true) verdict = "red";
|
|
4860
5022
|
const parsed = native.parseScoreFromReason(result.reason);
|
|
4861
5023
|
const score = result.score ?? parsed.score ?? native.defaultScoreForVerdict(verdict);
|
|
4862
5024
|
return {
|