@erichll/pi-auto-review 0.1.1

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.
@@ -0,0 +1,29 @@
1
+ export { BoundaryApprovalBroker } from "./broker.ts";
2
+ export { DenialCircuitBreaker } from "./circuit-breaker.ts";
3
+ export { boundaryRequestHash, OneShotGrantStore } from "./grants.ts";
4
+ export { RecentDenialStore } from "./overrides.ts";
5
+ export {
6
+ BOUNDARY_BROKER_SERVICE_KEY,
7
+ getBoundaryBroker,
8
+ publishBoundaryBroker,
9
+ } from "./service.ts";
10
+ export type {
11
+ BoundaryApprovalBrokerService,
12
+ } from "./service.ts";
13
+ export type {
14
+ BoundaryAuditEvent,
15
+ BoundaryDecision,
16
+ BoundaryGrant,
17
+ BoundaryHardDeny,
18
+ BoundaryRequest,
19
+ BoundaryReview,
20
+ BoundaryReviewContext,
21
+ BoundaryReviewer,
22
+ BoundaryReviewerContext,
23
+ BoundaryRiskLevel,
24
+ BoundarySource,
25
+ BoundarySurface,
26
+ BoundaryUserOverride,
27
+ RecentBoundaryDenial,
28
+ UserAuthorization,
29
+ } from "./types.ts";
@@ -0,0 +1,120 @@
1
+ import { boundaryRequestHash } from "./grants.ts";
2
+ import type {
3
+ BoundaryRequest,
4
+ BoundaryReview,
5
+ BoundaryReviewContext,
6
+ BoundaryUserOverride,
7
+ RecentBoundaryDenial,
8
+ } from "./types.ts";
9
+
10
+ type PendingOverride = {
11
+ originalRequestId: string;
12
+ approvedAt: number;
13
+ };
14
+
15
+ export class RecentDenialStore {
16
+ readonly #denials: RecentBoundaryDenial[] = [];
17
+ readonly #pending = new Map<string, PendingOverride>();
18
+ readonly #used = new Set<string>();
19
+ readonly #limit: number;
20
+ readonly #now: () => number;
21
+ readonly #overrideTtlMs: number;
22
+
23
+ constructor(
24
+ limit = 10,
25
+ now: () => number = Date.now,
26
+ overrideTtlMs = 60_000,
27
+ ) {
28
+ this.#limit = limit;
29
+ this.#now = now;
30
+ this.#overrideTtlMs = overrideTtlMs;
31
+ }
32
+
33
+ record(
34
+ request: BoundaryRequest,
35
+ context: BoundaryReviewContext,
36
+ review: BoundaryReview,
37
+ ): void {
38
+ const requestHash = boundaryRequestHash(request);
39
+ const kept = this.#denials.filter(
40
+ (denial) =>
41
+ !(
42
+ denial.sessionId === context.sessionId &&
43
+ denial.scopeKey === context.scopeKey &&
44
+ denial.requestHash === requestHash
45
+ ),
46
+ );
47
+ this.#denials.splice(0, this.#denials.length, ...kept);
48
+ this.#denials.unshift({
49
+ requestId: request.id,
50
+ requestHash,
51
+ request: structuredClone(request),
52
+ review: { ...review },
53
+ sessionId: context.sessionId,
54
+ scopeKey: context.scopeKey,
55
+ deniedAt: this.#now(),
56
+ });
57
+ if (this.#denials.length > this.#limit) {
58
+ this.#denials.length = this.#limit;
59
+ }
60
+ }
61
+
62
+ list(sessionId: string): RecentBoundaryDenial[] {
63
+ return this.#denials
64
+ .filter((denial) => denial.sessionId === sessionId)
65
+ .map((denial) => structuredClone(denial));
66
+ }
67
+
68
+ authorize(
69
+ requestId: string,
70
+ sessionId: string,
71
+ ): RecentBoundaryDenial | undefined {
72
+ const denial = this.#denials.find(
73
+ (entry) =>
74
+ entry.requestId === requestId &&
75
+ entry.sessionId === sessionId,
76
+ );
77
+ if (!denial) return;
78
+ const key = this.key(sessionId, denial.requestHash);
79
+ const pending = this.#pending.get(key);
80
+ if (
81
+ pending &&
82
+ this.#now() - pending.approvedAt > this.#overrideTtlMs
83
+ ) {
84
+ this.#pending.delete(key);
85
+ }
86
+ if (this.#used.has(key) || this.#pending.has(key)) return;
87
+ this.#pending.set(key, {
88
+ originalRequestId: denial.requestId,
89
+ approvedAt: this.#now(),
90
+ });
91
+ return structuredClone(denial);
92
+ }
93
+
94
+ consume(
95
+ request: BoundaryRequest,
96
+ context: BoundaryReviewContext,
97
+ ): BoundaryUserOverride | undefined {
98
+ const requestHash = boundaryRequestHash(request);
99
+ const key = this.key(context.sessionId, requestHash);
100
+ const pending = this.#pending.get(key);
101
+ if (!pending) return;
102
+ this.#pending.delete(key);
103
+ if (this.#now() - pending.approvedAt > this.#overrideTtlMs) return;
104
+ this.#used.add(key);
105
+ return { ...pending };
106
+ }
107
+
108
+ clear(): void {
109
+ this.#denials.length = 0;
110
+ this.#pending.clear();
111
+ this.#used.clear();
112
+ }
113
+
114
+ private key(
115
+ sessionId: string,
116
+ requestHash: string,
117
+ ): string {
118
+ return `${sessionId}\u0000${requestHash}`;
119
+ }
120
+ }
@@ -0,0 +1,50 @@
1
+ import type { BoundaryApprovalBroker } from "./broker.ts";
2
+ import type {
3
+ BoundaryDecision,
4
+ BoundaryRequest,
5
+ BoundaryReviewContext,
6
+ } from "./types.ts";
7
+
8
+ export const BOUNDARY_BROKER_SERVICE_KEY = Symbol.for(
9
+ "pi-auto-review:boundary-approval-broker",
10
+ );
11
+
12
+ export type BoundaryApprovalBrokerService = {
13
+ review(
14
+ request: BoundaryRequest,
15
+ context: BoundaryReviewContext,
16
+ ): Promise<BoundaryDecision>;
17
+ consumeGrant(
18
+ request: BoundaryRequest,
19
+ sessionId: string,
20
+ token: string,
21
+ ): boolean;
22
+ };
23
+
24
+ export function publishBoundaryBroker(
25
+ broker: BoundaryApprovalBroker,
26
+ ): () => void {
27
+ const target = globalThis as Record<symbol, unknown>;
28
+ if (target[BOUNDARY_BROKER_SERVICE_KEY]) {
29
+ throw new Error("pi-auto-review boundary broker is already published");
30
+ }
31
+ const service: BoundaryApprovalBrokerService = {
32
+ review: (request, context) => broker.review(request, context),
33
+ consumeGrant: (request, sessionId, token) =>
34
+ broker.consumeGrant(request, sessionId, token),
35
+ };
36
+ target[BOUNDARY_BROKER_SERVICE_KEY] = service;
37
+ return () => {
38
+ if (target[BOUNDARY_BROKER_SERVICE_KEY] === service) {
39
+ delete target[BOUNDARY_BROKER_SERVICE_KEY];
40
+ }
41
+ };
42
+ }
43
+
44
+ export function getBoundaryBroker():
45
+ | BoundaryApprovalBrokerService
46
+ | undefined {
47
+ return (globalThis as Record<symbol, unknown>)[
48
+ BOUNDARY_BROKER_SERVICE_KEY
49
+ ] as BoundaryApprovalBrokerService | undefined;
50
+ }
@@ -0,0 +1,118 @@
1
+ export type BoundarySurface =
2
+ | "command"
3
+ | "filesystem-read"
4
+ | "filesystem-write"
5
+ | "network"
6
+ | "mcp"
7
+ | "skill"
8
+ | (string & {});
9
+
10
+ export type BoundarySource =
11
+ | "permission-system"
12
+ | "landstrip"
13
+ | (string & {});
14
+
15
+ export type BoundaryRequest = {
16
+ id: string;
17
+ source: BoundarySource;
18
+ surface: BoundarySurface;
19
+ operation: string;
20
+ cwd: string;
21
+ command?: string;
22
+ path?: string;
23
+ resolvedPath?: string;
24
+ destination?: string;
25
+ toolCallId?: string;
26
+ toolName?: string;
27
+ skillName?: string;
28
+ toolInputPreview?: string;
29
+ agentName?: string;
30
+ matchedPolicy?: {
31
+ decision: "ask";
32
+ rule?: string;
33
+ };
34
+ };
35
+
36
+ export type BoundaryRiskLevel = "low" | "medium" | "high" | "critical";
37
+ export type UserAuthorization = "unknown" | "low" | "medium" | "high";
38
+
39
+ export type BoundaryReview = {
40
+ outcome: "allow" | "deny" | "defer";
41
+ riskLevel: BoundaryRiskLevel;
42
+ userAuthorization: UserAuthorization;
43
+ rationale: string;
44
+ };
45
+
46
+ export type BoundaryGrant = {
47
+ token: string;
48
+ requestHash: string;
49
+ sessionId: string;
50
+ expiresAt: number;
51
+ usesRemaining: 1;
52
+ };
53
+
54
+ export type BoundaryDecision =
55
+ | {
56
+ kind: "allow";
57
+ review: BoundaryReview;
58
+ grant?: BoundaryGrant;
59
+ }
60
+ | {
61
+ kind: "deny";
62
+ review: BoundaryReview;
63
+ circuitBreakerTripped: boolean;
64
+ }
65
+ | {
66
+ kind: "defer";
67
+ review: BoundaryReview;
68
+ };
69
+
70
+ export type BoundaryReviewContext = {
71
+ sessionId: string;
72
+ scopeKey: string;
73
+ issueGrant?: boolean;
74
+ };
75
+
76
+ export type BoundaryUserOverride = {
77
+ originalRequestId: string;
78
+ approvedAt: number;
79
+ };
80
+
81
+ export type BoundaryReviewerContext = {
82
+ userOverride?: BoundaryUserOverride;
83
+ };
84
+
85
+ export type RecentBoundaryDenial = {
86
+ requestId: string;
87
+ requestHash: string;
88
+ request: BoundaryRequest;
89
+ review: BoundaryReview;
90
+ sessionId: string;
91
+ scopeKey: string;
92
+ deniedAt: number;
93
+ };
94
+
95
+ export type BoundaryAuditEvent = {
96
+ type:
97
+ | "hard_deny"
98
+ | "review_decision"
99
+ | "review_failure"
100
+ | "circuit_breaker"
101
+ | "grant_issued"
102
+ | "grant_consumed"
103
+ | "grant_rejected"
104
+ | "override_authorized"
105
+ | "override_consumed";
106
+ requestId: string;
107
+ surface: string;
108
+ details: Record<string, unknown>;
109
+ };
110
+
111
+ export type BoundaryReviewer = (
112
+ request: BoundaryRequest,
113
+ context?: BoundaryReviewerContext,
114
+ ) => Promise<BoundaryReview>;
115
+
116
+ export type BoundaryHardDeny = (
117
+ request: BoundaryRequest,
118
+ ) => { rule: string; reason: string } | undefined;
@@ -0,0 +1,12 @@
1
+ {
2
+ "model": "cliproxyapi/codex-auto-review",
3
+ "reasoning": "medium",
4
+ "timeoutMs": 45000,
5
+ "maxTokens": 1600,
6
+ "retries": 1,
7
+ "maxUserTranscriptTokens": 1200,
8
+ "maxToolTranscriptTokens": 1200,
9
+ "maxRelevantResultTokens": 800,
10
+ "failureMode": "deny",
11
+ "grantTtlMs": 60000
12
+ }