mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
feat(ai-judge): review sink with telemetry health and fail-closed truth table
- add review.ts sink adapter with session-start review-log toggle detection and a privacy key denylist enforced before delegation - add judge.ts enforce truth table: allow requires mode, host contract, telemetry health, cohort qualification, owner approval, activation, judgment result, allow verdict, review acknowledgement, and current generation — each independently forces defer with a distinct reason - route the authorizer callback through the sink and the v0.1 production gate state, which is structurally unreachable and therefore fail-closed
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||||
import type { Model } from "@earendil-works/pi-ai";
|
import type { Model } from "@earendil-works/pi-ai";
|
||||||
@@ -15,6 +17,8 @@ import {
|
|||||||
} from "./model";
|
} from "./model";
|
||||||
import { PROMPT_VERSION, TOOL_SCHEMA_VERSION } from "./prompt";
|
import { PROMPT_VERSION, TOOL_SCHEMA_VERSION } from "./prompt";
|
||||||
import { loadJudgeConfig, type EffectiveJudgeConfig } from "./config";
|
import { loadJudgeConfig, type EffectiveJudgeConfig } from "./config";
|
||||||
|
import { createReviewSink, type ReviewSink } from "./review";
|
||||||
|
import { evaluateEnforceAuthority, v01ProductionGateState } from "./judge";
|
||||||
|
|
||||||
const LINK_NAME = "ai-bash-judge";
|
const LINK_NAME = "ai-bash-judge";
|
||||||
const REVIEW_SCHEMA_VERSION = 1;
|
const REVIEW_SCHEMA_VERSION = 1;
|
||||||
@@ -31,6 +35,8 @@ interface RootSession {
|
|||||||
/** Immutable effective-config snapshot captured at session start
|
/** Immutable effective-config snapshot captured at session start
|
||||||
* (reload-only application: a config edit lands on the next session). */
|
* (reload-only application: a config edit lands on the next session). */
|
||||||
readonly config: EffectiveJudgeConfig;
|
readonly config: EffectiveJudgeConfig;
|
||||||
|
/** Review-log toggle captured at session start (PIEXTENSIO-9 health). */
|
||||||
|
readonly reviewLogEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reasonLength(reason: string): number {
|
function reasonLength(reason: string): number {
|
||||||
@@ -92,6 +98,26 @@ function resultBase(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Read the permission-system review-log toggle (default true when unset). */
|
||||||
|
function readPermissionReviewLogEnabled(): boolean {
|
||||||
|
try {
|
||||||
|
const configPath = join(
|
||||||
|
getAgentDir(),
|
||||||
|
"extensions",
|
||||||
|
"pi-permission-system",
|
||||||
|
"config.json",
|
||||||
|
);
|
||||||
|
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
return parsed.permissionReviewLog !== false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Register a Shadow-only structured-output judge for local native Bash asks. */
|
/** Register a Shadow-only structured-output judge for local native Bash asks. */
|
||||||
export default function permissionAiJudge(pi: ExtensionAPI): void {
|
export default function permissionAiJudge(pi: ExtensionAPI): void {
|
||||||
let root: RootSession | undefined;
|
let root: RootSession | undefined;
|
||||||
@@ -112,6 +138,10 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
LINK_NAME,
|
LINK_NAME,
|
||||||
async (details, _query, log) => {
|
async (details, _query, log) => {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
const sink: ReviewSink = createReviewSink({
|
||||||
|
log,
|
||||||
|
reviewLogEnabled: captured.reviewLogEnabled,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
// Forwarded asks do not carry a structured child full
|
// Forwarded asks do not carry a structured child full
|
||||||
// command in permission-system 25.3/25.4. Never parse the
|
// command in permission-system 25.3/25.4. Never parse the
|
||||||
@@ -122,7 +152,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
details.forwarding !== undefined ||
|
details.forwarding !== undefined ||
|
||||||
details.payload.kind === "forwarded"
|
details.payload.kind === "forwarded"
|
||||||
) {
|
) {
|
||||||
log.review("ai_bash_judge.result", {
|
sink.review("ai_bash_judge.result", {
|
||||||
...resultBase(
|
...resultBase(
|
||||||
captured.judgeRuntimeId,
|
captured.judgeRuntimeId,
|
||||||
details,
|
details,
|
||||||
@@ -149,7 +179,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
if (
|
if (
|
||||||
captured.getSessionId() !== captured.expectedSessionId
|
captured.getSessionId() !== captured.expectedSessionId
|
||||||
) {
|
) {
|
||||||
log.review("ai_bash_judge.result", {
|
sink.review("ai_bash_judge.result", {
|
||||||
...resultBase(
|
...resultBase(
|
||||||
captured.judgeRuntimeId,
|
captured.judgeRuntimeId,
|
||||||
details,
|
details,
|
||||||
@@ -168,7 +198,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
|
|
||||||
const evidence = buildBashJudgmentEvidence(details);
|
const evidence = buildBashJudgmentEvidence(details);
|
||||||
if (evidence === undefined) {
|
if (evidence === undefined) {
|
||||||
log.review("ai_bash_judge.result", {
|
sink.review("ai_bash_judge.result", {
|
||||||
...resultBase(
|
...resultBase(
|
||||||
captured.judgeRuntimeId,
|
captured.judgeRuntimeId,
|
||||||
details,
|
details,
|
||||||
@@ -201,7 +231,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.kind === "judgment") {
|
if (result.kind === "judgment") {
|
||||||
log.review("ai_bash_judge.result", {
|
sink.review("ai_bash_judge.result", {
|
||||||
...resultBase(
|
...resultBase(
|
||||||
captured.judgeRuntimeId,
|
captured.judgeRuntimeId,
|
||||||
details,
|
details,
|
||||||
@@ -227,7 +257,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
evidenceQuality: evidenceQuality(true),
|
evidenceQuality: evidenceQuality(true),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.review("ai_bash_judge.result", {
|
sink.review("ai_bash_judge.result", {
|
||||||
...resultBase(
|
...resultBase(
|
||||||
captured.judgeRuntimeId,
|
captured.judgeRuntimeId,
|
||||||
details,
|
details,
|
||||||
@@ -249,14 +279,26 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bootstrap behavior is Shadow-only: the parsed prediction
|
// Enforce truth table (PIEXTENSIO-3 cat.4 / M5): v0.1
|
||||||
// is recorded but never changes permission authority.
|
// production gates are structurally unreachable, so any
|
||||||
return { kind: "defer" };
|
// configured mode resolves to defer here. The call
|
||||||
|
// exists so the truth table is the single authority
|
||||||
|
// seam — a future slice flips the gate inputs, not the
|
||||||
|
// callback's return path.
|
||||||
|
const authority = evaluateEnforceAuthority(
|
||||||
|
v01ProductionGateState(
|
||||||
|
captured.config.mode,
|
||||||
|
sink.health(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return authority.kind === "allow"
|
||||||
|
? { kind: "allow" }
|
||||||
|
: { kind: "defer" };
|
||||||
} catch {
|
} catch {
|
||||||
// A link exception would abort the whole authority chain.
|
// A link exception would abort the whole authority chain.
|
||||||
// Keep provider/payload/session failures fail-closed and do
|
// Keep provider/payload/session failures fail-closed and do
|
||||||
// not include raw errors or authorization evidence in logs.
|
// not include raw errors or authorization evidence in logs.
|
||||||
log.debug("ai_bash_judge.exception");
|
sink.debug("ai_bash_judge.exception");
|
||||||
return { kind: "defer" };
|
return { kind: "defer" };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,6 +323,7 @@ export default function permissionAiJudge(pi: ExtensionAPI): void {
|
|||||||
shutdown: new AbortController(),
|
shutdown: new AbortController(),
|
||||||
judgeRuntimeId: crypto.randomUUID(),
|
judgeRuntimeId: crypto.randomUUID(),
|
||||||
config: loadJudgeConfig({ agentDir: getAgentDir() }),
|
config: loadJudgeConfig({ agentDir: getAgentDir() }),
|
||||||
|
reviewLogEnabled: readPermissionReviewLogEnabled(),
|
||||||
};
|
};
|
||||||
for (const diagnostic of root.config.diagnostics) {
|
for (const diagnostic of root.config.diagnostics) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type { TelemetryHealth } from "./review";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce truth table (PIEXTENSIO-3 cat.4 / M5).
|
||||||
|
*
|
||||||
|
* `allow` authority requires every gate to hold **independently**; any one
|
||||||
|
* false forces defer. In v0.1 the promotion gates (cohort, approval,
|
||||||
|
* activation) are structurally unreachable — no upstream seam exists to
|
||||||
|
* record them — so a configured `enforce` mode can never grant authority:
|
||||||
|
* mechanically fail-closed by construction, verified by the truth-table
|
||||||
|
* tests toggling each condition.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type EnforceGateState = {
|
||||||
|
/** Host contract present (permission-system host version/seams). */
|
||||||
|
readonly hostContractPresent: boolean;
|
||||||
|
/** Runtime telemetry healthy at decision time. */
|
||||||
|
readonly telemetryHealth: TelemetryHealth;
|
||||||
|
/** Qualified passing promotion cohort (PIEXTENSIO-10 floor). */
|
||||||
|
readonly cohortQualified: boolean;
|
||||||
|
/** Recorded owner approval for the exact candidate identity. */
|
||||||
|
readonly ownerApprovalRecorded: boolean;
|
||||||
|
/** Independent explicit activation act (distinct from approval). */
|
||||||
|
readonly activationRecorded: boolean;
|
||||||
|
/** The attempt's terminal result kind. */
|
||||||
|
readonly resultKind: "judgment" | "preflight_defer" | "infrastructure_failure";
|
||||||
|
/** The semantic verdict when resultKind is judgment, else null. */
|
||||||
|
readonly verdict: "allow" | "deny" | "defer" | null;
|
||||||
|
/** Review write acknowledged durably before authority. */
|
||||||
|
readonly reviewAcknowledged: boolean;
|
||||||
|
/** The in-flight generation is still current (not fenced by shutdown). */
|
||||||
|
readonly generationCurrent: boolean;
|
||||||
|
/** Effective mode (only `enforce` can even consider authority). */
|
||||||
|
readonly mode: "shadow" | "enforce";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnforceOutcome = { kind: "allow" } | { kind: "defer"; blockedBy: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate the Enforce truth table. Shadow always defers. The distinct
|
||||||
|
* `blockedBy` reasons keep each gate's veto observable in tests (and in a
|
||||||
|
* future review event) without granting anything.
|
||||||
|
*/
|
||||||
|
export function evaluateEnforceAuthority(
|
||||||
|
state: EnforceGateState,
|
||||||
|
): EnforceOutcome {
|
||||||
|
if (state.mode !== "enforce") {
|
||||||
|
return { kind: "defer", blockedBy: "mode_shadow" };
|
||||||
|
}
|
||||||
|
if (!state.hostContractPresent) {
|
||||||
|
return { kind: "defer", blockedBy: "host_contract_absent" };
|
||||||
|
}
|
||||||
|
if (state.telemetryHealth !== "healthy") {
|
||||||
|
return { kind: "defer", blockedBy: `telemetry_${state.telemetryHealth}` };
|
||||||
|
}
|
||||||
|
if (!state.cohortQualified) {
|
||||||
|
return { kind: "defer", blockedBy: "cohort_not_qualified" };
|
||||||
|
}
|
||||||
|
if (!state.ownerApprovalRecorded) {
|
||||||
|
return { kind: "defer", blockedBy: "owner_approval_absent" };
|
||||||
|
}
|
||||||
|
if (!state.activationRecorded) {
|
||||||
|
return { kind: "defer", blockedBy: "activation_absent" };
|
||||||
|
}
|
||||||
|
if (state.resultKind !== "judgment") {
|
||||||
|
return { kind: "defer", blockedBy: `result_${state.resultKind}` };
|
||||||
|
}
|
||||||
|
if (state.verdict !== "allow") {
|
||||||
|
return { kind: "defer", blockedBy: `verdict_${state.verdict ?? "null"}` };
|
||||||
|
}
|
||||||
|
if (!state.reviewAcknowledged) {
|
||||||
|
return { kind: "defer", blockedBy: "review_unacknowledged" };
|
||||||
|
}
|
||||||
|
if (!state.generationCurrent) {
|
||||||
|
return { kind: "defer", blockedBy: "generation_stale" };
|
||||||
|
}
|
||||||
|
return { kind: "allow" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The v0.1 production gate state: the promotion gates are structurally
|
||||||
|
* unreachable until the upstream seams exist, so `enforce` defers here
|
||||||
|
* regardless of configuration. The full truth table above is exercised
|
||||||
|
* through injected fake gate states in tests only.
|
||||||
|
*/
|
||||||
|
export function v01ProductionGateState(
|
||||||
|
mode: "shadow" | "enforce",
|
||||||
|
telemetryHealth: TelemetryHealth,
|
||||||
|
): EnforceGateState {
|
||||||
|
return {
|
||||||
|
hostContractPresent: true,
|
||||||
|
telemetryHealth,
|
||||||
|
cohortQualified: false,
|
||||||
|
ownerApprovalRecorded: false,
|
||||||
|
activationRecorded: false,
|
||||||
|
resultKind: "judgment",
|
||||||
|
verdict: null,
|
||||||
|
reviewAcknowledged: false,
|
||||||
|
generationCurrent: true,
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { AuthorizerLog } from "@gotgenes/pi-permission-system";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Review-sink adapter (PIEXTENSIO-3 cat.5, PIEXTENSIO-9 telemetry-health
|
||||||
|
* prerequisite).
|
||||||
|
*
|
||||||
|
* Wraps the chain-provided `AuthorizerLog` with the write-acknowledgement
|
||||||
|
* semantics the reconstructed upstream seam lacks: the installed
|
||||||
|
* `review()` returns void (failures are swallowed into a UI warning), so
|
||||||
|
* this adapter tracks sink health from what it *can* observe —
|
||||||
|
*
|
||||||
|
* - `enabled`: derived once at session start by reading the
|
||||||
|
* permission-system review-log toggle from its config file. A disabled
|
||||||
|
* review sink marks the runtime non-evaluable (PIEXTENSIO-9: data
|
||||||
|
* collected while disabled can never enter a promotion cohort).
|
||||||
|
* - `write_failed`/`integrity_anomaly`: not detectable at runtime against
|
||||||
|
* the void seam; they are represented in the health enum so the truth
|
||||||
|
* table can be exercised with fake gates, and surface offline as
|
||||||
|
* coverage gaps (enrollment without a result).
|
||||||
|
*
|
||||||
|
* The privacy denylist is enforced at this sink: event keys matching the
|
||||||
|
* forbidden patterns are stripped before delegation, so a future upstream
|
||||||
|
* change that starts persisting unknown keys cannot leak Bash text,
|
||||||
|
* working directories, or model replies through the review log.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type TelemetryHealth =
|
||||||
|
| "healthy"
|
||||||
|
| "disabled"
|
||||||
|
| "write_failed"
|
||||||
|
| "integrity_anomaly";
|
||||||
|
|
||||||
|
/** Key-name patterns the permission-system's redactor already masks; the
|
||||||
|
* judge must not fight it by renaming, and must not add keys that carry
|
||||||
|
* evidence content. Forbidden for any *new* judge-owned event key. */
|
||||||
|
const FORBIDDEN_KEY_PATTERNS = [
|
||||||
|
/token/i,
|
||||||
|
/key/i,
|
||||||
|
/secret/i,
|
||||||
|
/password/i,
|
||||||
|
/credential/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface ReviewSink {
|
||||||
|
/** Current telemetry health; read by the truth table before authority. */
|
||||||
|
readonly health: () => TelemetryHealth;
|
||||||
|
/** Metadata-only review write through the acknowledged sink. */
|
||||||
|
readonly review: (event: string, details: Record<string, unknown>) => void;
|
||||||
|
/** Debug write (gated by the permission-system's debug toggle). */
|
||||||
|
readonly debug: (event: string, details?: Record<string, unknown>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewSinkDeps {
|
||||||
|
/** Chain-provided logging seam (returns void; health is tracked here). */
|
||||||
|
readonly log: AuthorizerLog;
|
||||||
|
/** Review-log toggle read at session start; false marks non-evaluable. */
|
||||||
|
readonly reviewLogEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip forbidden keys defensively; a metadata-only event never carries them. */
|
||||||
|
function stripForbiddenKeys(
|
||||||
|
details: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const clean: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(details)) {
|
||||||
|
if (FORBIDDEN_KEY_PATTERNS.some((pattern) => pattern.test(key))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clean[key] = value;
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReviewSink(deps: ReviewSinkDeps): ReviewSink {
|
||||||
|
let health: TelemetryHealth = deps.reviewLogEnabled
|
||||||
|
? "healthy"
|
||||||
|
: "disabled";
|
||||||
|
return {
|
||||||
|
health: () => health,
|
||||||
|
review: (event, details) => {
|
||||||
|
if (health === "disabled") {
|
||||||
|
// Writes while disabled are still attempted (the toggle is
|
||||||
|
// read at start; the permission-system may have re-enabled
|
||||||
|
// it), but the runtime stays non-evaluable.
|
||||||
|
deps.log.review(event, stripForbiddenKeys(details));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.log.review(event, stripForbiddenKeys(details));
|
||||||
|
},
|
||||||
|
debug: (event, details) => {
|
||||||
|
deps.log.debug(event, details);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
evaluateEnforceAuthority,
|
||||||
|
v01ProductionGateState,
|
||||||
|
type EnforceGateState,
|
||||||
|
} from "../src/judge";
|
||||||
|
|
||||||
|
const ALL_OPEN: EnforceGateState = {
|
||||||
|
hostContractPresent: true,
|
||||||
|
telemetryHealth: "healthy",
|
||||||
|
cohortQualified: true,
|
||||||
|
ownerApprovalRecorded: true,
|
||||||
|
activationRecorded: true,
|
||||||
|
resultKind: "judgment",
|
||||||
|
verdict: "allow",
|
||||||
|
reviewAcknowledged: true,
|
||||||
|
generationCurrent: true,
|
||||||
|
mode: "enforce",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("evaluateEnforceAuthority — every gate independently forces defer", () => {
|
||||||
|
it("allows only when every gate holds", () => {
|
||||||
|
expect(evaluateEnforceAuthority(ALL_OPEN)).toEqual({ kind: "allow" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const cases: ReadonlyArray<{
|
||||||
|
name: string;
|
||||||
|
patch: Partial<EnforceGateState>;
|
||||||
|
expectedReason: string;
|
||||||
|
}> = [
|
||||||
|
{ name: "shadow mode", patch: { mode: "shadow" }, expectedReason: "mode_shadow" },
|
||||||
|
{ name: "host contract absent", patch: { hostContractPresent: false }, expectedReason: "host_contract_absent" },
|
||||||
|
{ name: "telemetry disabled", patch: { telemetryHealth: "disabled" }, expectedReason: "telemetry_disabled" },
|
||||||
|
{ name: "telemetry write failed", patch: { telemetryHealth: "write_failed" }, expectedReason: "telemetry_write_failed" },
|
||||||
|
{ name: "telemetry integrity anomaly", patch: { telemetryHealth: "integrity_anomaly" }, expectedReason: "telemetry_integrity_anomaly" },
|
||||||
|
{ name: "cohort not qualified", patch: { cohortQualified: false }, expectedReason: "cohort_not_qualified" },
|
||||||
|
{ name: "owner approval absent", patch: { ownerApprovalRecorded: false }, expectedReason: "owner_approval_absent" },
|
||||||
|
{ name: "activation absent", patch: { activationRecorded: false }, expectedReason: "activation_absent" },
|
||||||
|
{ name: "preflight result", patch: { resultKind: "preflight_defer" }, expectedReason: "result_preflight_defer" },
|
||||||
|
{ name: "infrastructure result", patch: { resultKind: "infrastructure_failure" }, expectedReason: "result_infrastructure_failure" },
|
||||||
|
{ name: "semantic deny verdict", patch: { verdict: "deny" }, expectedReason: "verdict_deny" },
|
||||||
|
{ name: "semantic defer verdict", patch: { verdict: "defer" }, expectedReason: "verdict_defer" },
|
||||||
|
{ name: "review unacknowledged", patch: { reviewAcknowledged: false }, expectedReason: "review_unacknowledged" },
|
||||||
|
{ name: "stale generation", patch: { generationCurrent: false }, expectedReason: "generation_stale" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, patch, expectedReason } of cases) {
|
||||||
|
it(`${name} defers with a distinct reason`, () => {
|
||||||
|
const state: EnforceGateState = { ...ALL_OPEN, ...patch };
|
||||||
|
expect(evaluateEnforceAuthority(state)).toEqual({
|
||||||
|
kind: "defer",
|
||||||
|
blockedBy: expectedReason,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("evaluateEnforceAuthority — v0.1 production state", () => {
|
||||||
|
it("never grants authority for any mode or telemetry state in v0.1", () => {
|
||||||
|
const modes = ["shadow", "enforce"] as const;
|
||||||
|
const healths = ["healthy", "disabled", "write_failed", "integrity_anomaly"] as const;
|
||||||
|
for (const mode of modes) {
|
||||||
|
for (const health of healths) {
|
||||||
|
const outcome = evaluateEnforceAuthority(
|
||||||
|
v01ProductionGateState(mode, health),
|
||||||
|
);
|
||||||
|
expect(outcome.kind).toBe("defer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks v0.1 enforce on the cohort gate", () => {
|
||||||
|
const outcome = evaluateEnforceAuthority(
|
||||||
|
v01ProductionGateState("enforce", "healthy"),
|
||||||
|
);
|
||||||
|
expect(outcome).toEqual({ kind: "defer", blockedBy: "cohort_not_qualified" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createReviewSink, type ReviewSinkDeps } from "../src/review";
|
||||||
|
import type { AuthorizerLog } from "@gotgenes/pi-permission-system";
|
||||||
|
|
||||||
|
function fakeLog(): AuthorizerLog & {
|
||||||
|
reviews: Array<{ event: string; details: Record<string, unknown> }>;
|
||||||
|
debugs: Array<{ event: string; details?: Record<string, unknown> }>;
|
||||||
|
} {
|
||||||
|
const reviews: Array<{ event: string; details: Record<string, unknown> }> = [];
|
||||||
|
const debugs: Array<{ event: string; details?: Record<string, unknown> }> = [];
|
||||||
|
return {
|
||||||
|
reviews,
|
||||||
|
debugs,
|
||||||
|
review: (event, details = {}) => reviews.push({ event, details }),
|
||||||
|
debug: (event, details) => debugs.push({ event, details }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("createReviewSink — telemetry health", () => {
|
||||||
|
it("marks the runtime disabled when the review log toggle is off", () => {
|
||||||
|
const log = fakeLog();
|
||||||
|
const deps: ReviewSinkDeps = { log, reviewLogEnabled: false };
|
||||||
|
const sink = createReviewSink(deps);
|
||||||
|
expect(sink.health()).toBe("disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports healthy when the toggle is on", () => {
|
||||||
|
const sink = createReviewSink({ log: fakeLog(), reviewLogEnabled: true });
|
||||||
|
expect(sink.health()).toBe("healthy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createReviewSink — privacy denylist at the sink", () => {
|
||||||
|
it("strips keys matching forbidden patterns before delegation", () => {
|
||||||
|
const log = fakeLog();
|
||||||
|
const sink = createReviewSink({ log, reviewLogEnabled: true });
|
||||||
|
sink.review("ai_bash_judge.result", {
|
||||||
|
requestId: "req-1",
|
||||||
|
apiToken: "leak",
|
||||||
|
sshKey: "leak",
|
||||||
|
secrets: "leak",
|
||||||
|
password: "leak",
|
||||||
|
credentials: "leak",
|
||||||
|
outputUsage: 10,
|
||||||
|
});
|
||||||
|
expect(log.reviews).toEqual([
|
||||||
|
{
|
||||||
|
event: "ai_bash_judge.result",
|
||||||
|
details: { requestId: "req-1", outputUsage: 10 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes metadata-only events through unchanged", () => {
|
||||||
|
const log = fakeLog();
|
||||||
|
const sink = createReviewSink({ log, reviewLogEnabled: true });
|
||||||
|
sink.review("ai_bash_judge.result", {
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: "req-1",
|
||||||
|
judgeRuntimeId: "abc",
|
||||||
|
mode: "shadow",
|
||||||
|
resultKind: "judgment",
|
||||||
|
});
|
||||||
|
expect(log.reviews[0]?.details).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: "req-1",
|
||||||
|
judgeRuntimeId: "abc",
|
||||||
|
mode: "shadow",
|
||||||
|
resultKind: "judgment",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates debug writes without stripping", () => {
|
||||||
|
const log = fakeLog();
|
||||||
|
const sink = createReviewSink({ log, reviewLogEnabled: true });
|
||||||
|
sink.debug("ai_bash_judge.exception");
|
||||||
|
expect(log.debugs).toEqual([{ event: "ai_bash_judge.exception" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user