@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/session-overrides.ts
CHANGED
|
@@ -1,87 +1,53 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
2
|
-
import type { AutoPermissionsConfig } from "./config.js";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { PromptChoiceClassification } from "./evaluation-log.js";
|
|
4
|
-
import type {
|
|
5
|
-
import type
|
|
3
|
+
import type { Gate } from "./gates.js";
|
|
4
|
+
import { PERMISSION_OVERRIDE_CHOICES, type PermissionOverride } from "./override-evidence.js";
|
|
6
5
|
import type { DenialSummary } from "./settings-menu.js";
|
|
7
|
-
import {
|
|
8
|
-
grantStandingApproval,
|
|
9
|
-
readStandingApprovals,
|
|
10
|
-
STANDING_APPROVAL_LIMIT,
|
|
11
|
-
standingApprovalsToPermissionOverrides,
|
|
12
|
-
} from "./standing-overrides.js";
|
|
13
6
|
|
|
14
7
|
/**
|
|
15
|
-
* Overrides persist as custom session entries
|
|
8
|
+
* Overrides persist as custom session entries,
|
|
16
9
|
* so a resumed session keeps the user's prompt decisions instead of
|
|
17
10
|
* forgetting every allow and standing block constraint.
|
|
18
11
|
*/
|
|
19
12
|
const OVERRIDES_ENTRY_TYPE = "auto-permissions-overrides";
|
|
20
13
|
|
|
21
14
|
export interface SessionOverrides {
|
|
22
|
-
/** Commands a `request_override` grant cleared for the rest of this session. */
|
|
23
|
-
readonly allowedConventionCommands: ReadonlySet<string>;
|
|
24
15
|
/** The override records, live, for merging into reviewer evidence. */
|
|
25
16
|
list(): readonly PermissionOverride[];
|
|
26
|
-
allowConvention(command: string): void;
|
|
27
|
-
activateOverrideTool(): void;
|
|
28
|
-
reconcileOverrideTool(): void;
|
|
29
|
-
persist(): void;
|
|
30
17
|
restore(branch: readonly unknown[]): void;
|
|
31
|
-
removeStanding(matches: (override: PermissionOverride) => boolean): void;
|
|
32
|
-
loadStanding(config: AutoPermissionsConfig, ctx: ExtensionContext): void;
|
|
33
18
|
recordPromptDecision(
|
|
34
|
-
|
|
19
|
+
gate: Pick<Gate, "label">,
|
|
20
|
+
command: string,
|
|
35
21
|
classification: PromptChoiceClassification,
|
|
36
22
|
detail: string,
|
|
37
23
|
anchorKey: string | undefined,
|
|
38
24
|
): void;
|
|
39
25
|
allowRetry(denial: DenialSummary, anchorKey: string | undefined): void;
|
|
40
|
-
resetForSession(): void;
|
|
41
26
|
}
|
|
42
27
|
|
|
43
28
|
/**
|
|
44
|
-
* The user's own permission decisions for this session: the
|
|
45
|
-
*
|
|
46
|
-
* guardian is shown as user-source evidence.
|
|
29
|
+
* The user's own permission decisions for this session: the override records
|
|
30
|
+
* the guardian is shown as user-source evidence.
|
|
47
31
|
*
|
|
48
|
-
* One owner, because the pieces are one fact seen
|
|
49
|
-
* record
|
|
50
|
-
* approvals) the cross-project ledger.
|
|
32
|
+
* One owner, because the pieces are one fact seen two ways — the in-memory
|
|
33
|
+
* record and the session entry that survives a resume.
|
|
51
34
|
*/
|
|
52
35
|
export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
53
|
-
const allowedConventionCommands = new Set<string>();
|
|
54
36
|
const permissionOverrides: PermissionOverride[] = [];
|
|
55
37
|
let overrideSeq = 0;
|
|
56
|
-
let standingApprovalCapNotified = false;
|
|
57
|
-
let overrideToolActivated = false;
|
|
58
|
-
|
|
59
|
-
function reconcileOverrideTool(): void {
|
|
60
|
-
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
61
|
-
const active = pi.getActiveTools();
|
|
62
|
-
const present = active.includes("request_override");
|
|
63
|
-
if (overrideToolActivated === present) return;
|
|
64
|
-
pi.setActiveTools(
|
|
65
|
-
overrideToolActivated
|
|
66
|
-
? [...active, "request_override"]
|
|
67
|
-
: active.filter((name) => name !== "request_override"),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
38
|
|
|
71
39
|
function persist(): void {
|
|
72
40
|
try {
|
|
73
41
|
pi.appendEntry(OVERRIDES_ENTRY_TYPE, {
|
|
74
42
|
seq: overrideSeq,
|
|
75
|
-
overrides: permissionOverrides
|
|
76
|
-
.filter((override) => !override.standing)
|
|
77
|
-
.map((override) => ({ ...override })),
|
|
43
|
+
overrides: permissionOverrides.map((override) => ({ ...override })),
|
|
78
44
|
});
|
|
79
45
|
} catch {
|
|
80
46
|
// Persistence is best-effort; the in-memory records still apply now.
|
|
81
47
|
}
|
|
82
48
|
}
|
|
83
49
|
|
|
84
|
-
/** Fail-open
|
|
50
|
+
/** Fail-open: unreadable state means no overrides. */
|
|
85
51
|
function restoreOverrides(entries: readonly unknown[]): void {
|
|
86
52
|
permissionOverrides.length = 0;
|
|
87
53
|
overrideSeq = 0;
|
|
@@ -98,7 +64,7 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
|
98
64
|
|| typeof candidate.gateLabel !== "string"
|
|
99
65
|
|| typeof candidate.command !== "string"
|
|
100
66
|
|| typeof candidate.reviewerReason !== "string"
|
|
101
|
-
|| !
|
|
67
|
+
|| !PERMISSION_OVERRIDE_CHOICES.includes(candidate.choice)
|
|
102
68
|
) {
|
|
103
69
|
continue;
|
|
104
70
|
}
|
|
@@ -119,52 +85,24 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
|
119
85
|
}
|
|
120
86
|
}
|
|
121
87
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
88
|
+
function addOverride(
|
|
89
|
+
gateLabel: string,
|
|
90
|
+
command: string,
|
|
91
|
+
reviewerReason: string,
|
|
92
|
+
choice: PermissionOverride["choice"],
|
|
93
|
+
anchorKey: string | undefined,
|
|
94
|
+
): void {
|
|
95
|
+
permissionOverrides.push({ seq: overrideSeq++, anchorKey, gateLabel, command, reviewerReason, choice });
|
|
96
|
+
persist();
|
|
126
97
|
}
|
|
127
98
|
|
|
128
99
|
return {
|
|
129
|
-
allowedConventionCommands,
|
|
130
100
|
list: () => permissionOverrides,
|
|
131
|
-
allowConvention(command: string): void {
|
|
132
|
-
allowedConventionCommands.add(command);
|
|
133
|
-
},
|
|
134
|
-
reconcileOverrideTool,
|
|
135
|
-
activateOverrideTool(): void {
|
|
136
|
-
overrideToolActivated = true;
|
|
137
|
-
reconcileOverrideTool();
|
|
138
|
-
},
|
|
139
|
-
persist,
|
|
140
|
-
removeStanding,
|
|
141
101
|
|
|
142
102
|
restore(branch: readonly unknown[]): void {
|
|
143
103
|
// Restore prompt decisions from the branch: a resumed session keeps its
|
|
144
|
-
// allows and standing block constraints
|
|
104
|
+
// allows and standing block constraints.
|
|
145
105
|
restoreOverrides(branch);
|
|
146
|
-
for (const entry of branch as Array<{
|
|
147
|
-
type?: string;
|
|
148
|
-
message?: { role?: string; toolName?: string; details?: unknown };
|
|
149
|
-
}>) {
|
|
150
|
-
if (entry.type !== "message" || entry.message?.role !== "toolResult") continue;
|
|
151
|
-
if (entry.message.toolName !== "request_override") continue;
|
|
152
|
-
const details = entry.message.details as { success?: boolean; command?: string } | undefined;
|
|
153
|
-
if (details?.success && details.command) allowedConventionCommands.add(details.command);
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
/** Replace only ledger-backed evidence; session prompt decisions stay put. */
|
|
158
|
-
loadStanding(config: AutoPermissionsConfig, ctx: ExtensionContext): void {
|
|
159
|
-
removeStanding(() => true);
|
|
160
|
-
if (!config.standingApprovals.enabled) return;
|
|
161
|
-
try {
|
|
162
|
-
const records = readStandingApprovals(config.standingApprovals.path);
|
|
163
|
-
permissionOverrides.push(...standingApprovalsToPermissionOverrides(records));
|
|
164
|
-
} catch (error) {
|
|
165
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
-
ctx.ui.notify(`Could not load standing Auto Permissions approvals: ${message}`, "warning");
|
|
167
|
-
}
|
|
168
106
|
},
|
|
169
107
|
|
|
170
108
|
/**
|
|
@@ -173,56 +111,15 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
|
173
111
|
* a guardian judgment, which is the one case this may be called for.
|
|
174
112
|
*/
|
|
175
113
|
recordPromptDecision(
|
|
176
|
-
|
|
114
|
+
gate: Pick<Gate, "label">,
|
|
115
|
+
command: string,
|
|
177
116
|
classification: PromptChoiceClassification,
|
|
178
117
|
detail: string,
|
|
179
118
|
anchorKey: string | undefined,
|
|
180
119
|
): void {
|
|
181
|
-
const { ctx, config, gate, command } = scope;
|
|
182
120
|
const overrideChoice = classification.userChoice ?? (classification.allowsExecution ? "allow" as const : undefined);
|
|
183
121
|
if (!overrideChoice) return;
|
|
184
|
-
|
|
185
|
-
if (classification.standingApproval && config.standingApprovals.enabled) {
|
|
186
|
-
try {
|
|
187
|
-
const { evicted, override } = grantStandingApproval(
|
|
188
|
-
config.standingApprovals.path,
|
|
189
|
-
{
|
|
190
|
-
gate: { label: gate.label, group: gate.group },
|
|
191
|
-
command,
|
|
192
|
-
project: ctx.cwd,
|
|
193
|
-
reason: detail,
|
|
194
|
-
},
|
|
195
|
-
overrideSeq++,
|
|
196
|
-
anchorKey,
|
|
197
|
-
);
|
|
198
|
-
removeStanding(
|
|
199
|
-
(existing) => existing.standing?.gateGroup === gate.group && existing.command === command,
|
|
200
|
-
);
|
|
201
|
-
permissionOverrides.push(override);
|
|
202
|
-
recordedStanding = true;
|
|
203
|
-
if (evicted > 0 && !standingApprovalCapNotified) {
|
|
204
|
-
standingApprovalCapNotified = true;
|
|
205
|
-
ctx.ui.notify(
|
|
206
|
-
`Standing approvals reached ${STANDING_APPROVAL_LIMIT} entries; the oldest approval was removed.`,
|
|
207
|
-
"warning",
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
} catch (error) {
|
|
211
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
-
ctx.ui.notify(`Could not save standing approval: ${message}`, "warning");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (!recordedStanding) {
|
|
216
|
-
permissionOverrides.push({
|
|
217
|
-
seq: overrideSeq++,
|
|
218
|
-
anchorKey,
|
|
219
|
-
gateLabel: gate.label,
|
|
220
|
-
command,
|
|
221
|
-
reviewerReason: detail,
|
|
222
|
-
choice: overrideChoice,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
persist();
|
|
122
|
+
addOverride(gate.label, command, detail, overrideChoice, anchorKey);
|
|
226
123
|
},
|
|
227
124
|
|
|
228
125
|
/**
|
|
@@ -233,15 +130,7 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
|
233
130
|
* may retry. No new authorization pathway.
|
|
234
131
|
*/
|
|
235
132
|
allowRetry(denial: DenialSummary, anchorKey: string | undefined): void {
|
|
236
|
-
|
|
237
|
-
seq: overrideSeq++,
|
|
238
|
-
anchorKey,
|
|
239
|
-
gateLabel: denial.gateLabel,
|
|
240
|
-
command: denial.command,
|
|
241
|
-
reviewerReason: denial.reason,
|
|
242
|
-
choice: "allow",
|
|
243
|
-
});
|
|
244
|
-
persist();
|
|
133
|
+
addOverride(denial.gateLabel, denial.command, denial.reason, "allow", anchorKey);
|
|
245
134
|
try {
|
|
246
135
|
// The same channel a prompt note uses. This is a real user decision
|
|
247
136
|
// made in the menu, so a user message is honest provenance — and it
|
|
@@ -253,12 +142,5 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
|
|
|
253
142
|
// The override itself is already in force; the nudge is best-effort.
|
|
254
143
|
}
|
|
255
144
|
},
|
|
256
|
-
|
|
257
|
-
resetForSession(): void {
|
|
258
|
-
overrideToolActivated = false;
|
|
259
|
-
reconcileOverrideTool();
|
|
260
|
-
standingApprovalCapNotified = false;
|
|
261
|
-
allowedConventionCommands.clear();
|
|
262
|
-
},
|
|
263
145
|
};
|
|
264
146
|
}
|
package/settings-command.ts
CHANGED
|
@@ -18,18 +18,14 @@ import {
|
|
|
18
18
|
buildSettingItems,
|
|
19
19
|
createDenialsSubmenu,
|
|
20
20
|
createModelSubmenu,
|
|
21
|
-
createStandingApprovalsSubmenu,
|
|
22
21
|
createTimeoutSubmenu,
|
|
23
22
|
type DenialsSubmenuHost,
|
|
24
23
|
type DenialSummary,
|
|
25
|
-
type StandingApprovalsSubmenuHost,
|
|
26
|
-
type StandingApprovalSummary,
|
|
27
24
|
type MenuModel,
|
|
28
25
|
type ReviewerSettings,
|
|
29
26
|
type SettingChange,
|
|
30
27
|
type SubmenuHost,
|
|
31
28
|
} from "./settings-menu.js";
|
|
32
|
-
import { readStandingApprovals, revokeStandingApproval } from "./standing-overrides.js";
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
31
|
* What `/auto-permissions setup` says on the user's behalf. Named after the
|
|
@@ -75,7 +71,6 @@ export function registerSettingsCommand(
|
|
|
75
71
|
const path = autoPermissionsConfigPath();
|
|
76
72
|
let settings: ReviewerSettings;
|
|
77
73
|
let denialLog: AutoPermissionsConfig["denialLog"];
|
|
78
|
-
let standingApprovalsConfig: AutoPermissionsConfig["standingApprovals"];
|
|
79
74
|
try {
|
|
80
75
|
const config = loadAutoPermissionsConfig(path);
|
|
81
76
|
settings = {
|
|
@@ -84,7 +79,6 @@ export function registerSettingsCommand(
|
|
|
84
79
|
systemPromptSource: config.systemPromptSource,
|
|
85
80
|
};
|
|
86
81
|
denialLog = config.denialLog;
|
|
87
|
-
standingApprovalsConfig = config.standingApprovals;
|
|
88
82
|
} catch (error) {
|
|
89
83
|
// Editing a file we cannot validate is the one way this writer could
|
|
90
84
|
// quietly make things worse, so refuse rather than open.
|
|
@@ -142,34 +136,6 @@ export function registerSettingsCommand(
|
|
|
142
136
|
}
|
|
143
137
|
};
|
|
144
138
|
|
|
145
|
-
const standingApprovalSummaries = (): StandingApprovalSummary[] => {
|
|
146
|
-
if (!standingApprovalsConfig.enabled) return [];
|
|
147
|
-
try {
|
|
148
|
-
return readStandingApprovals(standingApprovalsConfig.path).map((record, index) => ({
|
|
149
|
-
id: `${index}:${record.ts}:${record.gate.label}`,
|
|
150
|
-
record,
|
|
151
|
-
}));
|
|
152
|
-
} catch {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const revokeApproval = (approval: StandingApprovalSummary): void => {
|
|
158
|
-
try {
|
|
159
|
-
if (!revokeStandingApproval(standingApprovalsConfig.path, approval.record)) {
|
|
160
|
-
ctx.ui.notify("That standing approval is no longer in the ledger.", "warning");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const config = loadAutoPermissionsConfig(path);
|
|
164
|
-
overrides.loadStanding(config, ctx);
|
|
165
|
-
reviewer.discardLineage();
|
|
166
|
-
ctx.ui.notify("Standing approval revoked.", "info");
|
|
167
|
-
} catch (error) {
|
|
168
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
-
ctx.ui.notify(`Could not revoke standing approval: ${message}`, "warning");
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
139
|
/**
|
|
174
140
|
* Closes the settings dialog. Assigned when `ctx.ui.custom` builds the
|
|
175
141
|
* component, so it is only ever called from inside the dialog it closes.
|
|
@@ -213,13 +179,6 @@ export function registerSettingsCommand(
|
|
|
213
179
|
selectTheme: getSelectListTheme(),
|
|
214
180
|
requestRender: () => tui.requestRender(),
|
|
215
181
|
};
|
|
216
|
-
const standingHost: StandingApprovalsSubmenuHost = {
|
|
217
|
-
standingApprovals: standingApprovalSummaries,
|
|
218
|
-
revokeStandingApproval: revokeApproval,
|
|
219
|
-
settingsTheme: getSettingsListTheme(),
|
|
220
|
-
selectTheme: getSelectListTheme(),
|
|
221
|
-
requestRender: () => tui.requestRender(),
|
|
222
|
-
};
|
|
223
182
|
const list = new SettingsList(
|
|
224
183
|
buildSettingItems(
|
|
225
184
|
settings,
|
|
@@ -227,13 +186,9 @@ export function registerSettingsCommand(
|
|
|
227
186
|
reviewerModel: createModelSubmenu(host),
|
|
228
187
|
timeout: createTimeoutSubmenu(host),
|
|
229
188
|
...(denialLog.enabled ? { recentDenials: createDenialsSubmenu(denialsHost) } : {}),
|
|
230
|
-
...(standingApprovalsConfig.enabled
|
|
231
|
-
? { standingApprovals: createStandingApprovalsSubmenu(standingHost) }
|
|
232
|
-
: {}),
|
|
233
189
|
},
|
|
234
190
|
undefined,
|
|
235
191
|
denialLog.enabled ? recentDenials().length : undefined,
|
|
236
|
-
standingApprovalsConfig.enabled ? standingApprovalSummaries().length : undefined,
|
|
237
192
|
),
|
|
238
193
|
10,
|
|
239
194
|
host.settingsTheme,
|
package/settings-menu.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Row model and pure edit logic behind `/auto-permissions`.
|
|
3
3
|
*
|
|
4
4
|
* Everything that decides *what* a row shows or what an edit means lives here,
|
|
5
|
-
* free of pi's runtime, so it is testable without a TUI.
|
|
6
|
-
* wiring: read config, render a SettingsList, persist through config-writer.
|
|
5
|
+
* free of pi's runtime, so it is testable without a TUI. settings-command.ts owns
|
|
6
|
+
* only the wiring: read config, render a SettingsList, persist through config-writer.
|
|
7
7
|
*/
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import {
|
|
@@ -24,10 +24,9 @@ import {
|
|
|
24
24
|
MIN_REVIEWER_TIMEOUT_MS,
|
|
25
25
|
REASONING_EFFORTS,
|
|
26
26
|
type ReasoningEffort,
|
|
27
|
+
type ReviewerConfig,
|
|
27
28
|
type SystemPromptSource,
|
|
28
29
|
} from "./config.js";
|
|
29
|
-
import type { ReviewerBlock } from "./config-writer.js";
|
|
30
|
-
import type { StandingApprovalRecord } from "./standing-overrides.js";
|
|
31
30
|
|
|
32
31
|
const ENABLED_ID = "enabled";
|
|
33
32
|
const REVIEWER_MODEL_ID = "reviewerModel";
|
|
@@ -35,7 +34,6 @@ const THINKING_LEVEL_ID = "reasoningEffort";
|
|
|
35
34
|
const TIMEOUT_ID = "timeoutMs";
|
|
36
35
|
const SYSTEM_PROMPT_ID = "systemPrompt";
|
|
37
36
|
export const RECENT_DENIALS_ID = "recentDenials";
|
|
38
|
-
export const STANDING_APPROVALS_ID = "standingApprovals";
|
|
39
37
|
|
|
40
38
|
export const ON = "on";
|
|
41
39
|
export const OFF = "off";
|
|
@@ -48,7 +46,7 @@ const NO_REVIEWER_MESSAGE = "Select a reviewer model first";
|
|
|
48
46
|
/** The slice of the config the menu edits, plus the read-only prompt source. */
|
|
49
47
|
export interface ReviewerSettings {
|
|
50
48
|
enabled: boolean;
|
|
51
|
-
reviewer?:
|
|
49
|
+
reviewer?: ReviewerConfig;
|
|
52
50
|
systemPromptSource: SystemPromptSource;
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -141,7 +139,6 @@ export interface SettingSubmenus {
|
|
|
141
139
|
reviewerModel?: SettingItem["submenu"];
|
|
142
140
|
timeout?: SettingItem["submenu"];
|
|
143
141
|
recentDenials?: SettingItem["submenu"];
|
|
144
|
-
standingApprovals?: SettingItem["submenu"];
|
|
145
142
|
}
|
|
146
143
|
|
|
147
144
|
/** Build the `/auto-permissions` rows for a settings snapshot. */
|
|
@@ -150,13 +147,12 @@ export function buildSettingItems(
|
|
|
150
147
|
submenus: SettingSubmenus = {},
|
|
151
148
|
home: string = homedir(),
|
|
152
149
|
denialCount?: number,
|
|
153
|
-
standingApprovalCount?: number,
|
|
154
150
|
): SettingItem[] {
|
|
155
151
|
return [
|
|
156
152
|
{
|
|
157
153
|
id: ENABLED_ID,
|
|
158
154
|
label: "Enabled",
|
|
159
|
-
description: "Review guarded commands. Off
|
|
155
|
+
description: "Review guarded commands. Off disables all gating, including deny rules.",
|
|
160
156
|
currentValue: toggleValue(settings.enabled),
|
|
161
157
|
values: TOGGLE_VALUES,
|
|
162
158
|
},
|
|
@@ -196,15 +192,6 @@ export function buildSettingItems(
|
|
|
196
192
|
submenu: submenus.recentDenials,
|
|
197
193
|
}]
|
|
198
194
|
: []),
|
|
199
|
-
...(submenus.standingApprovals
|
|
200
|
-
? [{
|
|
201
|
-
id: STANDING_APPROVALS_ID,
|
|
202
|
-
label: "Standing approvals",
|
|
203
|
-
description: "Comparable-command approvals shared across projects. Select one to revoke it.",
|
|
204
|
-
currentValue: standingApprovalCount === undefined ? "" : String(standingApprovalCount),
|
|
205
|
-
submenu: submenus.standingApprovals,
|
|
206
|
-
}]
|
|
207
|
-
: []),
|
|
208
195
|
];
|
|
209
196
|
}
|
|
210
197
|
|
|
@@ -568,120 +555,6 @@ export function createDenialsSubmenu(host: DenialsSubmenuHost): NonNullable<Sett
|
|
|
568
555
|
return (_currentValue, done) => new DenialsSubmenu(host, done);
|
|
569
556
|
}
|
|
570
557
|
|
|
571
|
-
export interface StandingApprovalSummary {
|
|
572
|
-
id: string;
|
|
573
|
-
record: StandingApprovalRecord;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
export interface StandingApprovalsSubmenuHost {
|
|
577
|
-
standingApprovals(): StandingApprovalSummary[];
|
|
578
|
-
revokeStandingApproval(approval: StandingApprovalSummary): void;
|
|
579
|
-
settingsTheme: SettingsListTheme;
|
|
580
|
-
selectTheme: SelectListTheme;
|
|
581
|
-
requestRender(): void;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
export function standingApprovalItem(approval: StandingApprovalSummary): SelectItem {
|
|
585
|
-
const command = approval.record.command.length > COMMAND_LABEL_MAX
|
|
586
|
-
? `${approval.record.command.slice(0, COMMAND_LABEL_MAX - 1)}…`
|
|
587
|
-
: approval.record.command;
|
|
588
|
-
return {
|
|
589
|
-
value: approval.id,
|
|
590
|
-
label: command,
|
|
591
|
-
description: `${approval.record.gate.label} · granted ${approval.record.ts.slice(0, 10)} · ${approval.record.project}`,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
class StandingApprovalsSubmenu implements Component {
|
|
596
|
-
private readonly approvals: StandingApprovalSummary[];
|
|
597
|
-
private list: SelectList;
|
|
598
|
-
private selected?: StandingApprovalSummary;
|
|
599
|
-
|
|
600
|
-
constructor(
|
|
601
|
-
private readonly host: StandingApprovalsSubmenuHost,
|
|
602
|
-
private readonly done: (value?: string) => void,
|
|
603
|
-
) {
|
|
604
|
-
this.approvals = host.standingApprovals();
|
|
605
|
-
this.list = this.buildApprovalList();
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private buildApprovalList(): SelectList {
|
|
609
|
-
const items = this.approvals.length
|
|
610
|
-
? this.approvals.map(standingApprovalItem)
|
|
611
|
-
: [{ value: "", label: "(no standing approvals)", description: "Comparable-command approvals will appear here." }];
|
|
612
|
-
const list = new SelectList(items, 10, this.host.selectTheme);
|
|
613
|
-
list.onCancel = () => this.done(undefined);
|
|
614
|
-
list.onSelect = (item) => {
|
|
615
|
-
const approval = this.approvals.find((candidate) => candidate.id === item.value);
|
|
616
|
-
if (!approval) {
|
|
617
|
-
this.done(undefined);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
this.selected = approval;
|
|
621
|
-
this.list = this.buildConfirmList();
|
|
622
|
-
this.host.requestRender();
|
|
623
|
-
};
|
|
624
|
-
return list;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
private buildConfirmList(): SelectList {
|
|
628
|
-
const list = new SelectList(
|
|
629
|
-
[
|
|
630
|
-
{
|
|
631
|
-
value: "revoke",
|
|
632
|
-
label: "Revoke",
|
|
633
|
-
description: "Removes this approval from the user-scoped standing ledger.",
|
|
634
|
-
},
|
|
635
|
-
{ value: "back", label: "Back" },
|
|
636
|
-
],
|
|
637
|
-
10,
|
|
638
|
-
this.host.selectTheme,
|
|
639
|
-
);
|
|
640
|
-
const back = () => {
|
|
641
|
-
this.selected = undefined;
|
|
642
|
-
this.list = this.buildApprovalList();
|
|
643
|
-
this.host.requestRender();
|
|
644
|
-
};
|
|
645
|
-
list.onCancel = back;
|
|
646
|
-
list.onSelect = (item) => {
|
|
647
|
-
if (item.value !== "revoke" || !this.selected) {
|
|
648
|
-
back();
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
this.host.revokeStandingApproval(this.selected);
|
|
652
|
-
this.done(undefined);
|
|
653
|
-
};
|
|
654
|
-
return list;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
invalidate(): void {
|
|
658
|
-
this.list.invalidate();
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
render(width: number): string[] {
|
|
662
|
-
const title = this.selected
|
|
663
|
-
? ` Revoke standing approval? ${truncateToWidth(this.selected.record.command, Math.max(1, width - 30))}`
|
|
664
|
-
: " Standing approvals";
|
|
665
|
-
return [
|
|
666
|
-
truncateToWidth(this.host.settingsTheme.hint(title), width),
|
|
667
|
-
"",
|
|
668
|
-
...this.list.render(width),
|
|
669
|
-
"",
|
|
670
|
-
truncateToWidth(this.host.settingsTheme.hint(" Enter to select · Esc to go back"), width),
|
|
671
|
-
];
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
handleInput(data: string): void {
|
|
675
|
-
this.list.handleInput(data);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
export function createStandingApprovalsSubmenu(
|
|
680
|
-
host: StandingApprovalsSubmenuHost,
|
|
681
|
-
): NonNullable<SettingItem["submenu"]> {
|
|
682
|
-
return (_currentValue, done) => new StandingApprovalsSubmenu(host, done);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
558
|
export function createTimeoutSubmenu(host: SubmenuHost): NonNullable<SettingItem["submenu"]> {
|
|
686
559
|
return (currentValue, done) =>
|
|
687
560
|
new PromptComponent(
|
package/usage-log.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
/** Rotate once the sidecar grows past this size; one previous generation is kept. */
|
|
6
|
-
export const USAGE_LOG_ROTATE_BYTES = 16 * 1024 * 1024;
|
|
2
|
+
import { appendJsonlRecord, SIDECAR_ROTATE_BYTES } from "./jsonl-sidecar.js";
|
|
7
3
|
|
|
8
4
|
interface UsageLogTotals {
|
|
9
5
|
input: number;
|
|
@@ -55,14 +51,13 @@ export function buildUsageLogRecord(
|
|
|
55
51
|
model: string,
|
|
56
52
|
usage: unknown,
|
|
57
53
|
label = "guardian",
|
|
58
|
-
source = "auto-permissions",
|
|
59
54
|
subagent = false,
|
|
60
55
|
): UsageLogRecord {
|
|
61
56
|
return {
|
|
62
57
|
v: 1,
|
|
63
58
|
id: randomUUID(),
|
|
64
59
|
ts: new Date().toISOString(),
|
|
65
|
-
source,
|
|
60
|
+
source: "auto-permissions",
|
|
66
61
|
label,
|
|
67
62
|
provider,
|
|
68
63
|
model,
|
|
@@ -71,18 +66,6 @@ export function buildUsageLogRecord(
|
|
|
71
66
|
};
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
function rotateIfLarge(path: string): void {
|
|
75
|
-
try {
|
|
76
|
-
if (statSync(path).size < USAGE_LOG_ROTATE_BYTES) return;
|
|
77
|
-
renameSync(path, `${path}.1`);
|
|
78
|
-
} catch {
|
|
79
|
-
// A missing or unrotatable sidecar simply keeps appending.
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
69
|
export function appendUsageRecord(path: string, record: UsageLogRecord): void {
|
|
84
|
-
|
|
85
|
-
rotateIfLarge(path);
|
|
86
|
-
appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
87
|
-
chmodSync(path, 0o600);
|
|
70
|
+
appendJsonlRecord(path, record, SIDECAR_ROTATE_BYTES);
|
|
88
71
|
}
|
package/widget-status.ts
CHANGED
|
@@ -18,8 +18,6 @@ interface ReviewStatusFrame {
|
|
|
18
18
|
glyph: string;
|
|
19
19
|
label: string;
|
|
20
20
|
tone: ReviewStatusTone;
|
|
21
|
-
/** True once no further frames are needed (state is static). */
|
|
22
|
-
done: boolean;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
export const WAITING_FRAMES = ["✶", "✸", "✻", "✽"] as const;
|
|
@@ -41,23 +39,21 @@ export function reviewStatusFrame(
|
|
|
41
39
|
glyph: "⋯",
|
|
42
40
|
label: "queued behind another review",
|
|
43
41
|
tone: "muted",
|
|
44
|
-
done: true,
|
|
45
42
|
};
|
|
46
43
|
case "waiting":
|
|
47
44
|
return {
|
|
48
45
|
glyph: WAITING_FRAMES[index % WAITING_FRAMES.length],
|
|
49
46
|
label: `waiting for ${reviewer}`,
|
|
50
47
|
tone: "warning",
|
|
51
|
-
done: false,
|
|
52
48
|
};
|
|
53
49
|
case "approved":
|
|
54
|
-
return { glyph: "✓", label: "approved", tone: "success"
|
|
50
|
+
return { glyph: "✓", label: "approved", tone: "success" };
|
|
55
51
|
case "revise":
|
|
56
|
-
return { glyph: "↻", label: "revision requested", tone: "warning"
|
|
52
|
+
return { glyph: "↻", label: "revision requested", tone: "warning" };
|
|
57
53
|
case "ask_user":
|
|
58
|
-
return { glyph: "?", label: "waiting for your approval", tone: "accent"
|
|
54
|
+
return { glyph: "?", label: "waiting for your approval", tone: "accent" };
|
|
59
55
|
case "blocked":
|
|
60
|
-
return { glyph: "✗", label: "blocked", tone: "error"
|
|
56
|
+
return { glyph: "✗", label: "blocked", tone: "error" };
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
59
|
|
|
@@ -74,15 +70,6 @@ export interface ReviewLinePalette {
|
|
|
74
70
|
error: (text: string) => string;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
export const PLAIN_PALETTE: ReviewLinePalette = {
|
|
78
|
-
header: (text) => text,
|
|
79
|
-
muted: (text) => text,
|
|
80
|
-
warning: (text) => text,
|
|
81
|
-
success: (text) => text,
|
|
82
|
-
accent: (text) => text,
|
|
83
|
-
error: (text) => text,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
73
|
/**
|
|
87
74
|
* Render the widget content: one status line, plus a dim detail line only
|
|
88
75
|
* when a guardian reason is present. The command is intentionally omitted —
|