@yagni-app/code-staging 0.3.0-staging.1067.1 → 0.3.0-staging.1073.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.
- package/dist/extension/config.d.ts +11 -1
- package/dist/extension/config.js +1 -1
- package/dist/extension/execPolicy.d.ts +81 -0
- package/dist/extension/execPolicy.js +453 -0
- package/dist/extension/guardian.d.ts +113 -0
- package/dist/extension/guardian.js +186 -0
- package/dist/extension/index.d.ts +8 -4
- package/dist/extension/index.js +35 -3
- package/dist/extension/permission.d.ts +48 -6
- package/dist/extension/permission.js +233 -24
- package/dist/extension/pipeline/personas.js +22 -0
- package/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +17 -0
- package/package.json +2 -2
|
@@ -72,13 +72,23 @@ export interface FetchCatalogOptions {
|
|
|
72
72
|
getToken: () => string | undefined;
|
|
73
73
|
fetchImpl?: typeof fetch;
|
|
74
74
|
}
|
|
75
|
+
/** The startup catalog response from GET /api/yagni-code/models. */
|
|
76
|
+
export interface CatalogResult {
|
|
77
|
+
models: ModelEntry[];
|
|
78
|
+
/**
|
|
79
|
+
* Per-workspace Guardian kill switch (yagni_code.guardian). Fail-safe:
|
|
80
|
+
* only an explicit `false` from the backend disables the Guardian — a
|
|
81
|
+
* missing field (older backend) reads as enabled.
|
|
82
|
+
*/
|
|
83
|
+
guardianEnabled: boolean;
|
|
84
|
+
}
|
|
75
85
|
/**
|
|
76
86
|
* Fetch the YAGNI model catalog at startup.
|
|
77
87
|
*
|
|
78
88
|
* @throws an actionable Error (mentioning `yagni login`) on any non-2xx
|
|
79
89
|
* response so the launcher can surface a clear re-authentication prompt.
|
|
80
90
|
*/
|
|
81
|
-
export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<
|
|
91
|
+
export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<CatalogResult>;
|
|
82
92
|
/** The startup company brief returned by GET /api/yagni-code/context. */
|
|
83
93
|
export interface ContextBrief {
|
|
84
94
|
brief: string;
|
package/dist/extension/config.js
CHANGED
|
@@ -85,7 +85,7 @@ export async function fetchCatalog(opts) {
|
|
|
85
85
|
throw new Error(`Failed to fetch YAGNI model catalog (HTTP ${res.status}). Run \`yagni login\` to re-authenticate.`);
|
|
86
86
|
}
|
|
87
87
|
const data = (await res.json());
|
|
88
|
-
return data.models;
|
|
88
|
+
return { models: data.models, guardianEnabled: data.guardianEnabled !== false };
|
|
89
89
|
}
|
|
90
90
|
/** Shape-check for a caller label: mirrors the model proxy's own validation regex. */
|
|
91
91
|
const CALLER_LABEL_RE = /^[a-z0-9][a-z0-9:_.-]{0,63}$/i;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec policy engine — classifies bash commands via prefix rules + lightweight
|
|
3
|
+
* shell tokenization (YAG-504).
|
|
4
|
+
*
|
|
5
|
+
* Pure: no I/O, no network, no model. Loads at startup and classifies
|
|
6
|
+
* synchronously. The curated default set auto-allows read-only commands
|
|
7
|
+
* (ls, cat, rg, git status/log/diff), forbids destructive ones (rm -rf,
|
|
8
|
+
* git reset --hard, git push --force, pipe-to-shell), and prompts for the
|
|
9
|
+
* ambiguous middle band (npm install, git commit, curl, …).
|
|
10
|
+
*
|
|
11
|
+
* The `prompt` band is what the Guardian arbitrates — see guardian.ts.
|
|
12
|
+
*
|
|
13
|
+
* Tokenization is a lightweight inline parser — not shell-quote — because the
|
|
14
|
+
* extension is bundled into @yagni-app/code's dist (a file copy, not a real
|
|
15
|
+
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
16
|
+
* We only need: split on whitespace (respecting single/double quotes), detect
|
|
17
|
+
* control operators (|, &&, ||, ;), and flag shell constructs ($(...),
|
|
18
|
+
* backticks, redirects) that we can't statically analyze.
|
|
19
|
+
*/
|
|
20
|
+
export type TokenEntry = string | {
|
|
21
|
+
op: "pipe" | "and" | "or" | "semi" | "redirect" | "substitution";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Parse a shell command string into tokens and control operators.
|
|
25
|
+
*
|
|
26
|
+
* Handles:
|
|
27
|
+
* - Single and double quoted strings (preserves spaces inside)
|
|
28
|
+
* - Control operators: |, &&, ||, ;
|
|
29
|
+
* - Shell constructs we flag as unanalyzable: $(), backticks, >, <
|
|
30
|
+
*
|
|
31
|
+
* Does NOT handle: variable expansion, glob patterns, heredocs, nested
|
|
32
|
+
* subshells beyond the first level. Commands using those are classified
|
|
33
|
+
* as "prompt" (let the Guardian review).
|
|
34
|
+
*/
|
|
35
|
+
export declare function shellParse(command: string): TokenEntry[];
|
|
36
|
+
export type ExecDecision = "allow" | "prompt" | "forbidden";
|
|
37
|
+
export interface PrefixRule {
|
|
38
|
+
/** Ordered tokens; a string[] element means alternatives (any match). */
|
|
39
|
+
pattern: (string | string[])[];
|
|
40
|
+
decision: ExecDecision;
|
|
41
|
+
justification: string;
|
|
42
|
+
/**
|
|
43
|
+
* Escape hatch for allow rules whose command has a mutating flag: if any
|
|
44
|
+
* token AFTER the matched prefix equals one of these (or, for entries ending
|
|
45
|
+
* in "*", starts with the part before the star), the rule does NOT match and
|
|
46
|
+
* evaluation falls through to later rules (usually landing in the prompt
|
|
47
|
+
* band). Example: sed is read-only except with -i/--in-place.
|
|
48
|
+
*/
|
|
49
|
+
unlessTokens?: string[];
|
|
50
|
+
/** Positive test invocations (validated at load if present). */
|
|
51
|
+
match?: string[][];
|
|
52
|
+
/** Negative test invocations (validated at load if present). */
|
|
53
|
+
notMatch?: string[][];
|
|
54
|
+
}
|
|
55
|
+
export interface ExecPolicy {
|
|
56
|
+
rules: PrefixRule[];
|
|
57
|
+
}
|
|
58
|
+
export interface ExecClassification {
|
|
59
|
+
decision: ExecDecision;
|
|
60
|
+
justification: string;
|
|
61
|
+
matchedRule?: PrefixRule;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a command string into tokens using our lightweight tokenizer. Returns
|
|
65
|
+
* string tokens only (control operators and constructs are filtered out —
|
|
66
|
+
* detected separately).
|
|
67
|
+
*/
|
|
68
|
+
export declare function tokenize(command: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Classify a full bash command string against the exec policy.
|
|
71
|
+
*
|
|
72
|
+
* Compound commands (pipes, &&, ||, ;) are split into segments and each is
|
|
73
|
+
* classified independently. The strictest decision wins (forbidden > prompt >
|
|
74
|
+
* allow). Commands with shell constructs we can't parse (command substitution,
|
|
75
|
+
* redirects beyond pipe) are classified as prompt. Pipe-to-shell is always
|
|
76
|
+
* forbidden.
|
|
77
|
+
*/
|
|
78
|
+
export declare function classifyCommand(command: string, policy: ExecPolicy): ExecClassification;
|
|
79
|
+
/** Curated default rules — the shipped safety floor. */
|
|
80
|
+
export declare const DEFAULT_EXEC_POLICY: ExecPolicy;
|
|
81
|
+
//# sourceMappingURL=execPolicy.d.ts.map
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec policy engine — classifies bash commands via prefix rules + lightweight
|
|
3
|
+
* shell tokenization (YAG-504).
|
|
4
|
+
*
|
|
5
|
+
* Pure: no I/O, no network, no model. Loads at startup and classifies
|
|
6
|
+
* synchronously. The curated default set auto-allows read-only commands
|
|
7
|
+
* (ls, cat, rg, git status/log/diff), forbids destructive ones (rm -rf,
|
|
8
|
+
* git reset --hard, git push --force, pipe-to-shell), and prompts for the
|
|
9
|
+
* ambiguous middle band (npm install, git commit, curl, …).
|
|
10
|
+
*
|
|
11
|
+
* The `prompt` band is what the Guardian arbitrates — see guardian.ts.
|
|
12
|
+
*
|
|
13
|
+
* Tokenization is a lightweight inline parser — not shell-quote — because the
|
|
14
|
+
* extension is bundled into @yagni-app/code's dist (a file copy, not a real
|
|
15
|
+
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
16
|
+
* We only need: split on whitespace (respecting single/double quotes), detect
|
|
17
|
+
* control operators (|, &&, ||, ;), and flag shell constructs ($(...),
|
|
18
|
+
* backticks, redirects) that we can't statically analyze.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Parse a shell command string into tokens and control operators.
|
|
22
|
+
*
|
|
23
|
+
* Handles:
|
|
24
|
+
* - Single and double quoted strings (preserves spaces inside)
|
|
25
|
+
* - Control operators: |, &&, ||, ;
|
|
26
|
+
* - Shell constructs we flag as unanalyzable: $(), backticks, >, <
|
|
27
|
+
*
|
|
28
|
+
* Does NOT handle: variable expansion, glob patterns, heredocs, nested
|
|
29
|
+
* subshells beyond the first level. Commands using those are classified
|
|
30
|
+
* as "prompt" (let the Guardian review).
|
|
31
|
+
*/
|
|
32
|
+
export function shellParse(command) {
|
|
33
|
+
const tokens = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
let current = "";
|
|
36
|
+
let inSingle = false;
|
|
37
|
+
let inDouble = false;
|
|
38
|
+
let hasConstruct = false;
|
|
39
|
+
const pushCurrent = () => {
|
|
40
|
+
if (current.length > 0) {
|
|
41
|
+
tokens.push(current);
|
|
42
|
+
current = "";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
while (i < command.length) {
|
|
46
|
+
const ch = command[i];
|
|
47
|
+
if (inSingle) {
|
|
48
|
+
if (ch === "'") {
|
|
49
|
+
inSingle = false;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
current += ch;
|
|
53
|
+
}
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (inDouble) {
|
|
58
|
+
if (ch === '"') {
|
|
59
|
+
inDouble = false;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
current += ch;
|
|
63
|
+
}
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
switch (ch) {
|
|
68
|
+
case "'":
|
|
69
|
+
inSingle = true;
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
case '"':
|
|
73
|
+
inDouble = true;
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
case " ":
|
|
77
|
+
case "\t":
|
|
78
|
+
case "\n":
|
|
79
|
+
pushCurrent();
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
case "|":
|
|
83
|
+
if (command[i + 1] === "|") {
|
|
84
|
+
pushCurrent();
|
|
85
|
+
tokens.push({ op: "or" });
|
|
86
|
+
i += 2;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
pushCurrent();
|
|
90
|
+
tokens.push({ op: "pipe" });
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
case "&":
|
|
95
|
+
if (command[i + 1] === "&") {
|
|
96
|
+
pushCurrent();
|
|
97
|
+
tokens.push({ op: "and" });
|
|
98
|
+
i += 2;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Single & — background operator, treat as construct
|
|
102
|
+
hasConstruct = true;
|
|
103
|
+
current += ch;
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
case ";":
|
|
108
|
+
pushCurrent();
|
|
109
|
+
tokens.push({ op: "semi" });
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
case ">":
|
|
113
|
+
case "<":
|
|
114
|
+
pushCurrent();
|
|
115
|
+
tokens.push({ op: "redirect" });
|
|
116
|
+
hasConstruct = true;
|
|
117
|
+
// Skip the operator char(s) and any following space
|
|
118
|
+
i++;
|
|
119
|
+
if (command[i] === ch)
|
|
120
|
+
i++; // >> or <<
|
|
121
|
+
while (command[i] === " " || command[i] === "\t")
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
case "$":
|
|
125
|
+
if (command[i + 1] === "(") {
|
|
126
|
+
pushCurrent();
|
|
127
|
+
tokens.push({ op: "substitution" });
|
|
128
|
+
hasConstruct = true;
|
|
129
|
+
// Skip until matching )
|
|
130
|
+
i += 2;
|
|
131
|
+
let depth = 1;
|
|
132
|
+
while (i < command.length && depth > 0) {
|
|
133
|
+
if (command[i] === "(")
|
|
134
|
+
depth++;
|
|
135
|
+
if (command[i] === ")")
|
|
136
|
+
depth--;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
current += ch;
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
case "`":
|
|
145
|
+
pushCurrent();
|
|
146
|
+
tokens.push({ op: "substitution" });
|
|
147
|
+
hasConstruct = true;
|
|
148
|
+
i++;
|
|
149
|
+
while (i < command.length && command[i] !== "`")
|
|
150
|
+
i++;
|
|
151
|
+
if (i < command.length)
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
default:
|
|
155
|
+
current += ch;
|
|
156
|
+
i++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
pushCurrent();
|
|
161
|
+
// If we detected constructs but didn't emit them as operator tokens
|
|
162
|
+
// (e.g. background &), surface that via a substitution token.
|
|
163
|
+
if (hasConstruct && !tokens.some((t) => typeof t === "object")) {
|
|
164
|
+
tokens.push({ op: "substitution" });
|
|
165
|
+
}
|
|
166
|
+
return tokens;
|
|
167
|
+
}
|
|
168
|
+
/** Interpreters that, when piped into, indicate code execution — always forbidden. */
|
|
169
|
+
const PIPE_TO_SHELL = new Set([
|
|
170
|
+
"sh", "bash", "zsh", "fish", "nc", "ncat", "socat",
|
|
171
|
+
"python", "python3", "perl", "ruby", "node",
|
|
172
|
+
]);
|
|
173
|
+
/**
|
|
174
|
+
* Parse a command string into tokens using our lightweight tokenizer. Returns
|
|
175
|
+
* string tokens only (control operators and constructs are filtered out —
|
|
176
|
+
* detected separately).
|
|
177
|
+
*/
|
|
178
|
+
export function tokenize(command) {
|
|
179
|
+
return shellParse(command).filter((t) => typeof t === "string");
|
|
180
|
+
}
|
|
181
|
+
/** Operator tokens we can safely split on (compound command segments). */
|
|
182
|
+
const SPLIT_OPS = new Set(["pipe", "and", "or", "semi"]);
|
|
183
|
+
/**
|
|
184
|
+
* Detect whether the command uses shell constructs we can't statically classify
|
|
185
|
+
* (command substitution, redirects, etc.) — anything that is NOT a splittable
|
|
186
|
+
* operator (pipe, and, or, semi).
|
|
187
|
+
*/
|
|
188
|
+
function hasUnhandledConstructs(command) {
|
|
189
|
+
return shellParse(command).some((t) => typeof t === "object" && "op" in t && !SPLIT_OPS.has(t.op));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Split a command into segments at control operators (|, &&, ||, ;).
|
|
193
|
+
* Returns the raw string of each segment.
|
|
194
|
+
*/
|
|
195
|
+
function splitSegments(command) {
|
|
196
|
+
const tokens = shellParse(command);
|
|
197
|
+
const segments = [];
|
|
198
|
+
let current = [];
|
|
199
|
+
for (const t of tokens) {
|
|
200
|
+
if (typeof t === "object") {
|
|
201
|
+
if (SPLIT_OPS.has(t.op)) {
|
|
202
|
+
if (current.length > 0)
|
|
203
|
+
segments.push(current.join(" "));
|
|
204
|
+
current = [];
|
|
205
|
+
}
|
|
206
|
+
// Non-splittable operators (redirect, substitution) are already
|
|
207
|
+
// handled by hasUnhandledConstructs above — we won't reach splitSegments.
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
current.push(t);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (current.length > 0)
|
|
214
|
+
segments.push(current.join(" "));
|
|
215
|
+
return segments;
|
|
216
|
+
}
|
|
217
|
+
/** Check if any segment pipes into a known shell/network interpreter. */
|
|
218
|
+
function isPipeToShell(command) {
|
|
219
|
+
const tokens = shellParse(command);
|
|
220
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
221
|
+
const t = tokens[i];
|
|
222
|
+
if (typeof t === "object" && t.op === "pipe") {
|
|
223
|
+
const next = tokens[i + 1];
|
|
224
|
+
if (typeof next === "string" && PIPE_TO_SHELL.has(next))
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
/** Match a token array against a prefix rule's pattern. */
|
|
231
|
+
function matchRule(tokens, rule) {
|
|
232
|
+
if (tokens.length < rule.pattern.length)
|
|
233
|
+
return false;
|
|
234
|
+
for (let i = 0; i < rule.pattern.length; i++) {
|
|
235
|
+
const pat = rule.pattern[i];
|
|
236
|
+
const tok = tokens[i];
|
|
237
|
+
if (typeof pat === "string") {
|
|
238
|
+
if (pat !== tok)
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
if (!pat.includes(tok))
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (rule.unlessTokens) {
|
|
247
|
+
for (const tok of tokens.slice(rule.pattern.length)) {
|
|
248
|
+
for (const unless of rule.unlessTokens) {
|
|
249
|
+
const matches = unless.endsWith("*")
|
|
250
|
+
? tok.startsWith(unless.slice(0, -1))
|
|
251
|
+
: tok === unless;
|
|
252
|
+
if (matches)
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
/** Classify a single command segment (no shell constructs). */
|
|
260
|
+
function classifySegment(segment, rules) {
|
|
261
|
+
const tokens = tokenize(segment);
|
|
262
|
+
if (tokens.length === 0) {
|
|
263
|
+
return { decision: "prompt", justification: "empty command segment" };
|
|
264
|
+
}
|
|
265
|
+
// First match wins (rules are ordered; more specific rules come first).
|
|
266
|
+
for (const rule of rules) {
|
|
267
|
+
if (matchRule(tokens, rule)) {
|
|
268
|
+
return { decision: rule.decision, justification: rule.justification, matchedRule: rule };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// No rule matched → prompt (fail toward review, not toward allow)
|
|
272
|
+
return { decision: "prompt", justification: `no policy rule matched for "${tokens[0]}"` };
|
|
273
|
+
}
|
|
274
|
+
/** Decision severity: forbidden > prompt > allow. */
|
|
275
|
+
const SEVERITY = { allow: 0, prompt: 1, forbidden: 2 };
|
|
276
|
+
/**
|
|
277
|
+
* Classify a full bash command string against the exec policy.
|
|
278
|
+
*
|
|
279
|
+
* Compound commands (pipes, &&, ||, ;) are split into segments and each is
|
|
280
|
+
* classified independently. The strictest decision wins (forbidden > prompt >
|
|
281
|
+
* allow). Commands with shell constructs we can't parse (command substitution,
|
|
282
|
+
* redirects beyond pipe) are classified as prompt. Pipe-to-shell is always
|
|
283
|
+
* forbidden.
|
|
284
|
+
*/
|
|
285
|
+
export function classifyCommand(command, policy) {
|
|
286
|
+
// Pipe-to-shell is always forbidden regardless of other rules.
|
|
287
|
+
if (isPipeToShell(command)) {
|
|
288
|
+
return {
|
|
289
|
+
decision: "forbidden",
|
|
290
|
+
justification: "piping into a shell or network interpreter is forbidden",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// Command substitution, redirects, and other constructs we can't statically
|
|
294
|
+
// analyze → prompt (let the Guardian review). Only checked for constructs
|
|
295
|
+
// that are NOT splittable (|, &&, ||, ;) — those are handled below.
|
|
296
|
+
if (hasUnhandledConstructs(command)) {
|
|
297
|
+
return {
|
|
298
|
+
decision: "prompt",
|
|
299
|
+
justification: "command uses shell constructs (substitution/redirect) that cannot be statically analyzed",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const segments = splitSegments(command);
|
|
303
|
+
if (segments.length <= 1) {
|
|
304
|
+
return classifySegment(command, policy.rules);
|
|
305
|
+
}
|
|
306
|
+
// Compound command: classify each segment, take the strictest.
|
|
307
|
+
let result = { decision: "allow", justification: "all segments allowed" };
|
|
308
|
+
for (const seg of segments) {
|
|
309
|
+
const segResult = classifySegment(seg, policy.rules);
|
|
310
|
+
if (SEVERITY[segResult.decision] > SEVERITY[result.decision]) {
|
|
311
|
+
result = segResult;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
/** Curated default rules — the shipped safety floor. */
|
|
317
|
+
export const DEFAULT_EXEC_POLICY = {
|
|
318
|
+
rules: [
|
|
319
|
+
// --- forbidden: destructive commands (highest priority) ---
|
|
320
|
+
{
|
|
321
|
+
pattern: ["rm", ["-rf", "-fr", "-r", "-f", "--recursive", "--force"]],
|
|
322
|
+
decision: "forbidden",
|
|
323
|
+
justification: "recursive/forced deletion is destructive and irreversible",
|
|
324
|
+
},
|
|
325
|
+
{ pattern: ["rm", "-r"], decision: "forbidden", justification: "recursive deletion is destructive" },
|
|
326
|
+
{ pattern: ["rm", "-f"], decision: "forbidden", justification: "forced deletion bypasses prompts" },
|
|
327
|
+
{ pattern: ["rm", "--recursive"], decision: "forbidden", justification: "recursive deletion is destructive" },
|
|
328
|
+
{ pattern: ["rm", "--force"], decision: "forbidden", justification: "forced deletion bypasses prompts" },
|
|
329
|
+
{ pattern: ["git", "reset", "--hard"], decision: "forbidden", justification: "hard reset discards uncommitted changes irreversibly" },
|
|
330
|
+
{ pattern: ["git", "push", ["--force", "-f"]], decision: "forbidden", justification: "force-push rewrites shared history" },
|
|
331
|
+
{ pattern: ["git", "clean", ["-fd", "-df", "-f", "-d"]], decision: "forbidden", justification: "git clean removes untracked files irreversibly" },
|
|
332
|
+
{ pattern: ["git", "checkout", "--"], decision: "forbidden", justification: "discards working tree changes" },
|
|
333
|
+
{ pattern: ["chmod", "-R", "777"], decision: "forbidden", justification: "recursive world-writable permission change weakens security" },
|
|
334
|
+
{ pattern: ["chown", "-R"], decision: "forbidden", justification: "recursive ownership change" },
|
|
335
|
+
{ pattern: ["dd"], decision: "forbidden", justification: "low-level disk operations are destructive" },
|
|
336
|
+
{ pattern: ["mkfs"], decision: "forbidden", justification: "filesystem formatting is destructive" },
|
|
337
|
+
{ pattern: ["shutdown"], decision: "forbidden", justification: "system shutdown" },
|
|
338
|
+
{ pattern: ["reboot"], decision: "forbidden", justification: "system reboot" },
|
|
339
|
+
{ pattern: ["kill", "-9"], decision: "forbidden", justification: "force kill is destructive" },
|
|
340
|
+
{ pattern: ["kill", "-KILL"], decision: "forbidden", justification: "force kill is destructive" },
|
|
341
|
+
{ pattern: ["truncate"], decision: "forbidden", justification: "truncates files destructively" },
|
|
342
|
+
// --- allow: read-only commands ---
|
|
343
|
+
{ pattern: ["ls"], decision: "allow", justification: "list directory contents" },
|
|
344
|
+
{ pattern: ["cat"], decision: "allow", justification: "read file contents" },
|
|
345
|
+
{ pattern: ["head"], decision: "allow", justification: "read file head" },
|
|
346
|
+
{ pattern: ["tail"], decision: "allow", justification: "read file tail" },
|
|
347
|
+
{ pattern: ["wc"], decision: "allow", justification: "count lines/words" },
|
|
348
|
+
{ pattern: ["pwd"], decision: "allow", justification: "print working directory" },
|
|
349
|
+
{ pattern: ["which"], decision: "allow", justification: "locate a command" },
|
|
350
|
+
{ pattern: ["echo"], decision: "allow", justification: "print text" },
|
|
351
|
+
{ pattern: ["true"], decision: "allow", justification: "no-op success" },
|
|
352
|
+
{ pattern: ["false"], decision: "allow", justification: "no-op failure" },
|
|
353
|
+
{ pattern: ["test"], decision: "allow", justification: "test condition" },
|
|
354
|
+
{
|
|
355
|
+
pattern: ["find"],
|
|
356
|
+
decision: "allow",
|
|
357
|
+
justification: "search for files (read-only without -delete/-exec)",
|
|
358
|
+
unlessTokens: ["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint*", "-fls"],
|
|
359
|
+
},
|
|
360
|
+
{ pattern: ["grep"], decision: "allow", justification: "search text" },
|
|
361
|
+
{ pattern: ["rg"], decision: "allow", justification: "search text (ripgrep)" },
|
|
362
|
+
{ pattern: ["ag"], decision: "allow", justification: "search text (silver searcher)" },
|
|
363
|
+
{ pattern: ["cd"], decision: "allow", justification: "change directory (scoped to this bash invocation)" },
|
|
364
|
+
{
|
|
365
|
+
pattern: ["sed"],
|
|
366
|
+
decision: "allow",
|
|
367
|
+
justification: "stream-edit text to stdout (read-only without -i)",
|
|
368
|
+
unlessTokens: ["-i*", "--in-place*"],
|
|
369
|
+
},
|
|
370
|
+
{ pattern: ["awk"], decision: "allow", justification: "text processing to stdout" },
|
|
371
|
+
{ pattern: ["sort"], decision: "allow", justification: "sort lines" },
|
|
372
|
+
{ pattern: ["uniq"], decision: "allow", justification: "filter duplicate lines" },
|
|
373
|
+
{ pattern: ["cut"], decision: "allow", justification: "extract columns" },
|
|
374
|
+
{ pattern: ["tr"], decision: "allow", justification: "translate characters" },
|
|
375
|
+
{ pattern: ["diff"], decision: "allow", justification: "compare files" },
|
|
376
|
+
{ pattern: ["nl"], decision: "allow", justification: "number lines" },
|
|
377
|
+
{ pattern: ["jq"], decision: "allow", justification: "filter JSON to stdout" },
|
|
378
|
+
{ pattern: ["stat"], decision: "allow", justification: "show file metadata" },
|
|
379
|
+
{ pattern: ["file"], decision: "allow", justification: "identify file type" },
|
|
380
|
+
{ pattern: ["basename"], decision: "allow", justification: "strip directory from path" },
|
|
381
|
+
{ pattern: ["dirname"], decision: "allow", justification: "extract directory from path" },
|
|
382
|
+
{ pattern: ["realpath"], decision: "allow", justification: "resolve a path" },
|
|
383
|
+
{ pattern: ["readlink"], decision: "allow", justification: "resolve a symlink" },
|
|
384
|
+
{ pattern: ["tree"], decision: "allow", justification: "list directory tree" },
|
|
385
|
+
{ pattern: ["du"], decision: "allow", justification: "show disk usage" },
|
|
386
|
+
{ pattern: ["df"], decision: "allow", justification: "show filesystem usage" },
|
|
387
|
+
{ pattern: ["date"], decision: "allow", justification: "show date/time" },
|
|
388
|
+
{ pattern: ["printf"], decision: "allow", justification: "print formatted text" },
|
|
389
|
+
{ pattern: ["whoami"], decision: "allow", justification: "show current user" },
|
|
390
|
+
{ pattern: ["uname"], decision: "allow", justification: "show system info" },
|
|
391
|
+
{ pattern: ["git", "status"], decision: "allow", justification: "show working tree status" },
|
|
392
|
+
{ pattern: ["git", "log"], decision: "allow", justification: "show commit log" },
|
|
393
|
+
{ pattern: ["git", "diff"], decision: "allow", justification: "show changes" },
|
|
394
|
+
{ pattern: ["git", "branch"], decision: "allow", justification: "list branches" },
|
|
395
|
+
{ pattern: ["git", "show"], decision: "allow", justification: "show a commit" },
|
|
396
|
+
{ pattern: ["git", "remote"], decision: "allow", justification: "list remotes" },
|
|
397
|
+
{ pattern: ["git", "rev-parse"], decision: "allow", justification: "resolve git refs" },
|
|
398
|
+
{ pattern: ["git", "worktree", "list"], decision: "allow", justification: "list worktrees" },
|
|
399
|
+
{ pattern: ["git", "blame"], decision: "allow", justification: "show line authorship" },
|
|
400
|
+
{ pattern: ["git", "grep"], decision: "allow", justification: "search tracked files" },
|
|
401
|
+
{ pattern: ["git", "ls-files"], decision: "allow", justification: "list tracked files" },
|
|
402
|
+
{ pattern: ["git", "describe"], decision: "allow", justification: "describe a commit" },
|
|
403
|
+
{ pattern: ["git", "shortlog"], decision: "allow", justification: "summarize commit log" },
|
|
404
|
+
{ pattern: ["git", "stash", "list"], decision: "allow", justification: "list stashes" },
|
|
405
|
+
{ pattern: ["gh", "pr", ["view", "list", "diff", "checks", "status"]], decision: "allow", justification: "read pull request data" },
|
|
406
|
+
{ pattern: ["gh", "issue", ["view", "list", "status"]], decision: "allow", justification: "read issue data" },
|
|
407
|
+
{ pattern: ["gh", "run", ["view", "list"]], decision: "allow", justification: "read workflow run data" },
|
|
408
|
+
{ pattern: ["gh", "repo", "view"], decision: "allow", justification: "read repository data" },
|
|
409
|
+
{ pattern: ["gh", "search"], decision: "allow", justification: "search GitHub" },
|
|
410
|
+
{ pattern: ["node", "--version"], decision: "allow", justification: "check node version" },
|
|
411
|
+
{ pattern: ["node", "-v"], decision: "allow", justification: "check node version" },
|
|
412
|
+
{ pattern: ["npm", "ls"], decision: "allow", justification: "list installed packages" },
|
|
413
|
+
{ pattern: ["pnpm", "ls"], decision: "allow", justification: "list installed packages" },
|
|
414
|
+
{ pattern: ["pnpm", "--version"], decision: "allow", justification: "check pnpm version" },
|
|
415
|
+
{ pattern: ["tsc", "--version"], decision: "allow", justification: "check typescript version" },
|
|
416
|
+
// --- prompt: potentially destructive but context-dependent ---
|
|
417
|
+
{ pattern: ["rm"], decision: "prompt", justification: "file deletion — review the target" },
|
|
418
|
+
{ pattern: ["git", "commit"], decision: "prompt", justification: "creates a commit — confirm intent" },
|
|
419
|
+
{ pattern: ["git", "push"], decision: "prompt", justification: "pushes to remote — confirm intent" },
|
|
420
|
+
{ pattern: ["git", "merge"], decision: "prompt", justification: "merges branches — may conflict" },
|
|
421
|
+
{ pattern: ["git", "rebase"], decision: "prompt", justification: "rebases — rewrites history" },
|
|
422
|
+
{ pattern: ["git", "stash"], decision: "prompt", justification: "stashes working changes" },
|
|
423
|
+
{ pattern: ["git", "checkout"], decision: "prompt", justification: "switches branch or restores files" },
|
|
424
|
+
{ pattern: ["git", "switch"], decision: "prompt", justification: "switches branch" },
|
|
425
|
+
{ pattern: ["git", "reset"], decision: "prompt", justification: "resets HEAD — may discard changes" },
|
|
426
|
+
{ pattern: ["git", "revert"], decision: "prompt", justification: "creates a revert commit" },
|
|
427
|
+
{ pattern: ["git", "cherry-pick"], decision: "prompt", justification: "applies a specific commit" },
|
|
428
|
+
{ pattern: ["npm", "install"], decision: "prompt", justification: "installs packages — modifies node_modules" },
|
|
429
|
+
{ pattern: ["npm", "ci"], decision: "prompt", justification: "clean install — modifies node_modules" },
|
|
430
|
+
{ pattern: ["pnpm", "install"], decision: "prompt", justification: "installs packages — modifies node_modules" },
|
|
431
|
+
{ pattern: ["pnpm", "add"], decision: "prompt", justification: "adds a dependency" },
|
|
432
|
+
{ pattern: ["pnpm", "remove"], decision: "prompt", justification: "removes a dependency" },
|
|
433
|
+
{ pattern: ["npm", "run"], decision: "prompt", justification: "runs a script — may have side effects" },
|
|
434
|
+
{ pattern: ["pnpm", ["run", "exec"]], decision: "prompt", justification: "runs a script or binary — may have side effects" },
|
|
435
|
+
{ pattern: ["npx"], decision: "prompt", justification: "executes a package — may have side effects" },
|
|
436
|
+
{ pattern: ["curl"], decision: "prompt", justification: "network request — review the URL" },
|
|
437
|
+
{ pattern: ["wget"], decision: "prompt", justification: "network download — review the URL" },
|
|
438
|
+
{ pattern: ["docker"], decision: "prompt", justification: "docker command — may have side effects" },
|
|
439
|
+
{ pattern: ["psql"], decision: "prompt", justification: "database command — may modify data" },
|
|
440
|
+
{ pattern: ["fly"], decision: "prompt", justification: "Fly.io command — may affect production" },
|
|
441
|
+
{ pattern: ["kubectl"], decision: "prompt", justification: "Kubernetes command — may affect production" },
|
|
442
|
+
{ pattern: ["mv"], decision: "prompt", justification: "moves files — may overwrite" },
|
|
443
|
+
{ pattern: ["cp"], decision: "prompt", justification: "copies files" },
|
|
444
|
+
{ pattern: ["mkdir"], decision: "prompt", justification: "creates directories" },
|
|
445
|
+
{ pattern: ["touch"], decision: "prompt", justification: "creates or updates file timestamps" },
|
|
446
|
+
{ pattern: ["tar"], decision: "prompt", justification: "archive operation" },
|
|
447
|
+
{ pattern: ["zip"], decision: "prompt", justification: "archive operation" },
|
|
448
|
+
{ pattern: ["unzip"], decision: "prompt", justification: "archive operation" },
|
|
449
|
+
{ pattern: ["ps"], decision: "prompt", justification: "lists processes" },
|
|
450
|
+
{ pattern: ["kill"], decision: "prompt", justification: "sends a signal to a process" },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
//# sourceMappingURL=execPolicy.js.map
|