a11oy / packages /policy /src /gates /thresholdPolicySeverity_gate.ts
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame
11.1 kB
// SPDX-License-Identifier: Apache-2.0
// (c) 2026 Lutar, Stephen P. -- SZL Holdings
// ORCID: 0009-0001-0110-4173
//
// Layer 6 -- severity-indexed witness threshold policy gate.
//
// Policy rationale:
// High-consequence actions should not pass on a flat confidence score. The
// evidence threshold rises with severity, and the witness quorum rises from
// 2-of-N for ordinary/property actions to 3-of-N for capital/critical actions.
// This runtime gate is TypeScript-only: it does not claim Lean closure.
//
// Policy: allow iff confidence >= severity-adjusted threshold and the attested
// unique witness count meets the required quorum. Allowed decisions emit a
// deterministic DSSE-shaped HMAC receipt for downstream audit.
import { createHmac, createHash } from "node:crypto";
export type SeverityClass = "low" | "medium" | "high" | "critical" | "capital";
export type DecisionClass = "ordinary" | "property" | "capital";
export interface SeverityWitness {
id: string;
role: string;
attested: boolean;
}
export interface DsseSignature {
keyid: string;
sig: string;
}
export interface DsseEnvelope {
payloadType: "application/vnd.szl.threshold-policy.v1+json";
payload: string;
signatures: DsseSignature[];
}
export interface ThresholdPolicySeverityGateConfig {
baseThreshold?: number;
severitySlope?: number;
maxThreshold?: number;
standardWitnesses?: number;
capitalWitnesses?: number;
signingKey?: string;
keyId?: string;
}
export interface ThresholdPolicySeverityGateOpts {
actionId: string;
severity: SeverityClass;
decisionClass?: DecisionClass;
confidence: number;
witnesses: readonly SeverityWitness[];
}
export interface ThresholdPolicySeverityDecision {
allow: boolean;
rationale: string;
formula: "ThresholdPolicySeverity";
claimStatus: "verified-runtime";
leanTheorem: null;
leanFile: null;
leanCommitSha: null;
actionId: string;
severity: SeverityClass;
decisionClass: DecisionClass;
confidence: number;
requiredThreshold: number;
requiredWitnesses: number;
attestedWitnesses: number;
witnessIds: readonly string[];
lambdaScore: number;
dsseReceipt?: DsseEnvelope;
}
const DEFAULT_BASE_THRESHOLD = 0.70;
const DEFAULT_SEVERITY_SLOPE = 0.20;
const DEFAULT_MAX_THRESHOLD = 0.95;
const DEFAULT_STANDARD_WITNESSES = 2;
const DEFAULT_CAPITAL_WITNESSES = 3;
// DEMO ONLY — not non-repudiable. DEFAULT_SIGNING_KEY is a hard-coded symmetric
// HMAC secret for local/dev use. HMAC-SHA-256 gives integrity + authenticity to
// key-holders but is NOT an asymmetric signature: anyone with read access to the
// key can mint receipts and there is zero non-repudiation. In production replace
// with an Ed25519 keypair (production signature scheme — non-repudiable) or a
// Sigstore keyless flow (https://docs.sigstore.dev/cosign/signing/signing_with_blobs/).
// See PhD Crypto Verdict 2026-05-30 Finding A2.
const DEFAULT_SIGNING_KEY = "a11oy-threshold-policy-dev-key";
const DEFAULT_KEY_ID = "hmac-sha256:a11oy-threshold-policy-dev";
const SEVERITY_WEIGHT: Record<SeverityClass, number> = {
low: 0,
medium: 0.35,
high: 0.65,
critical: 0.9,
capital: 1,
};
function assertUnit(name: string, value: number): void {
if (!Number.isFinite(value) || value < 0 || value > 1) {
throw new Error(`ThresholdPolicySeverityGate: ${name} must be in [0,1]; got ${value}`);
}
}
function assertPositiveInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value < 1) {
throw new Error(`ThresholdPolicySeverityGate: ${name} must be a positive integer; got ${value}`);
}
}
function canonicalJson(value: unknown): string {
if (value === null) return "null";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(canonicalJson).join(",")}]`;
}
if (typeof value === "object") {
const record = value as Record<string, unknown>;
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
}
throw new Error("ThresholdPolicySeverityGate: unsupported receipt payload value");
}
function b64url(input: string): string {
return Buffer.from(input, "utf8").toString("base64url");
}
/**
* Pre-Authentication Encoding for this gate's DSSE-shaped receipt.
*
* NOTE — this is NOT the canonical DSSE v1 PAE. The DSSE spec
* (https://github.com/secure-systems-lab/dsse/blob/master/protocol.md) defines:
* PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
* where "DSSEv1" is a LITERAL prefix (not length-counted) and SP = 0x20.
* This implementation instead treats "DSSEv1" as a length-counted piece, emitting
* LEN("DSSEv1") SP "DSSEv1" SP LEN(type) SP type SP LEN(payload) SP payload
* so receipts from this gate are NOT wire-compatible with an upstream DSSE
* verifier or with the canonical encoding being unified in rae1 (a11oy#149).
* Documented for the comment sweep; logic intentionally unchanged here.
* See PhD Crypto Verdict 2026-05-30 Finding A1 (three incompatible PAE encodings).
*/
function pae(payloadType: string, payload: string): string {
const pieces = ["DSSEv1", payloadType, payload];
return pieces.map((piece) => `${Buffer.byteLength(piece, "utf8")} ${piece}`).join(" ");
}
function signDsse(payload: Record<string, unknown>, signingKey: string, keyid: string): DsseEnvelope {
const payloadType = "application/vnd.szl.threshold-policy.v1+json" as const;
const encodedPayload = b64url(canonicalJson(payload));
// DEMO ONLY — not non-repudiable: this is a symmetric HMAC-SHA-256 MAC tag
// placed in the DSSE `signatures[].sig` field, not an asymmetric signature.
// Verifiable only by holders of `signingKey`. See Finding A2 (crypto verdict).
const sig = createHmac("sha256", signingKey)
.update(pae(payloadType, encodedPayload), "utf8")
.digest("base64url");
return {
payloadType,
payload: encodedPayload,
signatures: [{ keyid, sig }],
};
}
function uniqueAttestedWitnesses(witnesses: readonly SeverityWitness[]): SeverityWitness[] {
const seen = new Set<string>();
const unique: SeverityWitness[] = [];
for (const witness of witnesses) {
if (typeof witness.id !== "string" || witness.id.trim() === "") {
throw new Error("ThresholdPolicySeverityGate: witness id is required");
}
if (typeof witness.role !== "string" || witness.role.trim() === "") {
throw new Error(`ThresholdPolicySeverityGate: witness role is required for ${witness.id}`);
}
const id = witness.id.normalize("NFC");
if (witness.attested && !seen.has(id)) {
seen.add(id);
unique.push({ ...witness, id, role: witness.role.normalize("NFC") });
}
}
return unique;
}
export function thresholdPolicySeverityGate(
config: ThresholdPolicySeverityGateConfig = {}
): (opts: ThresholdPolicySeverityGateOpts) => ThresholdPolicySeverityDecision {
const baseThreshold = config.baseThreshold ?? DEFAULT_BASE_THRESHOLD;
const severitySlope = config.severitySlope ?? DEFAULT_SEVERITY_SLOPE;
const maxThreshold = config.maxThreshold ?? DEFAULT_MAX_THRESHOLD;
const standardWitnesses = config.standardWitnesses ?? DEFAULT_STANDARD_WITNESSES;
const capitalWitnesses = config.capitalWitnesses ?? DEFAULT_CAPITAL_WITNESSES;
const signingKey = config.signingKey ?? DEFAULT_SIGNING_KEY;
const keyId = config.keyId ?? DEFAULT_KEY_ID;
assertUnit("baseThreshold", baseThreshold);
assertUnit("severitySlope", severitySlope);
assertUnit("maxThreshold", maxThreshold);
assertPositiveInteger("standardWitnesses", standardWitnesses);
assertPositiveInteger("capitalWitnesses", capitalWitnesses);
if (maxThreshold < baseThreshold) {
throw new Error("ThresholdPolicySeverityGate: maxThreshold must be >= baseThreshold");
}
return function gate(opts: ThresholdPolicySeverityGateOpts): ThresholdPolicySeverityDecision {
if (!opts.actionId || typeof opts.actionId !== "string") {
throw new Error("ThresholdPolicySeverityGate: actionId is required");
}
if (!(opts.severity in SEVERITY_WEIGHT)) {
throw new Error(`ThresholdPolicySeverityGate: unknown severity ${String(opts.severity)}`);
}
assertUnit("confidence", opts.confidence);
if (!Array.isArray(opts.witnesses)) {
throw new Error("ThresholdPolicySeverityGate: witnesses must be an array");
}
const decisionClass = opts.decisionClass ?? (opts.severity === "capital" ? "capital" : "ordinary");
if (!["ordinary", "property", "capital"].includes(decisionClass)) {
throw new Error(`ThresholdPolicySeverityGate: unknown decisionClass ${String(decisionClass)}`);
}
const requiredThreshold = Math.min(
maxThreshold,
baseThreshold + severitySlope * SEVERITY_WEIGHT[opts.severity],
);
const requiredWitnesses =
decisionClass === "capital" || opts.severity === "capital" || opts.severity === "critical"
? capitalWitnesses
: standardWitnesses;
const attested = uniqueAttestedWitnesses(opts.witnesses);
const witnessIds = attested.map((witness) => witness.id).sort();
const confidencePass = opts.confidence >= requiredThreshold;
const witnessPass = attested.length >= requiredWitnesses;
const allow = confidencePass && witnessPass;
const lambdaScore = Math.min(opts.confidence / requiredThreshold, attested.length / requiredWitnesses, 1);
const receiptPayload = {
actionId: opts.actionId.normalize("NFC"),
attestedWitnesses: attested.length,
confidence: opts.confidence,
decisionClass,
formula: "ThresholdPolicySeverity",
requiredThreshold,
requiredWitnesses,
severity: opts.severity,
witnessIds,
};
const receiptHash = createHash("sha256").update(canonicalJson(receiptPayload), "utf8").digest("hex");
const rationale = allow
? `ThresholdPolicySeverity action ${opts.actionId}: confidence ${opts.confidence.toFixed(3)} >= ` +
`${requiredThreshold.toFixed(3)} and ${attested.length}/${requiredWitnesses} witnesses attested. ` +
`DSSE receipt sha256=${receiptHash.slice(0, 16)}`
: `ThresholdPolicySeverity action ${opts.actionId}: ` +
`${confidencePass ? "confidence pass" : `confidence ${opts.confidence.toFixed(3)} < ${requiredThreshold.toFixed(3)}`}; ` +
`${witnessPass ? "witness pass" : `${attested.length}/${requiredWitnesses} witnesses attested`} — deny.`;
return {
allow,
rationale,
formula: "ThresholdPolicySeverity",
claimStatus: "verified-runtime",
leanTheorem: null,
leanFile: null,
leanCommitSha: null,
actionId: opts.actionId.normalize("NFC"),
severity: opts.severity,
decisionClass,
confidence: opts.confidence,
requiredThreshold,
requiredWitnesses,
attestedWitnesses: attested.length,
witnessIds,
lambdaScore,
dsseReceipt: allow ? signDsse({ ...receiptPayload, receiptHash }, signingKey, keyId) : undefined,
};
};
}