@hank-warren/pi-auto-permissions 0.16.2 → 0.17.0
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.
- package/CHANGELOG.md +24 -0
- package/README.md +24 -81
- package/auto-permissions-setup/SKILL.md +4 -2
- package/classify.ts +7 -26
- package/config-writer.ts +5 -50
- package/config.ts +84 -236
- package/denial-log.ts +5 -22
- package/evaluation-log.ts +8 -51
- package/gates.ts +6 -13
- package/guardian-reviewer.ts +68 -170
- package/index.ts +50 -254
- package/jsonl-sidecar.ts +22 -0
- package/override-evidence.ts +3 -17
- package/package.json +3 -8
- package/prompt-select.ts +4 -15
- package/review-display.ts +14 -175
- package/review-queue.ts +2 -12
- package/review-scope.ts +4 -4
- package/review.ts +35 -62
- package/session-overrides.ts +27 -145
- package/settings-command.ts +0 -45
- package/settings-menu.ts +5 -132
- package/usage-log.ts +3 -20
- package/widget-status.ts +4 -17
- package/approval-pulse.ts +0 -80
- package/loop-context.ts +0 -41
- package/loop-revise-budget.ts +0 -348
- package/openai-codex-transport.ts +0 -9
- package/standing-overrides.ts +0 -266
package/standing-overrides.ts
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmodSync,
|
|
3
|
-
closeSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
openSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
statSync,
|
|
9
|
-
unlinkSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
|
-
import { pid } from "node:process";
|
|
14
|
-
import { OVERRIDE_ANCHOR_START, type PermissionOverride } from "./override-evidence.js";
|
|
15
|
-
|
|
16
|
-
const STANDING_APPROVAL_SCOPE = "comparable" as const;
|
|
17
|
-
export const STANDING_APPROVAL_LIMIT = 200;
|
|
18
|
-
const STALE_LEDGER_LOCK_MS = 60_000;
|
|
19
|
-
|
|
20
|
-
export interface StandingApprovalRecord {
|
|
21
|
-
v: 1;
|
|
22
|
-
ts: string;
|
|
23
|
-
gate: {
|
|
24
|
-
label: string;
|
|
25
|
-
group: string;
|
|
26
|
-
};
|
|
27
|
-
command: string;
|
|
28
|
-
scope: typeof STANDING_APPROVAL_SCOPE;
|
|
29
|
-
project: string;
|
|
30
|
-
reason: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface StandingApprovalInput {
|
|
34
|
-
gate: StandingApprovalRecord["gate"];
|
|
35
|
-
command: string;
|
|
36
|
-
project: string;
|
|
37
|
-
reason: string;
|
|
38
|
-
timestamp?: Date;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function buildStandingApprovalRecord(input: StandingApprovalInput): StandingApprovalRecord {
|
|
42
|
-
return {
|
|
43
|
-
v: 1,
|
|
44
|
-
ts: (input.timestamp ?? new Date()).toISOString(),
|
|
45
|
-
gate: { label: input.gate.label, group: input.gate.group },
|
|
46
|
-
command: input.command,
|
|
47
|
-
scope: STANDING_APPROVAL_SCOPE,
|
|
48
|
-
project: input.project,
|
|
49
|
-
reason: input.reason,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parseRecord(value: unknown): StandingApprovalRecord | undefined {
|
|
54
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
55
|
-
const record = value as Partial<StandingApprovalRecord>;
|
|
56
|
-
const gate = record.gate as Partial<StandingApprovalRecord["gate"]> | undefined;
|
|
57
|
-
if (
|
|
58
|
-
record.v !== 1
|
|
59
|
-
|| typeof record.ts !== "string"
|
|
60
|
-
|| !Number.isFinite(Date.parse(record.ts))
|
|
61
|
-
|| !gate
|
|
62
|
-
|| typeof gate.label !== "string"
|
|
63
|
-
|| !gate.label.trim()
|
|
64
|
-
|| typeof gate.group !== "string"
|
|
65
|
-
|| !gate.group.trim()
|
|
66
|
-
|| typeof record.command !== "string"
|
|
67
|
-
|| !record.command.trim()
|
|
68
|
-
|| record.scope !== STANDING_APPROVAL_SCOPE
|
|
69
|
-
|| typeof record.project !== "string"
|
|
70
|
-
|| !record.project.trim()
|
|
71
|
-
|| typeof record.reason !== "string"
|
|
72
|
-
|| !record.reason.trim()
|
|
73
|
-
) {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
v: 1,
|
|
78
|
-
ts: record.ts,
|
|
79
|
-
gate: { label: gate.label.trim(), group: gate.group.trim() },
|
|
80
|
-
command: record.command,
|
|
81
|
-
scope: STANDING_APPROVAL_SCOPE,
|
|
82
|
-
project: record.project.trim(),
|
|
83
|
-
reason: record.reason.trim(),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Read valid records in file order. Malformed and future-version lines are ignored. */
|
|
88
|
-
export function readStandingApprovals(path: string): StandingApprovalRecord[] {
|
|
89
|
-
let text: string;
|
|
90
|
-
try {
|
|
91
|
-
text = readFileSync(path, "utf8");
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
const records: StandingApprovalRecord[] = [];
|
|
97
|
-
for (const line of text.split("\n")) {
|
|
98
|
-
if (!line.trim()) continue;
|
|
99
|
-
try {
|
|
100
|
-
const record = parseRecord(JSON.parse(line));
|
|
101
|
-
if (record) records.push(record);
|
|
102
|
-
} catch {
|
|
103
|
-
// One corrupt line must not discard later user approvals.
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return records;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function openLedgerLock(lockPath: string): number {
|
|
110
|
-
try {
|
|
111
|
-
return openSync(lockPath, "wx", 0o600);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
114
|
-
|
|
115
|
-
let modifiedAt: number;
|
|
116
|
-
try {
|
|
117
|
-
modifiedAt = statSync(lockPath).mtimeMs;
|
|
118
|
-
} catch (statError) {
|
|
119
|
-
if ((statError as NodeJS.ErrnoException).code === "ENOENT") {
|
|
120
|
-
return openSync(lockPath, "wx", 0o600);
|
|
121
|
-
}
|
|
122
|
-
throw statError;
|
|
123
|
-
}
|
|
124
|
-
if (Date.now() - modifiedAt <= STALE_LEDGER_LOCK_MS) throw error;
|
|
125
|
-
|
|
126
|
-
// Rename, rather than unlink, so two processes cannot both decide that the
|
|
127
|
-
// same stale lock is theirs to remove. Only one rename can win.
|
|
128
|
-
const stalePath = `${lockPath}.stale.${pid}.${Date.now()}`;
|
|
129
|
-
try {
|
|
130
|
-
renameSync(lockPath, stalePath);
|
|
131
|
-
} catch (renameError) {
|
|
132
|
-
if ((renameError as NodeJS.ErrnoException).code === "ENOENT") {
|
|
133
|
-
return openSync(lockPath, "wx", 0o600);
|
|
134
|
-
}
|
|
135
|
-
throw renameError;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
return openSync(lockPath, "wx", 0o600);
|
|
139
|
-
} finally {
|
|
140
|
-
try {
|
|
141
|
-
unlinkSync(stalePath);
|
|
142
|
-
} catch {
|
|
143
|
-
// The stale lock has already been displaced; cleanup is best effort.
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function withLedgerLock<T>(path: string, operation: () => T): T {
|
|
150
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
151
|
-
const lockPath = `${path}.lock`;
|
|
152
|
-
const descriptor = openLedgerLock(lockPath);
|
|
153
|
-
try {
|
|
154
|
-
return operation();
|
|
155
|
-
} finally {
|
|
156
|
-
closeSync(descriptor);
|
|
157
|
-
try {
|
|
158
|
-
unlinkSync(lockPath);
|
|
159
|
-
} catch {
|
|
160
|
-
// The operation's result is authoritative; lock cleanup is best effort.
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Callers hold the ledger lock, whose acquisition already created the directory. */
|
|
166
|
-
function writeStandingApprovals(path: string, records: readonly StandingApprovalRecord[]): void {
|
|
167
|
-
const temporary = `${path}.${pid}.tmp`;
|
|
168
|
-
try {
|
|
169
|
-
writeFileSync(
|
|
170
|
-
temporary,
|
|
171
|
-
records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "",
|
|
172
|
-
{ encoding: "utf8", mode: 0o600 },
|
|
173
|
-
);
|
|
174
|
-
chmodSync(temporary, 0o600);
|
|
175
|
-
renameSync(temporary, path);
|
|
176
|
-
chmodSync(path, 0o600);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
try {
|
|
179
|
-
unlinkSync(temporary);
|
|
180
|
-
} catch {
|
|
181
|
-
// The temporary file may not have been created.
|
|
182
|
-
}
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function appendStandingApproval(
|
|
188
|
-
path: string,
|
|
189
|
-
record: StandingApprovalRecord,
|
|
190
|
-
): { evicted: number } {
|
|
191
|
-
return withLedgerLock(path, () => {
|
|
192
|
-
// Re-granting the same command refreshes one approval instead of inflating
|
|
193
|
-
// the ledger and every review envelope that consumes it.
|
|
194
|
-
const records = readStandingApprovals(path)
|
|
195
|
-
.filter((existing) => existing.gate.group !== record.gate.group || existing.command !== record.command);
|
|
196
|
-
records.push(record);
|
|
197
|
-
const evicted = Math.max(0, records.length - STANDING_APPROVAL_LIMIT);
|
|
198
|
-
writeStandingApprovals(path, records.slice(evicted));
|
|
199
|
-
return { evicted };
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function standingApprovalToPermissionOverride(
|
|
204
|
-
record: StandingApprovalRecord,
|
|
205
|
-
seq: number,
|
|
206
|
-
anchorKey: string = OVERRIDE_ANCHOR_START,
|
|
207
|
-
): PermissionOverride {
|
|
208
|
-
return {
|
|
209
|
-
seq,
|
|
210
|
-
anchorKey,
|
|
211
|
-
gateLabel: record.gate.label,
|
|
212
|
-
command: record.command,
|
|
213
|
-
reviewerReason: record.reason,
|
|
214
|
-
choice: "allow_unnecessary",
|
|
215
|
-
standing: {
|
|
216
|
-
grantedAt: record.ts,
|
|
217
|
-
project: record.project,
|
|
218
|
-
gateGroup: record.gate.group,
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Session-start projection: ledger records precede this session's evidence. */
|
|
224
|
-
export function standingApprovalsToPermissionOverrides(
|
|
225
|
-
records: readonly StandingApprovalRecord[],
|
|
226
|
-
): PermissionOverride[] {
|
|
227
|
-
return records.map((record, index) => standingApprovalToPermissionOverride(record, index - records.length));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** One prompt selection atomically writes the ledger and yields live evidence. */
|
|
231
|
-
export function grantStandingApproval(
|
|
232
|
-
path: string,
|
|
233
|
-
input: StandingApprovalInput,
|
|
234
|
-
seq: number,
|
|
235
|
-
anchorKey?: string,
|
|
236
|
-
): { record: StandingApprovalRecord; override: PermissionOverride; evicted: number } {
|
|
237
|
-
const record = buildStandingApprovalRecord(input);
|
|
238
|
-
const { evicted } = appendStandingApproval(path, record);
|
|
239
|
-
return {
|
|
240
|
-
record,
|
|
241
|
-
override: standingApprovalToPermissionOverride(record, seq, anchorKey),
|
|
242
|
-
evicted,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function sameRecord(left: StandingApprovalRecord, right: StandingApprovalRecord): boolean {
|
|
247
|
-
return left.ts === right.ts
|
|
248
|
-
&& left.gate.label === right.gate.label
|
|
249
|
-
&& left.gate.group === right.gate.group
|
|
250
|
-
&& left.command === right.command
|
|
251
|
-
&& left.scope === right.scope
|
|
252
|
-
&& left.project === right.project
|
|
253
|
-
&& left.reason === right.reason;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Remove one selected record and preserve 0600 on the rewritten ledger. */
|
|
257
|
-
export function revokeStandingApproval(path: string, selected: StandingApprovalRecord): boolean {
|
|
258
|
-
return withLedgerLock(path, () => {
|
|
259
|
-
const records = readStandingApprovals(path);
|
|
260
|
-
const index = records.findIndex((record) => sameRecord(record, selected));
|
|
261
|
-
if (index < 0) return false;
|
|
262
|
-
records.splice(index, 1);
|
|
263
|
-
writeStandingApprovals(path, records);
|
|
264
|
-
return true;
|
|
265
|
-
});
|
|
266
|
-
}
|