@yagni-app/code-staging 0.3.3-staging.1132.1 → 0.3.4-staging.1145.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/footer.d.ts +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js +6 -6
- package/dist/extension/{approvedPrefixes.js → permission/approvedPrefixes.js} +1 -1
- package/dist/extension/permission/dbReadPolicy.d.ts +90 -0
- package/dist/extension/permission/dbReadPolicy.js +227 -0
- package/dist/extension/{execPolicy.js → permission/execPolicy.js} +41 -13
- package/dist/extension/{permission.d.ts → permission/gate.d.ts} +3 -3
- package/dist/extension/{permission.js → permission/gate.js} +3 -3
- package/dist/extension/{guardian.d.ts → permission/guardian.d.ts} +2 -2
- package/dist/extension/{guardian.js → permission/guardian.js} +1 -1
- package/dist/extension/permission/index.d.ts +14 -0
- package/dist/extension/permission/index.js +14 -0
- package/dist/extension/permission/packageManagerPolicy.d.ts +55 -0
- package/dist/extension/permission/packageManagerPolicy.js +170 -0
- package/package.json +2 -2
- /package/dist/extension/{approvedPrefixes.d.ts → permission/approvedPrefixes.d.ts} +0 -0
- /package/dist/extension/{execPolicy.d.ts → permission/execPolicy.d.ts} +0 -0
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* — useful reference for what data to replicate and how to format it.
|
|
40
40
|
*/
|
|
41
41
|
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
|
-
import type { ModeHolder, PermissionMode } from "./permission.js";
|
|
42
|
+
import type { ModeHolder, PermissionMode } from "./permission/gate.js";
|
|
43
43
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
44
44
|
export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
|
|
45
45
|
export declare function isShiftTab(data: string): boolean;
|
|
@@ -121,8 +121,8 @@ export { makeAskYagniTool } from "./askYagniTool.js";
|
|
|
121
121
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
122
122
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
123
123
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
124
|
-
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
|
|
125
|
-
export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./guardian.js";
|
|
124
|
+
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./permission/guardian.js";
|
|
125
|
+
export type { GuardianOutcome, GuardianVerdict, GuardianState, GuardianStateHandle, GuardianLimits, ReviewResult, ReviewCommandDeps, } from "./permission/guardian.js";
|
|
126
126
|
export type { Citation, MakeAskYagniToolOptions } from "./askYagniTool.js";
|
|
127
127
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
128
128
|
export type { MakeReviewToolOptions } from "./reviewTool.js";
|
|
@@ -167,10 +167,10 @@ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkd
|
|
|
167
167
|
export type { SubagentDef, SubagentSource } from "./subagents.js";
|
|
168
168
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
169
169
|
export type { TodoItem, TodoStatus, TodoTheme } from "./todos.js";
|
|
170
|
-
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
171
|
-
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
|
|
172
|
-
export type { ExecDecision, ExecPolicy, PrefixRule, ExecClassification, } from "./execPolicy.js";
|
|
173
|
-
export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission.js";
|
|
170
|
+
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission/gate.js";
|
|
171
|
+
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./permission/execPolicy.js";
|
|
172
|
+
export type { ExecDecision, ExecPolicy, PrefixRule, ExecClassification, } from "./permission/execPolicy.js";
|
|
173
|
+
export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermissionDeps, BlessRememberInfo, } from "./permission/gate.js";
|
|
174
174
|
export { makeBlessStore, blessPath } from "./bless.js";
|
|
175
175
|
export type { BlessStore, BlessRule } from "./bless.js";
|
|
176
176
|
export { bankDecision, fetchDecisions, supersedeDecision, parseDecideArgs, formatDecisionsList, shortId, registerDecisionCommands, DEFAULT_LIST_LIMIT, } from "./decisions.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { appendFileSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
6
|
-
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./approvedPrefixes.js";
|
|
6
|
+
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
|
|
7
7
|
import { redactCommand } from "./redact.js";
|
|
8
|
-
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./guardian.js";
|
|
8
|
+
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand } from "./permission/guardian.js";
|
|
9
9
|
import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
10
10
|
import { makeAskYagniTool } from "./askYagniTool.js";
|
|
11
11
|
import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
@@ -28,7 +28,7 @@ import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone }
|
|
|
28
28
|
import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
|
|
29
29
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
30
30
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
31
|
-
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission.js";
|
|
31
|
+
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
32
32
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
33
33
|
import { registerSubagents } from "./subagents.js";
|
|
34
34
|
import { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
@@ -887,7 +887,7 @@ export { makeAskYagniTool } from "./askYagniTool.js";
|
|
|
887
887
|
export { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js";
|
|
888
888
|
export { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
|
|
889
889
|
export { ADVISOR_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatAdvisorSubtotal, makeAdvisorState, } from "./advisor.js";
|
|
890
|
-
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./guardian.js";
|
|
890
|
+
export { DEFAULT_GUARDIAN_LIMITS, GUARDIAN_MODEL_TIER, formatGuardianSubtotal, makeGuardianState, resolveGuardianLimits, reviewCommand, } from "./permission/guardian.js";
|
|
891
891
|
export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
892
892
|
export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
893
893
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
@@ -930,8 +930,8 @@ export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkd
|
|
|
930
930
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
931
931
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
932
932
|
// the session bless-with-remember capture hook.
|
|
933
|
-
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission.js";
|
|
934
|
-
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./execPolicy.js";
|
|
933
|
+
export { decideGate, registerPermissionGate, filterStaleModeContext, filterStalePlanContext, buildModeContextMessage, DEFAULT_PERMISSION_POLICY, MODE_CONTEXT_TYPE, PLAN_CONTEXT_TYPE, PLAN_CONTEXT_MESSAGE, } from "./permission/gate.js";
|
|
934
|
+
export { classifyCommand, DEFAULT_EXEC_POLICY, } from "./permission/execPolicy.js";
|
|
935
935
|
// W4 judgment loop: the session bless store (tool + path-prefix, session-only).
|
|
936
936
|
export { makeBlessStore, blessPath } from "./bless.js";
|
|
937
937
|
// W4 judgment loop: the decisions surface (/decide + /decisions) + shared bank.
|
|
@@ -29,7 +29,7 @@ import { execFileSync } from "node:child_process";
|
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { dirname, join } from "node:path";
|
|
31
31
|
import { classifyCommand, shellParse, tokenize } from "./execPolicy.js";
|
|
32
|
-
import { codeStateHome } from "
|
|
32
|
+
import { codeStateHome } from "../stateHome.js";
|
|
33
33
|
// --- Derivation ---
|
|
34
34
|
/** Tools whose second token is a subcommand worth capturing in a prefix. */
|
|
35
35
|
const MULTI_SUBCOMMAND_TOOLS = new Set([
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database read auto-allow policy (YAG-561).
|
|
3
|
+
*
|
|
4
|
+
* Auto-allows *provably read-only* inline SQL for a small set of database
|
|
5
|
+
* clients, so routine SELECTs never reach the Guardian. Anything we cannot
|
|
6
|
+
* prove read-only is left ENTIRELY to the Guardian — this module has no
|
|
7
|
+
* "deny" outcome (see the invariant below).
|
|
8
|
+
*
|
|
9
|
+
* The hard, easy part is extracting the SQL safely WITHOUT regex over the raw
|
|
10
|
+
* command. We reuse execPolicy's shellParse — the same quote-aware tokenizer
|
|
11
|
+
* that powers the whole classifier — so `-c "SELECT 1; SELECT 2"` arrives as
|
|
12
|
+
* one clean, dequoted token and a `;` OUTSIDE quotes still reads as a real
|
|
13
|
+
* command separator. Extraction failures (unknown flag, unknown short-cluster,
|
|
14
|
+
* `-f file`, stdin `<`) simply left as `readonly: false`.
|
|
15
|
+
*
|
|
16
|
+
* ── The single invariant ────────────────────────────────────────────────
|
|
17
|
+
* classifySql returns "read" ONLY when the SQL is provably read-only. A
|
|
18
|
+
* false "read" auto-runs a write across every workspace's DB, which is
|
|
19
|
+
* unacceptable; a false "unknown" costs one Guardian consult, which is free.
|
|
20
|
+
* We therefore over-reject: a write marker, a mutating function name, or a
|
|
21
|
+
* `\` meta-command ANYWHERE in the SQL downgrades the whole thing to
|
|
22
|
+
* "unknown". No attempt is made to distinguish "called" from "mentioned in
|
|
23
|
+
* a string literal" — erring toward prompt is always the safe direction.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: a non-"read" result is NOT "forbidden". An UPDATE/INSERT/DELETE is
|
|
26
|
+
* sent to the Guardian where a human/LLM judges intent. This module never
|
|
27
|
+
* flat-denies a DB write.
|
|
28
|
+
*
|
|
29
|
+
* ── How to extend ────────────────────────────────────────────────────────
|
|
30
|
+
* Add a new client: push a DbClientSpec to DB_CLIENTS.
|
|
31
|
+
* Add a new mutating function: append to the relevant client's
|
|
32
|
+
* mutatingFunctions list (schema-prefix-agnostic word-boundary match).
|
|
33
|
+
* Add a new way to pass SQL: add a SqlArgExtractor to the client.
|
|
34
|
+
* Every change needs a paired allow + regression test in dbReadPolicy.test.ts.
|
|
35
|
+
* See AGENTS.md alongside this directory for the full recipe.
|
|
36
|
+
*/
|
|
37
|
+
/** A single argv shape that carries inline SQL (e.g. psql `-c "…"`). */
|
|
38
|
+
export interface SqlArgExtractor {
|
|
39
|
+
/**
|
|
40
|
+
* Token forms introducing SQL: `-c`, `--command`, `-e`, `--execute`, … The
|
|
41
|
+
* extractor accepts these in three forms: glued long (`--command=…`), glued
|
|
42
|
+
* short (`-c…` for the 2-char flags only), and bare (`-c` → SQL is the next
|
|
43
|
+
* token). A bare single-dash flag may also match a short cluster
|
|
44
|
+
* (`-Atc`) when its terminal letter is `shortCluster.terminal` and every
|
|
45
|
+
* preceding letter is in `shortCluster.allowedPrefixes`.
|
|
46
|
+
*/
|
|
47
|
+
flags: readonly string[];
|
|
48
|
+
shortCluster?: {
|
|
49
|
+
terminal: string;
|
|
50
|
+
allowedPrefixes: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Everything we know about one database client. */
|
|
54
|
+
export interface DbClientSpec {
|
|
55
|
+
binaries: readonly string[];
|
|
56
|
+
sqlArgExtractors: readonly SqlArgExtractor[];
|
|
57
|
+
/** Is this (dequoted, full) SQL provably read-only? */
|
|
58
|
+
classifySql: (sql: string) => boolean;
|
|
59
|
+
/** Function names that make a bare SELECT mutate/abuse — reject as unknown. */
|
|
60
|
+
mutatingFunctions: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The shared "provably read-only" gate. Returns true only when every
|
|
64
|
+
* statement in `sql` (after comment-stripping) begins with SELECT/WITH and
|
|
65
|
+
* contains no write verb, write clause, mutating function, or backslash
|
|
66
|
+
* meta-command.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isSqlReadOnly(sql: string, mutatingFunctions: readonly string[]): boolean;
|
|
69
|
+
/** The supported database clients. Add a new one here to expand coverage. */
|
|
70
|
+
export declare const DB_CLIENTS: DbClientSpec[];
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the SQL string for a known client binary from its tokenized argv.
|
|
73
|
+
* Returns `{ sql }` on success (the dequoted SQL as one string) or null when
|
|
74
|
+
* the argv shape can't be proven to carry inline SQL. Null → left to Guardian.
|
|
75
|
+
*
|
|
76
|
+
* Supported token shapes, in the order tried:
|
|
77
|
+
* 1. glued long flag `--command=SELECT …` / `--execute=SELECT …`
|
|
78
|
+
* 2. glued short flag `-cSELECT …` / `-eSELECT …` (2-char flag + payload)
|
|
79
|
+
* 3. bare flag `-c` / `--command` / `-Atc` → SQL is the NEXT token
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractSqlArg(tokens: readonly string[], spec: DbClientSpec): {
|
|
82
|
+
sql: string;
|
|
83
|
+
} | null;
|
|
84
|
+
/**
|
|
85
|
+
* Top-level: should this database command be auto-allowed? Consumes the raw
|
|
86
|
+
* tokenized argv (tokens[0] is the binary) and returns true ONLY when the
|
|
87
|
+
* inline SQL was extracted AND classified read-only.
|
|
88
|
+
*/
|
|
89
|
+
export declare function classifyDbRead(tokens: readonly string[]): boolean;
|
|
90
|
+
//# sourceMappingURL=dbReadPolicy.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database read auto-allow policy (YAG-561).
|
|
3
|
+
*
|
|
4
|
+
* Auto-allows *provably read-only* inline SQL for a small set of database
|
|
5
|
+
* clients, so routine SELECTs never reach the Guardian. Anything we cannot
|
|
6
|
+
* prove read-only is left ENTIRELY to the Guardian — this module has no
|
|
7
|
+
* "deny" outcome (see the invariant below).
|
|
8
|
+
*
|
|
9
|
+
* The hard, easy part is extracting the SQL safely WITHOUT regex over the raw
|
|
10
|
+
* command. We reuse execPolicy's shellParse — the same quote-aware tokenizer
|
|
11
|
+
* that powers the whole classifier — so `-c "SELECT 1; SELECT 2"` arrives as
|
|
12
|
+
* one clean, dequoted token and a `;` OUTSIDE quotes still reads as a real
|
|
13
|
+
* command separator. Extraction failures (unknown flag, unknown short-cluster,
|
|
14
|
+
* `-f file`, stdin `<`) simply left as `readonly: false`.
|
|
15
|
+
*
|
|
16
|
+
* ── The single invariant ────────────────────────────────────────────────
|
|
17
|
+
* classifySql returns "read" ONLY when the SQL is provably read-only. A
|
|
18
|
+
* false "read" auto-runs a write across every workspace's DB, which is
|
|
19
|
+
* unacceptable; a false "unknown" costs one Guardian consult, which is free.
|
|
20
|
+
* We therefore over-reject: a write marker, a mutating function name, or a
|
|
21
|
+
* `\` meta-command ANYWHERE in the SQL downgrades the whole thing to
|
|
22
|
+
* "unknown". No attempt is made to distinguish "called" from "mentioned in
|
|
23
|
+
* a string literal" — erring toward prompt is always the safe direction.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: a non-"read" result is NOT "forbidden". An UPDATE/INSERT/DELETE is
|
|
26
|
+
* sent to the Guardian where a human/LLM judges intent. This module never
|
|
27
|
+
* flat-denies a DB write.
|
|
28
|
+
*
|
|
29
|
+
* ── How to extend ────────────────────────────────────────────────────────
|
|
30
|
+
* Add a new client: push a DbClientSpec to DB_CLIENTS.
|
|
31
|
+
* Add a new mutating function: append to the relevant client's
|
|
32
|
+
* mutatingFunctions list (schema-prefix-agnostic word-boundary match).
|
|
33
|
+
* Add a new way to pass SQL: add a SqlArgExtractor to the client.
|
|
34
|
+
* Every change needs a paired allow + regression test in dbReadPolicy.test.ts.
|
|
35
|
+
* See AGENTS.md alongside this directory for the full recipe.
|
|
36
|
+
*/
|
|
37
|
+
// --- Read-only classifier helpers ---
|
|
38
|
+
/** Strip leading/leading whitespace and `/* … */` block + `-- …` line comments. */
|
|
39
|
+
function stripSqlComments(sql) {
|
|
40
|
+
let out = sql;
|
|
41
|
+
// Block comments (non-greedy, across newlines).
|
|
42
|
+
out = out.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
43
|
+
// Line comments to end-of-line.
|
|
44
|
+
out = out.replace(/--[^\n]*/g, " ");
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Write verbs / statements that are never read-only. */
|
|
48
|
+
const WRITE_VERBS = /\b(insert|update|delete|truncate|drop|create|alter|grant|revoke|copy|vacuum|merge|call|set|replace|load)\b/i;
|
|
49
|
+
/** Statements that use SELECT but still write (INTO, FOR UPDATE, …). */
|
|
50
|
+
const WRITE_CLAUSES = /\b(into\s+(outfile|dumpfile|new\s+table|table)|for\s+(update|share)|returning)\b/i;
|
|
51
|
+
/** Match a function name with a word boundary (catches `schema.fn` and `fn(`). */
|
|
52
|
+
function mentionsFunction(sql, fn) {
|
|
53
|
+
const re = new RegExp(`(^|[^\\w])${fn}\\s*\\(`, "i");
|
|
54
|
+
return re.test(sql);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The shared "provably read-only" gate. Returns true only when every
|
|
58
|
+
* statement in `sql` (after comment-stripping) begins with SELECT/WITH and
|
|
59
|
+
* contains no write verb, write clause, mutating function, or backslash
|
|
60
|
+
* meta-command.
|
|
61
|
+
*/
|
|
62
|
+
export function isSqlReadOnly(sql, mutatingFunctions) {
|
|
63
|
+
const stripped = stripSqlComments(sql).trim();
|
|
64
|
+
// Any backslash is a psql/mysql meta-command (`\copy`, `\!`, `\i`) — not a
|
|
65
|
+
// plain SQL statement. Never auto-allow.
|
|
66
|
+
if (stripped.includes("\\"))
|
|
67
|
+
return false;
|
|
68
|
+
// A mutating function ANYWHERE (schema-prefixed, string-literal mention, or
|
|
69
|
+
// genuine call) downgrades the whole thing. Over-reject on purpose.
|
|
70
|
+
if (mutatingFunctions.some((fn) => mentionsFunction(stripped, fn)))
|
|
71
|
+
return false;
|
|
72
|
+
// Write verbs / clauses anywhere → not read-only. (Single pass over the whole
|
|
73
|
+
// multi-statement string means any `;`-joined write poisons the read.)
|
|
74
|
+
if (WRITE_VERBS.test(stripped))
|
|
75
|
+
return false;
|
|
76
|
+
if (WRITE_CLAUSES.test(stripped))
|
|
77
|
+
return false;
|
|
78
|
+
// The first statement must open with SELECT or WITH. (A leading `WITH`
|
|
79
|
+
// CTE is safe, but `WITH` can also be INSERT … WITH — the WRITE_VERBS
|
|
80
|
+
// check already caught any write verb, so a remaining `WITH` lead is a CTE.)
|
|
81
|
+
if (!/^(select|with)\b/i.test(stripped))
|
|
82
|
+
return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// --- Client registry ---
|
|
86
|
+
/**
|
|
87
|
+
* PostgreSQL. Inline SQL via `-c` / `--command` (glued or `=`) and the
|
|
88
|
+
* `-Atc`-style short cluster. Mutating built-ins that a plain SELECT can
|
|
89
|
+
* smuggle are rejected as unknown.
|
|
90
|
+
*/
|
|
91
|
+
const postgresMutatingFunctions = [
|
|
92
|
+
// sequence / identity
|
|
93
|
+
"nextval", "setval",
|
|
94
|
+
// large objects (server filesystem writes)
|
|
95
|
+
"lo_import", "lo_export", "lo_creat", "lo_create", "lo_unlink", "lo_put",
|
|
96
|
+
// server admin / signaling
|
|
97
|
+
"pg_terminate_backend", "pg_cancel_backend", "pg_reload_conf",
|
|
98
|
+
"pg_rotate_logfile", "pg_promote", "pg_log_backend_memory_contexts",
|
|
99
|
+
// advisory locks / notification / snapshots (state-changing)
|
|
100
|
+
"pg_advisory_lock", "pg_advisory_lock_shared", "pg_advisory_xact_lock",
|
|
101
|
+
"pg_advisory_xact_lock_shared", "pg_try_advisory_lock",
|
|
102
|
+
"pg_try_advisory_lock_shared", "pg_try_advisory_xact_lock",
|
|
103
|
+
"pg_try_advisory_xact_lock_shared", "pg_advisory_unlock",
|
|
104
|
+
"pg_advisory_unlock_shared", "pg_advisory_unlock_all", "pg_notify",
|
|
105
|
+
"pg_export_snapshot",
|
|
106
|
+
// replication slots
|
|
107
|
+
"pg_create_logical_replication_slot", "pg_drop_replication_slot",
|
|
108
|
+
"pg_create_physical_replication_slot",
|
|
109
|
+
// superuser server-side filesystem reads (exfil surface)
|
|
110
|
+
"pg_read_file", "pg_read_binary_file", "pg_ls_dir", "pg_stat_file",
|
|
111
|
+
// resource abuse
|
|
112
|
+
"pg_sleep", "pg_sleep_for", "pg_sleep_until",
|
|
113
|
+
// remote execution via dblink
|
|
114
|
+
"dblink", "dblink_exec", "dblink_connect", "dblink_disconnect",
|
|
115
|
+
"dblink_send_query", "dblink_get_result", "dblink_open", "dblink_fetch",
|
|
116
|
+
];
|
|
117
|
+
const mysqlMutatingFunctions = [
|
|
118
|
+
"sleep", "benchmark", "load_file",
|
|
119
|
+
"get_lock", "release_lock", "release_all_locks",
|
|
120
|
+
];
|
|
121
|
+
/** The supported database clients. Add a new one here to expand coverage. */
|
|
122
|
+
export const DB_CLIENTS = [
|
|
123
|
+
{
|
|
124
|
+
binaries: ["psql"],
|
|
125
|
+
sqlArgExtractors: [
|
|
126
|
+
{
|
|
127
|
+
flags: ["-c", "--command"],
|
|
128
|
+
shortCluster: {
|
|
129
|
+
terminal: "c",
|
|
130
|
+
allowedPrefixes: ["A", "t", "P", "q", "X", "a", "b", "e", "E", "f", "h", "n", "o", "p", "s", "S", "T", "U", "v", "V", "w", "W", "x", "z"],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
classifySql: (sql) => isSqlReadOnly(sql, postgresMutatingFunctions),
|
|
135
|
+
mutatingFunctions: postgresMutatingFunctions,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
binaries: ["mysql", "mariadb"],
|
|
139
|
+
sqlArgExtractors: [
|
|
140
|
+
{ flags: ["-e", "--execute"] },
|
|
141
|
+
],
|
|
142
|
+
classifySql: (sql) => isSqlReadOnly(sql, mysqlMutatingFunctions),
|
|
143
|
+
mutatingFunctions: mysqlMutatingFunctions,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
// --- SQL extraction (reuses shellParse — no regex over the raw command) ---
|
|
147
|
+
/**
|
|
148
|
+
* Does the flag (exact equality) match a token, including a single-dash
|
|
149
|
+
* short-cluster form like `-Atc` (terminal flag letter + an allowlisted set of
|
|
150
|
+
* prefix letters)? Only used when the token has no glued SQL payload.
|
|
151
|
+
*/
|
|
152
|
+
function matchesFlagExactly(token, extractor) {
|
|
153
|
+
if (extractor.flags.includes(token))
|
|
154
|
+
return true;
|
|
155
|
+
if (extractor.shortCluster && token.startsWith("-") && !token.startsWith("--")) {
|
|
156
|
+
const { terminal, allowedPrefixes } = extractor.shortCluster;
|
|
157
|
+
const body = token.slice(1); // drop the leading '-'
|
|
158
|
+
if (!body.endsWith(terminal) || body.length < 2)
|
|
159
|
+
return false;
|
|
160
|
+
const prefixes = body.slice(0, -1).split("");
|
|
161
|
+
return prefixes.every((p) => allowedPrefixes.includes(p));
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the SQL string for a known client binary from its tokenized argv.
|
|
167
|
+
* Returns `{ sql }` on success (the dequoted SQL as one string) or null when
|
|
168
|
+
* the argv shape can't be proven to carry inline SQL. Null → left to Guardian.
|
|
169
|
+
*
|
|
170
|
+
* Supported token shapes, in the order tried:
|
|
171
|
+
* 1. glued long flag `--command=SELECT …` / `--execute=SELECT …`
|
|
172
|
+
* 2. glued short flag `-cSELECT …` / `-eSELECT …` (2-char flag + payload)
|
|
173
|
+
* 3. bare flag `-c` / `--command` / `-Atc` → SQL is the NEXT token
|
|
174
|
+
*/
|
|
175
|
+
export function extractSqlArg(tokens, spec) {
|
|
176
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
177
|
+
const tok = tokens[i];
|
|
178
|
+
for (const extractor of spec.sqlArgExtractors) {
|
|
179
|
+
// 1. glued long flag: `--command=SELECT …`
|
|
180
|
+
for (const f of extractor.flags) {
|
|
181
|
+
if (f.startsWith("--") && tok.startsWith(f + "=")) {
|
|
182
|
+
const sql = tok.slice(f.length + 1);
|
|
183
|
+
if (sql.length > 0)
|
|
184
|
+
return { sql };
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 2. glued short flag: `-cSELECT …` (only for bare 2-char flags in the
|
|
189
|
+
// extractor, not short-clusters — a cluster like `-Atc` is a pure
|
|
190
|
+
// flag, not glue).
|
|
191
|
+
for (const f of extractor.flags) {
|
|
192
|
+
if (f.length === 2 && f.startsWith("-") && !f.startsWith("--")) {
|
|
193
|
+
if (tok.startsWith(f) && tok.length > 2) {
|
|
194
|
+
const sql = tok.slice(2);
|
|
195
|
+
if (sql.length > 0)
|
|
196
|
+
return { sql };
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 3. bare flag → SQL is the next token.
|
|
202
|
+
if (matchesFlagExactly(tok, extractor)) {
|
|
203
|
+
const v = tokens[i + 1];
|
|
204
|
+
if (typeof v === "string" && v.length > 0)
|
|
205
|
+
return { sql: v };
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Top-level: should this database command be auto-allowed? Consumes the raw
|
|
214
|
+
* tokenized argv (tokens[0] is the binary) and returns true ONLY when the
|
|
215
|
+
* inline SQL was extracted AND classified read-only.
|
|
216
|
+
*/
|
|
217
|
+
export function classifyDbRead(tokens) {
|
|
218
|
+
const binary = tokens[0];
|
|
219
|
+
const spec = DB_CLIENTS.find((c) => c.binaries.includes(binary));
|
|
220
|
+
if (!spec)
|
|
221
|
+
return false;
|
|
222
|
+
const extracted = extractSqlArg(tokens, spec);
|
|
223
|
+
if (!extracted)
|
|
224
|
+
return false;
|
|
225
|
+
return spec.classifySql(extracted.sql);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=dbReadPolicy.js.map
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* extension is bundled into @yagni-app/code's dist (a file copy, not a real
|
|
35
35
|
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
36
36
|
*/
|
|
37
|
+
import { classifyDbRead } from "./dbReadPolicy.js";
|
|
38
|
+
import { forwarderLabel, forwarderTailStart, normalizePackageManagerTokens, PACKAGE_MANAGER_ALLOW_RULES, } from "./packageManagerPolicy.js";
|
|
37
39
|
/**
|
|
38
40
|
* Parse a shell command string into tokens and control operators.
|
|
39
41
|
*
|
|
@@ -574,11 +576,13 @@ function matchRule(tokens, rule) {
|
|
|
574
576
|
const pat = rule.pattern[i];
|
|
575
577
|
const tok = tokens[i];
|
|
576
578
|
if (typeof pat === "string") {
|
|
577
|
-
|
|
579
|
+
// A trailing "*" glob lets a single pattern cover a namespace of tokens
|
|
580
|
+
// (e.g. "test:*" matches "test:backend", "test:file", …).
|
|
581
|
+
if (!tokenMatchesEntry(tok, pat))
|
|
578
582
|
return false;
|
|
579
583
|
}
|
|
580
584
|
else {
|
|
581
|
-
if (!pat.
|
|
585
|
+
if (!pat.some((entry) => tokenMatchesEntry(tok, entry)))
|
|
582
586
|
return false;
|
|
583
587
|
}
|
|
584
588
|
}
|
|
@@ -633,11 +637,28 @@ function classifySegmentTokens(rawTokens, policy, opts) {
|
|
|
633
637
|
const pathPrefixed = normalizedWord !== cmdWord;
|
|
634
638
|
let tokens = pathPrefixed ? [normalizedWord, ...strippedTokens.slice(1)] : strippedTokens;
|
|
635
639
|
tokens = normalizeGitTokens(tokens);
|
|
640
|
+
tokens = normalizePackageManagerTokens(tokens);
|
|
636
641
|
const neverAllow = stripped || pathPrefixed;
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
642
|
+
// Database read promotion: a known client (psql / mysql / …) whose inline
|
|
643
|
+
// SQL is provably read-only is auto-allowed BEFORE rule matching. A
|
|
644
|
+
// non-read (write, unknown shape, -f file) result means classifyDbRead
|
|
645
|
+
// returns false and we fall straight through to the prompt-band `psql`/
|
|
646
|
+
// `mysql` rule — never to a forbidden outcome. Only consulted when the
|
|
647
|
+
// command is not already disqualified (neverAllow) and we are not in the
|
|
648
|
+
// forbidden-only danger scan.
|
|
649
|
+
if (!neverAllow && !opts.forbiddenOnly && classifyDbRead(tokens)) {
|
|
650
|
+
return {
|
|
651
|
+
decision: "allow",
|
|
652
|
+
justification: "database read-only query (SELECT/WITH, no write or mutation)",
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// Forwarders (xargs / npx / <mgr> exec / <mgr> dlx) run their argv tail:
|
|
656
|
+
// classify the tail as its own segment so `pnpm exec rm -rf` inherits rm's
|
|
657
|
+
// forbidden floor and `npx tsc --noEmit` inherits tsc's allow. The forwarder
|
|
658
|
+
// itself is never allow; an unknown tail stays in the prompt band.
|
|
659
|
+
const tailStart = forwarderTailStart(tokens);
|
|
660
|
+
if (tailStart !== null && opts.depth < MAX_SCAN_DEPTH) {
|
|
661
|
+
let j = tailStart;
|
|
641
662
|
while (j < tokens.length && tokens[j].startsWith("-"))
|
|
642
663
|
j++;
|
|
643
664
|
const tail = tokens.slice(j);
|
|
@@ -646,12 +667,18 @@ function classifySegmentTokens(rawTokens, policy, opts) {
|
|
|
646
667
|
if (tailResult.decision === "forbidden")
|
|
647
668
|
return tailResult;
|
|
648
669
|
if (tailResult.decision === "allow" && !neverAllow) {
|
|
649
|
-
return {
|
|
670
|
+
return {
|
|
671
|
+
decision: "allow",
|
|
672
|
+
justification: `${forwarderLabel(tokens)} forwards to a read-only command`,
|
|
673
|
+
};
|
|
650
674
|
}
|
|
651
675
|
}
|
|
652
676
|
if (opts.forbiddenOnly)
|
|
653
677
|
return { decision: "allow", justification: "no forbidden match" };
|
|
654
|
-
return {
|
|
678
|
+
return {
|
|
679
|
+
decision: "prompt",
|
|
680
|
+
justification: `${forwarderLabel(tokens)} executes its argument command — review the target`,
|
|
681
|
+
};
|
|
655
682
|
}
|
|
656
683
|
// First match wins (rules are ordered; more specific rules come first).
|
|
657
684
|
for (const rule of policy.rules) {
|
|
@@ -949,11 +976,12 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
949
976
|
{ pattern: ["pnpm", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
|
|
950
977
|
{ pattern: ["npm", "test"], decision: "allow", justification: "run tests (routine dev-loop operation)" },
|
|
951
978
|
{ pattern: ["npm", "run", "lint"], decision: "allow", justification: "run linter (routine dev-loop operation)" },
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
979
|
+
// package-manager dev-loop band: dev-loop binaries (tsc --noEmit, tsx
|
|
980
|
+
// --test, vitest, jest) plus the test:/build:/lint: script namespaces.
|
|
981
|
+
// The npx/<mgr> exec/<mgr> dlx spellings reach these through tail
|
|
982
|
+
// forwarding (see classifySegmentTokens), so there is no separate
|
|
983
|
+
// "npx tsc" rule — `npx tsc --noEmit` forwards to the bare tsc rule.
|
|
984
|
+
...PACKAGE_MANAGER_ALLOW_RULES,
|
|
957
985
|
// misc read-only commands
|
|
958
986
|
{ pattern: ["printenv"], decision: "allow", justification: "print environment variables (read-only)" },
|
|
959
987
|
{ pattern: ["npm", ["view", "info"]], decision: "allow", justification: "read package metadata from registry" },
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
|
|
31
|
-
import type { HookRunner } from "
|
|
32
|
-
import { type BlessStore } from "
|
|
31
|
+
import type { HookRunner } from "../hooks.js";
|
|
32
|
+
import { type BlessStore } from "../bless.js";
|
|
33
33
|
import { type ExecPolicy } from "./execPolicy.js";
|
|
34
34
|
import { type GuardianError, type GuardianRiskLevel } from "./guardian.js";
|
|
35
35
|
export type PermissionMode = "auto" | "plan" | "review";
|
|
@@ -209,4 +209,4 @@ export declare const filterStalePlanContext: typeof filterStaleModeContext;
|
|
|
209
209
|
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
210
210
|
*/
|
|
211
211
|
export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
|
|
212
|
-
//# sourceMappingURL=
|
|
212
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
* the context so the model doesn't keep believing it is restricted.
|
|
28
28
|
*/
|
|
29
29
|
import { describePrefix, matchesGrant, validateGrant, } from "./approvedPrefixes.js";
|
|
30
|
-
import { makeBlessStore as defaultMakeBlessStore } from "
|
|
30
|
+
import { makeBlessStore as defaultMakeBlessStore } from "../bless.js";
|
|
31
31
|
import { classifyCommand, DEFAULT_EXEC_POLICY } from "./execPolicy.js";
|
|
32
|
-
import { isDebug } from "
|
|
32
|
+
import { isDebug } from "../diagnostics.js";
|
|
33
33
|
import { buildDiagnosticEvent, checkCircuitBreaker, DEFAULT_GUARDIAN_LIMITS, } from "./guardian.js";
|
|
34
34
|
export function createModeHolder(initial = "auto") {
|
|
35
35
|
let current = initial;
|
|
@@ -931,4 +931,4 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
931
931
|
},
|
|
932
932
|
});
|
|
933
933
|
}
|
|
934
|
-
//# sourceMappingURL=
|
|
934
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* exoneration — so an ask-preferring model cannot disarm the breaker by
|
|
28
28
|
* alternating deny/ask.
|
|
29
29
|
*/
|
|
30
|
-
import { runStage as defaultRunStage } from "
|
|
31
|
-
import type { PipelineStage } from "
|
|
30
|
+
import { runStage as defaultRunStage } from "../pipeline/runner.js";
|
|
31
|
+
import type { PipelineStage } from "../pipeline/types.js";
|
|
32
32
|
export type GuardianOutcome = "allow" | "ask" | "deny";
|
|
33
33
|
export type GuardianRiskLevel = "low" | "medium" | "high" | "critical";
|
|
34
34
|
export interface GuardianVerdict {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* exoneration — so an ask-preferring model cannot disarm the breaker by
|
|
28
28
|
* alternating deny/ask.
|
|
29
29
|
*/
|
|
30
|
-
import { runStage as defaultRunStage } from "
|
|
30
|
+
import { runStage as defaultRunStage } from "../pipeline/runner.js";
|
|
31
31
|
export const DEFAULT_GUARDIAN_LIMITS = {
|
|
32
32
|
maxReviews: 120,
|
|
33
33
|
maxConsecutiveDenials: 3,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission domain barrel.
|
|
3
|
+
*
|
|
4
|
+
* This file is the *public* re-export surface ONLY. Internal modules import
|
|
5
|
+
* each other directly (gate.ts → ./execPolicy.js), never through this barrel,
|
|
6
|
+
* to keep the dependency graph grep-able and avoid circular imports.
|
|
7
|
+
*
|
|
8
|
+
* See AGENTS.md alongside this directory for the seam map and how to extend.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./execPolicy.js";
|
|
11
|
+
export * from "./guardian.js";
|
|
12
|
+
export * from "./approvedPrefixes.js";
|
|
13
|
+
export * from "./gate.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission domain barrel.
|
|
3
|
+
*
|
|
4
|
+
* This file is the *public* re-export surface ONLY. Internal modules import
|
|
5
|
+
* each other directly (gate.ts → ./execPolicy.js), never through this barrel,
|
|
6
|
+
* to keep the dependency graph grep-able and avoid circular imports.
|
|
7
|
+
*
|
|
8
|
+
* See AGENTS.md alongside this directory for the seam map and how to extend.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./execPolicy.js";
|
|
11
|
+
export * from "./guardian.js";
|
|
12
|
+
export * from "./approvedPrefixes.js";
|
|
13
|
+
export * from "./gate.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-manager and dev-loop auto-allow policy (YAG-561).
|
|
3
|
+
*
|
|
4
|
+
* Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
|
|
5
|
+
* package-manager / dev-loop band, while execPolicy.ts owns the core classifier
|
|
6
|
+
* (rule matching, construct floor, danger scan) and wires these helpers in.
|
|
7
|
+
*
|
|
8
|
+
* Two independent mechanisms live here:
|
|
9
|
+
*
|
|
10
|
+
* 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
|
|
11
|
+
* match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
|
|
12
|
+
* "which workspace" options (never arbitrary flags), because an option we
|
|
13
|
+
* do NOT recognize must degrade to `prompt`, never to `allow`.
|
|
14
|
+
*
|
|
15
|
+
* 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
|
|
16
|
+
* `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
|
|
17
|
+
* tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
|
|
18
|
+
* forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
|
|
19
|
+
*
|
|
20
|
+
* The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
|
|
21
|
+
* - dev-loop binaries reachable through a forwarder (and directly):
|
|
22
|
+
* tsc (requires --noEmit), tsx (requires --test), vitest, jest;
|
|
23
|
+
* - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
|
|
26
|
+
* `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
|
|
27
|
+
* and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
|
|
28
|
+
* prompt band and go to the Guardian.
|
|
29
|
+
*
|
|
30
|
+
* See AGENTS.md alongside this directory for the "add a new spelling" recipe.
|
|
31
|
+
*/
|
|
32
|
+
import type { PrefixRule } from "./execPolicy.js";
|
|
33
|
+
/**
|
|
34
|
+
* Strip value-bearing "which workspace" options so they don't break prefix
|
|
35
|
+
* matching. Matching-only: the returned tokens are used to CLASSIFY, never to
|
|
36
|
+
* run. Only the handlers below are stripped; anything unrecognized falls
|
|
37
|
+
* through untouched and degrades to `prompt` (fail closed).
|
|
38
|
+
*
|
|
39
|
+
* pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
|
|
40
|
+
* npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
|
|
41
|
+
* yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizePackageManagerTokens(tokens: string[]): string[];
|
|
44
|
+
/**
|
|
45
|
+
* If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
|
|
46
|
+
* return the index at which the forwarded command's argv begins. Return null
|
|
47
|
+
* otherwise. Forwarders exercise `require`d escape hatch: they match only when
|
|
48
|
+
* the manager word is a bare, path-unprefixed, unwrapped token (callers already
|
|
49
|
+
* forced `neverAllow` for wrappers/paths before this runs).
|
|
50
|
+
*/
|
|
51
|
+
export declare function forwarderTailStart(tokens: string[]): number | null;
|
|
52
|
+
/** Human label for a forwarder, used in justification strings. */
|
|
53
|
+
export declare function forwarderLabel(tokens: string[]): string;
|
|
54
|
+
export declare const PACKAGE_MANAGER_ALLOW_RULES: PrefixRule[];
|
|
55
|
+
//# sourceMappingURL=packageManagerPolicy.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-manager and dev-loop auto-allow policy (YAG-561).
|
|
3
|
+
*
|
|
4
|
+
* Companion to execPolicy.ts: this file owns the *data* and *parsing* for the
|
|
5
|
+
* package-manager / dev-loop band, while execPolicy.ts owns the core classifier
|
|
6
|
+
* (rule matching, construct floor, danger scan) and wires these helpers in.
|
|
7
|
+
*
|
|
8
|
+
* Two independent mechanisms live here:
|
|
9
|
+
*
|
|
10
|
+
* 1. Workspace-selector normalization — `pnpm --filter <pkg> test:file` should
|
|
11
|
+
* match the same rules as `pnpm test:file`. We strip ONLY the value-bearing
|
|
12
|
+
* "which workspace" options (never arbitrary flags), because an option we
|
|
13
|
+
* do NOT recognize must degrade to `prompt`, never to `allow`.
|
|
14
|
+
*
|
|
15
|
+
* 2. Exec/dlx forwarders — `npx`, `pnpm exec`, `npm exec`, `yarn exec`,
|
|
16
|
+
* `pnpm dlx`, `yarn dlx`, and `xargs` all forward to their argv tail. The
|
|
17
|
+
* tail is classified as its own segment so `pnpm exec rm -rf` inherits rm's
|
|
18
|
+
* forbidden floor and `npx tsc --noEmit` inherits tsc's allow.
|
|
19
|
+
*
|
|
20
|
+
* The allow rules themselves (`PACKAGE_MANAGER_ALLOW_RULES`) cover:
|
|
21
|
+
* - dev-loop binaries reachable through a forwarder (and directly):
|
|
22
|
+
* tsc (requires --noEmit), tsx (requires --test), vitest, jest;
|
|
23
|
+
* - script namespaces `test:*` / `build:*` / `lint:*` across pnpm/npm/yarn.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT auto-allowed here: arbitrary `pnpm exec <binary>`, any
|
|
26
|
+
* `run <script>` outside the three namespaces (`pnpm run deploy` is prompt),
|
|
27
|
+
* and `pnpm install/add/remove` (they mutate node_modules). Those stay in the
|
|
28
|
+
* prompt band and go to the Guardian.
|
|
29
|
+
*
|
|
30
|
+
* See AGENTS.md alongside this directory for the "add a new spelling" recipe.
|
|
31
|
+
*/
|
|
32
|
+
// --- Workspace-selector normalization ---
|
|
33
|
+
/**
|
|
34
|
+
* Strip value-bearing "which workspace" options so they don't break prefix
|
|
35
|
+
* matching. Matching-only: the returned tokens are used to CLASSIFY, never to
|
|
36
|
+
* run. Only the handlers below are stripped; anything unrecognized falls
|
|
37
|
+
* through untouched and degrades to `prompt` (fail closed).
|
|
38
|
+
*
|
|
39
|
+
* pnpm: --filter <pkg> / -F <pkg> (+ --filter=<pkg> glue)
|
|
40
|
+
* npm : --workspace <pkg> / -w <pkg> (+ --workspace=<pkg> glue)
|
|
41
|
+
* yarn: (none handled yet — `yarn workspace <name>` is left alone on purpose)
|
|
42
|
+
*/
|
|
43
|
+
export function normalizePackageManagerTokens(tokens) {
|
|
44
|
+
const cmd = tokens[0];
|
|
45
|
+
if (cmd !== "pnpm" && cmd !== "npm" && cmd !== "yarn")
|
|
46
|
+
return tokens;
|
|
47
|
+
const out = [cmd];
|
|
48
|
+
let i = 1;
|
|
49
|
+
while (i < tokens.length) {
|
|
50
|
+
const t = tokens[i];
|
|
51
|
+
if (cmd === "pnpm") {
|
|
52
|
+
// `--filter pkg` / `-F pkg` → skip option + its value
|
|
53
|
+
if (t === "--filter" || t === "-F") {
|
|
54
|
+
i += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// glued `--filter=pkg` / `-F=pkg` → skip the single token
|
|
58
|
+
if (t.startsWith("--filter=") || t.startsWith("-F=")) {
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (cmd === "npm") {
|
|
64
|
+
// `--workspace pkg` / `-w pkg` → skip option + its value
|
|
65
|
+
if (t === "--workspace" || t === "-w") {
|
|
66
|
+
i += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (t.startsWith("--workspace=") || t.startsWith("-w=")) {
|
|
70
|
+
i += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// First non-selector token (yarn has none we handle) — stop stripping.
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
out.push(...tokens.slice(i));
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
// --- Exec/dlx forwarders ---
|
|
81
|
+
/**
|
|
82
|
+
* If `tokens[0]` names a forwarder (xargs / npx / <mgr> exec / <mgr> dlx),
|
|
83
|
+
* return the index at which the forwarded command's argv begins. Return null
|
|
84
|
+
* otherwise. Forwarders exercise `require`d escape hatch: they match only when
|
|
85
|
+
* the manager word is a bare, path-unprefixed, unwrapped token (callers already
|
|
86
|
+
* forced `neverAllow` for wrappers/paths before this runs).
|
|
87
|
+
*/
|
|
88
|
+
export function forwarderTailStart(tokens) {
|
|
89
|
+
const cmd = tokens[0];
|
|
90
|
+
if (cmd === "xargs" || cmd === "npx")
|
|
91
|
+
return 1;
|
|
92
|
+
if (tokens.length < 2)
|
|
93
|
+
return null;
|
|
94
|
+
const sub = tokens[1];
|
|
95
|
+
if (sub === "exec" && (cmd === "pnpm" || cmd === "npm" || cmd === "yarn"))
|
|
96
|
+
return 2;
|
|
97
|
+
if (sub === "dlx" && (cmd === "pnpm" || cmd === "yarn"))
|
|
98
|
+
return 2;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** Human label for a forwarder, used in justification strings. */
|
|
102
|
+
export function forwarderLabel(tokens) {
|
|
103
|
+
const cmd = tokens[0];
|
|
104
|
+
if (cmd === "npx" || cmd === "xargs")
|
|
105
|
+
return cmd;
|
|
106
|
+
return tokens.slice(0, 2).join(" ");
|
|
107
|
+
}
|
|
108
|
+
// --- Allow rules ---
|
|
109
|
+
/**
|
|
110
|
+
* The auto-allow rules for the package-manager / dev-loop band. Appended to
|
|
111
|
+
* DEFAULT_EXEC_POLICY.rules by execPolicy.ts (first-match-wins ordering puts
|
|
112
|
+
* these specific rules ahead of the `npx`/`run`/`exec` prompt fallbacks).
|
|
113
|
+
*
|
|
114
|
+
* Safety invariants for every entry:
|
|
115
|
+
* - tsc only allows with --noEmit (bare `tsc` would emit .js/.d.ts).
|
|
116
|
+
* - tsx only allows with --test (bare `tsx file.ts` executes arbitrary TS).
|
|
117
|
+
* - vitest / jest are pure test runners (no mutating flag to fence).
|
|
118
|
+
* - script namespaces are read-only-by-convention test/build/lint and are
|
|
119
|
+
* the ONLY `run` targets allowed; everything else (`run deploy`) is prompt.
|
|
120
|
+
*/
|
|
121
|
+
// The bare script name AND its `name:*` namespace (e.g. both `test` and
|
|
122
|
+
// `test:backend`) — declared once so each manager rule reuses the same set.
|
|
123
|
+
const SCRIPT_NAMESPACES = ["test", "test:*", "build", "build:*", "lint", "lint:*"];
|
|
124
|
+
export const PACKAGE_MANAGER_ALLOW_RULES = [
|
|
125
|
+
// dev-loop binaries (reachable directly or through a forwarder)
|
|
126
|
+
{
|
|
127
|
+
pattern: ["tsc"],
|
|
128
|
+
flagsAnywhere: ["--noEmit"],
|
|
129
|
+
decision: "allow",
|
|
130
|
+
justification: "typecheck only (--noEmit writes no files)",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
pattern: ["tsx"],
|
|
134
|
+
flagsAnywhere: ["--test"],
|
|
135
|
+
decision: "allow",
|
|
136
|
+
justification: "run tests via tsx (--test only, no script execution)",
|
|
137
|
+
},
|
|
138
|
+
{ pattern: ["vitest"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
|
|
139
|
+
{ pattern: ["vitest", "run"], decision: "allow", justification: "run vitest tests (routine dev-loop operation)" },
|
|
140
|
+
{ pattern: ["jest"], decision: "allow", justification: "run jest tests (routine dev-loop operation)" },
|
|
141
|
+
// script namespaces — direct form (pnpm/yarn support `mgr <script>`) and
|
|
142
|
+
// universal `run <script>` form. npm direct form is omitted: npm requires
|
|
143
|
+
// `run` for colon-namespaced scripts.
|
|
144
|
+
{
|
|
145
|
+
pattern: ["pnpm", SCRIPT_NAMESPACES],
|
|
146
|
+
decision: "allow",
|
|
147
|
+
justification: "run a test/build/lint script (routine dev-loop operation)",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
pattern: ["pnpm", "run", SCRIPT_NAMESPACES],
|
|
151
|
+
decision: "allow",
|
|
152
|
+
justification: "run a test/build/lint script (routine dev-loop operation)",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pattern: ["yarn", SCRIPT_NAMESPACES],
|
|
156
|
+
decision: "allow",
|
|
157
|
+
justification: "run a test/build/lint script (routine dev-loop operation)",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
pattern: ["yarn", "run", SCRIPT_NAMESPACES],
|
|
161
|
+
decision: "allow",
|
|
162
|
+
justification: "run a test/build/lint script (routine dev-loop operation)",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: ["npm", "run", SCRIPT_NAMESPACES],
|
|
166
|
+
decision: "allow",
|
|
167
|
+
justification: "run a test/build/lint script (routine dev-loop operation)",
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
//# sourceMappingURL=packageManagerPolicy.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4-staging.1145.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"smol-toml": "^1.8.0",
|
|
40
40
|
"typebox": "^1.3.11"
|
|
41
41
|
},
|
|
42
|
-
"yagniSourceSha": "
|
|
42
|
+
"yagniSourceSha": "23495abda59558b06033f88e56f41c8e2417e294"
|
|
43
43
|
}
|
|
File without changes
|
|
File without changes
|