mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
/**
|
|
* Records the per-call terminal decision so an evaluated-and-allowed call is
|
|
* distinguishable from a never-evaluated one. The fail-closed boundary owns the
|
|
* recorder and calls exactly one of `recordDecision` / `recordError` per call.
|
|
*/
|
|
export interface DecisionRecorder {
|
|
/** Record a terminal allow/block decision (also bumps the tool-call count). */
|
|
recordDecision(action: "allow" | "block"): void;
|
|
/** Record a gate error that blocked fail-closed (also bumps the count). */
|
|
recordError(): void;
|
|
}
|
|
|
|
/** Narrow logging surface the summary needs: a debug line and a warning. */
|
|
export interface AuditLogger {
|
|
debug(event: string, details?: Record<string, unknown>): void;
|
|
warn(message: string): void;
|
|
}
|
|
|
|
/** Narrow surface the session-shutdown handler depends on. */
|
|
export interface DecisionSummaryWriter {
|
|
writeSummary(logger: AuditLogger): void;
|
|
}
|
|
|
|
/**
|
|
* In-process, per-session decision counters.
|
|
*
|
|
* The boundary produces exactly one terminal decision per tool call, so
|
|
* `toolCalls` must always equal `allowed + blocked + errors`. `writeSummary`
|
|
* emits the counters on `session_shutdown` and flags any mismatch as a cheap
|
|
* structural self-check — a mismatch means a code path re-opened a silent
|
|
* (never-recorded) exit.
|
|
*/
|
|
export class DecisionAudit implements DecisionRecorder {
|
|
private toolCalls = 0;
|
|
private allowed = 0;
|
|
private blocked = 0;
|
|
private errors = 0;
|
|
|
|
recordDecision(action: "allow" | "block"): void {
|
|
this.toolCalls++;
|
|
if (action === "allow") {
|
|
this.allowed++;
|
|
} else {
|
|
this.blocked++;
|
|
}
|
|
}
|
|
|
|
recordError(): void {
|
|
this.toolCalls++;
|
|
this.errors++;
|
|
}
|
|
|
|
/**
|
|
* Emit one `permission.session_summary` debug line with the counters. When
|
|
* `toolCalls !== allowed + blocked + errors`, also emit a warning — the
|
|
* invariant violation means a tool call resolved without a recorded terminal
|
|
* decision (a re-opened silent path).
|
|
*/
|
|
writeSummary(logger: AuditLogger): void {
|
|
const counts = {
|
|
toolCalls: this.toolCalls,
|
|
allowed: this.allowed,
|
|
blocked: this.blocked,
|
|
errors: this.errors,
|
|
};
|
|
logger.debug("permission.session_summary", counts);
|
|
if (this.toolCalls !== this.allowed + this.blocked + this.errors) {
|
|
logger.warn(
|
|
`[pi-permission-system] decision audit invariant violated: ${this.toolCalls} tool calls != ` +
|
|
`${this.allowed} allowed + ${this.blocked} blocked + ${this.errors} errors. ` +
|
|
"A tool call resolved without a recorded terminal decision.",
|
|
);
|
|
}
|
|
}
|
|
}
|