mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user