vigiles 5.0.1 → 5.2.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/README.md +15 -9
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
- package/dist/adapters/claude-code/agent-runtime.js +66 -37
- package/dist/adapters/claude-code/dialect.js +37 -2
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +48 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/eval.d.ts +94 -0
- package/dist/adapters/codex/eval.js +227 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/cli.js +712 -21
- package/dist/codex.d.ts +1 -0
- package/dist/codex.js +3 -0
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +169 -74
- package/dist/core/description-overlap.d.ts +27 -0
- package/dist/core/description-overlap.js +53 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/frontmatter-read.d.ts +25 -0
- package/dist/core/frontmatter-read.js +138 -0
- package/dist/core/hook-events.d.ts +34 -0
- package/dist/core/hook-events.js +48 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-config.d.ts +20 -0
- package/dist/core/mcp-config.js +40 -0
- package/dist/core/mcp-hook.d.ts +35 -0
- package/dist/core/mcp-hook.js +70 -0
- package/dist/core/mcp-tool.d.ts +50 -0
- package/dist/core/mcp-tool.js +61 -0
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +142 -3
- package/dist/core/spec.js +48 -0
- package/dist/core/tool-contract.d.ts +68 -0
- package/dist/core/tool-contract.js +113 -0
- package/dist/core/types.d.ts +91 -2
- package/dist/core/validate.js +23 -1
- package/dist/eval.d.ts +69 -13
- package/dist/eval.js +106 -51
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +63 -3
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/plugin-loader.d.ts +1 -0
- package/dist/plugin-loader.js +71 -18
- package/dist/scaffold-test.d.ts +30 -0
- package/dist/scaffold-test.js +158 -0
- package/dist/scan-behavioral.d.ts +73 -0
- package/dist/scan-behavioral.js +150 -0
- package/dist/scan.d.ts +166 -1
- package/dist/scan.js +622 -55
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
- package/skills/edit-spec/SKILL.md +1 -1
package/dist/codex.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export * from "./adapters/codex/model-mock.js";
|
|
|
17
17
|
export * from "./adapters/codex/mock-model.js";
|
|
18
18
|
export * from "./adapters/codex/driver.js";
|
|
19
19
|
export * from "./adapters/codex/adapter.js";
|
|
20
|
+
export * from "./adapters/codex/eval.js";
|
|
20
21
|
//# sourceMappingURL=codex.d.ts.map
|
package/dist/codex.js
CHANGED
|
@@ -33,4 +33,7 @@ __exportStar(require("./adapters/codex/model-mock.js"), exports);
|
|
|
33
33
|
__exportStar(require("./adapters/codex/mock-model.js"), exports);
|
|
34
34
|
__exportStar(require("./adapters/codex/driver.js"), exports);
|
|
35
35
|
__exportStar(require("./adapters/codex/adapter.js"), exports);
|
|
36
|
+
// Eval-tier transport (increment 2 — scaffold, pending live-binary validation):
|
|
37
|
+
// parseCodexEvalRun (the ModelOutputParser for `codex exec --json`) + codexEvalRunner.
|
|
38
|
+
__exportStar(require("./adapters/codex/eval.js"), exports);
|
|
36
39
|
//# sourceMappingURL=codex.js.map
|
package/dist/core/adapter.d.ts
CHANGED
|
@@ -48,6 +48,16 @@ export interface AdapterCapabilities {
|
|
|
48
48
|
* tier and the `HookProtocol` port do not apply.
|
|
49
49
|
*/
|
|
50
50
|
readonly shellHooks: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The harness has **subagents** — a named, model-dispatched delegate with its
|
|
53
|
+
* own tool contract and frontmatter (Claude Code's `agents/*.md`, OpenCode's
|
|
54
|
+
* agent surface). Gates the subagent-surface lint rules (`subagent-tool-contract`,
|
|
55
|
+
* `subagent-frontmatter`, `untested-subagent`, `mcp-tool-resolves`): where this
|
|
56
|
+
* is `false` those rules report **n/a** rather than running. `false` for Codex,
|
|
57
|
+
* whose `[agents]` TOML is a concurrency table, not a tool-contract file — a
|
|
58
|
+
* wholly different concept that deliberately shares the word.
|
|
59
|
+
*/
|
|
60
|
+
readonly subagents: boolean;
|
|
51
61
|
}
|
|
52
62
|
export interface HarnessAdapter {
|
|
53
63
|
/** Stable identifier, e.g. "claude-code". The CLI/registry key. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, no-LLM Bash effect classifier.
|
|
3
|
+
*
|
|
4
|
+
* Classifies a Bash command string into a three-way verdict:
|
|
5
|
+
*
|
|
6
|
+
* "read-only" — proven: every leaf command head is in the read-only
|
|
7
|
+
* catalog, no output redirection, no dynamic/residue node.
|
|
8
|
+
* "side-effecting" — at least one leaf is provably effecting (unknown head
|
|
9
|
+
* outside the catalog, effecting flag, output redirection).
|
|
10
|
+
* "undecidable" — a residue construct (eval, $VAR head, $(…) head, sh -c,
|
|
11
|
+
* xargs, pipe-to-shell, ProcSubst, background &, Subshell)
|
|
12
|
+
* makes the static verdict unsound; fail-closed.
|
|
13
|
+
*
|
|
14
|
+
* FAIL-CLOSED: "read-only" is returned ONLY when it can be proven. Anything
|
|
15
|
+
* not proven read-only → "side-effecting" or "undecidable" (never "read-only").
|
|
16
|
+
* The zero-false-read-only property is the invariant this module maintains.
|
|
17
|
+
*
|
|
18
|
+
* Parse errors → "undecidable" (can't analyze ⇒ not read-only).
|
|
19
|
+
*
|
|
20
|
+
* This is a STANDALONE pure module. It does NOT import any adapter or dialect.
|
|
21
|
+
* Wiring into effectSurface / scan is a separate follow-up step.
|
|
22
|
+
*
|
|
23
|
+
* See `research/bash-effect-classification.md` for the full design rationale.
|
|
24
|
+
*/
|
|
25
|
+
/** The three-way verdict. */
|
|
26
|
+
export type BashEffect = "read-only" | "side-effecting" | "undecidable";
|
|
27
|
+
/**
|
|
28
|
+
* Classify a Bash command string by its effect. Sound by construction:
|
|
29
|
+
* returns "read-only" ONLY when every leaf command is a catalogued read-only
|
|
30
|
+
* head with no output redirection and no effecting flag; "undecidable" for the
|
|
31
|
+
* dynamic residue (eval, $(...)-as-command, $VAR head, sh -c, xargs, pipe-into-shell);
|
|
32
|
+
* "side-effecting" otherwise (the fail-closed default). NEVER returns "read-only"
|
|
33
|
+
* for a command it cannot fully prove read-only.
|
|
34
|
+
*/
|
|
35
|
+
export declare function classifyBashCommand(command: string): BashEffect;
|
|
36
|
+
/**
|
|
37
|
+
* True iff classifyBashCommand(command) === "read-only" — the safe predicate a
|
|
38
|
+
* caller uses to decide "this Bash is provably an observation."
|
|
39
|
+
*/
|
|
40
|
+
export declare function isReadOnlyBash(command: string): boolean;
|
|
41
|
+
//# sourceMappingURL=bash-effects.d.ts.map
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic, no-LLM Bash effect classifier.
|
|
4
|
+
*
|
|
5
|
+
* Classifies a Bash command string into a three-way verdict:
|
|
6
|
+
*
|
|
7
|
+
* "read-only" — proven: every leaf command head is in the read-only
|
|
8
|
+
* catalog, no output redirection, no dynamic/residue node.
|
|
9
|
+
* "side-effecting" — at least one leaf is provably effecting (unknown head
|
|
10
|
+
* outside the catalog, effecting flag, output redirection).
|
|
11
|
+
* "undecidable" — a residue construct (eval, $VAR head, $(…) head, sh -c,
|
|
12
|
+
* xargs, pipe-to-shell, ProcSubst, background &, Subshell)
|
|
13
|
+
* makes the static verdict unsound; fail-closed.
|
|
14
|
+
*
|
|
15
|
+
* FAIL-CLOSED: "read-only" is returned ONLY when it can be proven. Anything
|
|
16
|
+
* not proven read-only → "side-effecting" or "undecidable" (never "read-only").
|
|
17
|
+
* The zero-false-read-only property is the invariant this module maintains.
|
|
18
|
+
*
|
|
19
|
+
* Parse errors → "undecidable" (can't analyze ⇒ not read-only).
|
|
20
|
+
*
|
|
21
|
+
* This is a STANDALONE pure module. It does NOT import any adapter or dialect.
|
|
22
|
+
* Wiring into effectSurface / scan is a separate follow-up step.
|
|
23
|
+
*
|
|
24
|
+
* See `research/bash-effect-classification.md` for the full design rationale.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.classifyBashCommand = classifyBashCommand;
|
|
28
|
+
exports.isReadOnlyBash = isReadOnlyBash;
|
|
29
|
+
// mvdan-sh is a CJS package (GopherJS build) with no bundled TypeScript types.
|
|
30
|
+
// The project compiles to CommonJS (Node16, no "type":"module"), so plain
|
|
31
|
+
// require() works and is the idiomatic pattern here (see linters.ts).
|
|
32
|
+
const _sh = require("mvdan-sh");
|
|
33
|
+
const sh = _sh;
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Redirect operator codes from mvdan-sh (empirically confirmed via probe).
|
|
36
|
+
// Op values:
|
|
37
|
+
// 54 = > (RdrOut) 55 = >> (AppOut) 60 = >| (RdrClob)
|
|
38
|
+
// 64 = &> (RdrAll) 65 = &>> (AppAll)
|
|
39
|
+
// 56 = < (RdrIn — NOT a write) 59 = >& (DplOut — NOT a file write)
|
|
40
|
+
// 61 = << (Hdoc — input) 63 = <<< (HereStr — input)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const WRITE_REDIR_OPS = new Set([54, 55, 60, 64, 65]);
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Shell-escape heads: commands that dispatch arbitrary code as an argument.
|
|
45
|
+
// We treat ALL of these as undecidable regardless of their flags.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const SHELL_ESCAPE_HEADS = new Set([
|
|
48
|
+
"eval",
|
|
49
|
+
"sh",
|
|
50
|
+
"bash",
|
|
51
|
+
"zsh",
|
|
52
|
+
"dash",
|
|
53
|
+
"ksh",
|
|
54
|
+
"source",
|
|
55
|
+
".",
|
|
56
|
+
"xargs",
|
|
57
|
+
"env",
|
|
58
|
+
"command",
|
|
59
|
+
"exec",
|
|
60
|
+
"nohup",
|
|
61
|
+
"sudo",
|
|
62
|
+
"timeout",
|
|
63
|
+
"time",
|
|
64
|
+
]);
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Read-only command catalog (~35 heads, CONSERVATIVE).
|
|
67
|
+
// Only commands that are CLEARLY read-only without effecting flags are listed.
|
|
68
|
+
// Unknown heads → "side-effecting" (fail-closed).
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const READ_ONLY_HEADS = new Set([
|
|
71
|
+
"cat",
|
|
72
|
+
"ls",
|
|
73
|
+
"grep",
|
|
74
|
+
"egrep",
|
|
75
|
+
"fgrep",
|
|
76
|
+
"rg",
|
|
77
|
+
"head",
|
|
78
|
+
"tail",
|
|
79
|
+
"wc",
|
|
80
|
+
"stat",
|
|
81
|
+
"file",
|
|
82
|
+
"echo",
|
|
83
|
+
"printf",
|
|
84
|
+
"pwd",
|
|
85
|
+
"whoami",
|
|
86
|
+
"id",
|
|
87
|
+
"hostname",
|
|
88
|
+
"date",
|
|
89
|
+
"dirname",
|
|
90
|
+
"basename",
|
|
91
|
+
"realpath",
|
|
92
|
+
"readlink",
|
|
93
|
+
"tree",
|
|
94
|
+
"du",
|
|
95
|
+
"df",
|
|
96
|
+
"cut",
|
|
97
|
+
"tr",
|
|
98
|
+
"uniq",
|
|
99
|
+
"nl",
|
|
100
|
+
"od",
|
|
101
|
+
"cksum",
|
|
102
|
+
"md5sum",
|
|
103
|
+
"sha256sum",
|
|
104
|
+
"sha1sum",
|
|
105
|
+
"which",
|
|
106
|
+
"type",
|
|
107
|
+
"test",
|
|
108
|
+
"[",
|
|
109
|
+
"true",
|
|
110
|
+
"false",
|
|
111
|
+
"sleep",
|
|
112
|
+
"diff",
|
|
113
|
+
"cmp",
|
|
114
|
+
"strings",
|
|
115
|
+
"xxd",
|
|
116
|
+
"readelf",
|
|
117
|
+
"nm",
|
|
118
|
+
"ps",
|
|
119
|
+
"pgrep",
|
|
120
|
+
]);
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Flag-sensitive command helpers.
|
|
123
|
+
// Each returns the verdict for a command with those static args, or null to
|
|
124
|
+
// fall through to the catalog lookup (only used by classifyCall).
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
/** Flags on `find` that make it effecting. */
|
|
127
|
+
const FIND_EFFECTING_FLAGS = new Set([
|
|
128
|
+
"-delete",
|
|
129
|
+
"-exec",
|
|
130
|
+
"-execdir",
|
|
131
|
+
"-ok",
|
|
132
|
+
"-okdir",
|
|
133
|
+
"-fprint",
|
|
134
|
+
"-fprint0",
|
|
135
|
+
"-fprintf",
|
|
136
|
+
"-fls",
|
|
137
|
+
]);
|
|
138
|
+
/** Read-only `git` subcommands (first positional arg after `git`). */
|
|
139
|
+
const GIT_READ_ONLY_SUBCMDS = new Set([
|
|
140
|
+
"status",
|
|
141
|
+
"log",
|
|
142
|
+
"diff",
|
|
143
|
+
"show",
|
|
144
|
+
"branch",
|
|
145
|
+
"tag",
|
|
146
|
+
"rev-parse",
|
|
147
|
+
"describe",
|
|
148
|
+
"ls-files",
|
|
149
|
+
"ls-tree",
|
|
150
|
+
"cat-file",
|
|
151
|
+
"blame",
|
|
152
|
+
"shortlog",
|
|
153
|
+
"remote",
|
|
154
|
+
"config",
|
|
155
|
+
"stash",
|
|
156
|
+
"grep",
|
|
157
|
+
"format-patch",
|
|
158
|
+
]);
|
|
159
|
+
/** Classify `git <args>` — read-only only for safe subcommands. */
|
|
160
|
+
function classifyGit(staticArgs) {
|
|
161
|
+
const subcmd = staticArgs[0];
|
|
162
|
+
if (!subcmd || subcmd.startsWith("-"))
|
|
163
|
+
return "side-effecting";
|
|
164
|
+
if (!GIT_READ_ONLY_SUBCMDS.has(subcmd))
|
|
165
|
+
return "side-effecting";
|
|
166
|
+
if (subcmd === "stash") {
|
|
167
|
+
const action = staticArgs[1];
|
|
168
|
+
if (!action || (action !== "list" && action !== "show")) {
|
|
169
|
+
return "side-effecting";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return "read-only";
|
|
173
|
+
}
|
|
174
|
+
/** Classify `find <args>` — side-effecting if any effecting flag present. */
|
|
175
|
+
function classifyFind(staticArgs) {
|
|
176
|
+
for (const arg of staticArgs) {
|
|
177
|
+
if (FIND_EFFECTING_FLAGS.has(arg))
|
|
178
|
+
return "side-effecting";
|
|
179
|
+
}
|
|
180
|
+
return "read-only";
|
|
181
|
+
}
|
|
182
|
+
/** Classify `sort <args>` — side-effecting with `-o`. */
|
|
183
|
+
function classifySort(staticArgs) {
|
|
184
|
+
for (const arg of staticArgs) {
|
|
185
|
+
if (arg === "-o" || (arg.startsWith("-o") && arg.length > 2)) {
|
|
186
|
+
return "side-effecting";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return "read-only";
|
|
190
|
+
}
|
|
191
|
+
/** Classify `sed <args>` — side-effecting with `-i` / `--in-place`. */
|
|
192
|
+
function classifySed(staticArgs) {
|
|
193
|
+
for (const arg of staticArgs) {
|
|
194
|
+
if (arg === "-i" || arg.startsWith("-i") || arg === "--in-place") {
|
|
195
|
+
return "side-effecting";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return "read-only";
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Helpers: literal extraction from a Word node.
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
/** Returns the static string value of a Word iff it is a single Lit part. */
|
|
204
|
+
function getLiteral(word) {
|
|
205
|
+
if (!word?.Parts || word.Parts.length !== 1)
|
|
206
|
+
return null;
|
|
207
|
+
const part = word.Parts[0];
|
|
208
|
+
if (!part)
|
|
209
|
+
return null;
|
|
210
|
+
if (sh.syntax.NodeType(part) === "Lit")
|
|
211
|
+
return part.Value ?? null;
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/** Like getLiteral but also unwraps a single-Lit DblQuoted word. */
|
|
215
|
+
function getLiteralDeep(word) {
|
|
216
|
+
if (!word?.Parts || word.Parts.length !== 1)
|
|
217
|
+
return null;
|
|
218
|
+
const p = word.Parts[0];
|
|
219
|
+
if (!p)
|
|
220
|
+
return null;
|
|
221
|
+
const t = sh.syntax.NodeType(p);
|
|
222
|
+
if (t === "Lit")
|
|
223
|
+
return p.Value ?? null;
|
|
224
|
+
if (t === "DblQuoted") {
|
|
225
|
+
const inner = p;
|
|
226
|
+
if (!inner.Parts || inner.Parts.length !== 1)
|
|
227
|
+
return null;
|
|
228
|
+
const ip = inner.Parts[0];
|
|
229
|
+
if (!ip || sh.syntax.NodeType(ip) !== "Lit")
|
|
230
|
+
return null;
|
|
231
|
+
return ip.Value ?? null;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Combine two verdicts: undecidable > side-effecting > read-only.
|
|
237
|
+
* A single undecidable dominates; otherwise a single side-effecting dominates.
|
|
238
|
+
*/
|
|
239
|
+
function combine(a, b) {
|
|
240
|
+
if (a === "undecidable" || b === "undecidable")
|
|
241
|
+
return "undecidable";
|
|
242
|
+
if (a === "side-effecting" || b === "side-effecting")
|
|
243
|
+
return "side-effecting";
|
|
244
|
+
return "read-only";
|
|
245
|
+
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Per-leaf classifier.
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
/** Collect static (literal) arg strings from a CallExpr's args, skipping i=0 (head). */
|
|
250
|
+
function staticCallArgs(args) {
|
|
251
|
+
const result = [];
|
|
252
|
+
for (let i = 1; i < args.length; i++) {
|
|
253
|
+
const lit = getLiteralDeep(args[i]);
|
|
254
|
+
if (lit !== null)
|
|
255
|
+
result.push(lit);
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
/** Resolve the head of a CallExpr to a static string, or null if dynamic. */
|
|
260
|
+
function resolveHead(args) {
|
|
261
|
+
const headWord = args[0];
|
|
262
|
+
if (!headWord)
|
|
263
|
+
return null;
|
|
264
|
+
return getLiteral(headWord);
|
|
265
|
+
}
|
|
266
|
+
/** Classify a flag-sensitive command once the head and static args are known. */
|
|
267
|
+
function classifyFlagSensitive(head, staticArgs) {
|
|
268
|
+
switch (head) {
|
|
269
|
+
case "git":
|
|
270
|
+
return classifyGit(staticArgs);
|
|
271
|
+
case "find":
|
|
272
|
+
return classifyFind(staticArgs);
|
|
273
|
+
case "sort":
|
|
274
|
+
return classifySort(staticArgs);
|
|
275
|
+
case "sed":
|
|
276
|
+
return classifySed(staticArgs);
|
|
277
|
+
case "tee":
|
|
278
|
+
return "side-effecting";
|
|
279
|
+
case "awk":
|
|
280
|
+
case "gawk":
|
|
281
|
+
return "side-effecting"; // awk programs can write internally
|
|
282
|
+
default:
|
|
283
|
+
return null; // fall through to catalog
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Classify a single CallExpr node (one simple command). */
|
|
287
|
+
function classifyCall(node) {
|
|
288
|
+
const args = node.Args;
|
|
289
|
+
if (!args || args.length === 0)
|
|
290
|
+
return "side-effecting";
|
|
291
|
+
const head = resolveHead(args);
|
|
292
|
+
if (head === null)
|
|
293
|
+
return "undecidable"; // dynamic head
|
|
294
|
+
if (SHELL_ESCAPE_HEADS.has(head))
|
|
295
|
+
return "undecidable";
|
|
296
|
+
const sArgs = staticCallArgs(args);
|
|
297
|
+
const sensitive = classifyFlagSensitive(head, sArgs);
|
|
298
|
+
if (sensitive !== null)
|
|
299
|
+
return sensitive;
|
|
300
|
+
return READ_ONLY_HEADS.has(head) ? "read-only" : "side-effecting";
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// AST walker — classifies statements and commands.
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
/** Classify a Stmt node (handles redirections and background). */
|
|
306
|
+
function classifyStmt(stmt) {
|
|
307
|
+
if (stmt.Background === true)
|
|
308
|
+
return "undecidable";
|
|
309
|
+
if (stmt.Redirs) {
|
|
310
|
+
for (const r of stmt.Redirs) {
|
|
311
|
+
if (WRITE_REDIR_OPS.has(r.Op))
|
|
312
|
+
return "side-effecting";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const cmd = stmt.Cmd;
|
|
316
|
+
if (!cmd)
|
|
317
|
+
return "side-effecting";
|
|
318
|
+
return classifyCmd(cmd);
|
|
319
|
+
}
|
|
320
|
+
/** Classify a Cmd node (the typed command inside a Stmt). */
|
|
321
|
+
function classifyCmd(cmd) {
|
|
322
|
+
const t = sh.syntax.NodeType(cmd);
|
|
323
|
+
switch (t) {
|
|
324
|
+
case "CallExpr":
|
|
325
|
+
return classifyCall(cmd);
|
|
326
|
+
case "BinaryCmd":
|
|
327
|
+
return classifyBinaryCmd(cmd);
|
|
328
|
+
case "Subshell":
|
|
329
|
+
case "Block":
|
|
330
|
+
return classifyStmtList(cmd.Stmts);
|
|
331
|
+
default:
|
|
332
|
+
// IfClause, WhileClause, ForClause, CaseClause, FuncDecl, etc.
|
|
333
|
+
return "side-effecting";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Classify a BinaryCmd (pipe / && / ||). */
|
|
337
|
+
function classifyBinaryCmd(cmd) {
|
|
338
|
+
const xVerdict = cmd.X ? classifyStmt(cmd.X) : "side-effecting";
|
|
339
|
+
const yVerdict = cmd.Y ? classifyStmt(cmd.Y) : "side-effecting";
|
|
340
|
+
// For a pipe where the right side is a shell-escape, that already returns
|
|
341
|
+
// "undecidable" from classifyCall/classifyStmt — combine handles it.
|
|
342
|
+
return combine(xVerdict, yVerdict);
|
|
343
|
+
}
|
|
344
|
+
/** Classify a list of Stmt nodes (Subshell/Block body or top-level File). */
|
|
345
|
+
function classifyStmtList(stmts) {
|
|
346
|
+
if (!stmts || stmts.length === 0)
|
|
347
|
+
return "read-only";
|
|
348
|
+
let result = "read-only";
|
|
349
|
+
for (const s of stmts) {
|
|
350
|
+
result = combine(result, classifyStmt(s));
|
|
351
|
+
if (result === "undecidable")
|
|
352
|
+
return "undecidable";
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// ProcSubst residue detector (pre-pass over the full AST).
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
/** Returns true if the AST contains a ProcSubst node anywhere. */
|
|
360
|
+
function hasProcSubst(root) {
|
|
361
|
+
let found = false;
|
|
362
|
+
sh.syntax.Walk(root, (node) => {
|
|
363
|
+
if (found)
|
|
364
|
+
return false;
|
|
365
|
+
if (sh.syntax.NodeType(node) === "ProcSubst") {
|
|
366
|
+
found = true;
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
});
|
|
371
|
+
return found;
|
|
372
|
+
}
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Public API
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
/**
|
|
377
|
+
* Classify a Bash command string by its effect. Sound by construction:
|
|
378
|
+
* returns "read-only" ONLY when every leaf command is a catalogued read-only
|
|
379
|
+
* head with no output redirection and no effecting flag; "undecidable" for the
|
|
380
|
+
* dynamic residue (eval, $(...)-as-command, $VAR head, sh -c, xargs, pipe-into-shell);
|
|
381
|
+
* "side-effecting" otherwise (the fail-closed default). NEVER returns "read-only"
|
|
382
|
+
* for a command it cannot fully prove read-only.
|
|
383
|
+
*/
|
|
384
|
+
function classifyBashCommand(command) {
|
|
385
|
+
let file;
|
|
386
|
+
try {
|
|
387
|
+
file = sh.syntax.NewParser().Parse(command, "cmd.sh");
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// mvdan-sh throws a Go error object (not an Error instance) on parse failure.
|
|
391
|
+
return "undecidable";
|
|
392
|
+
}
|
|
393
|
+
// Pre-pass: detect ProcSubst (process substitution) anywhere in the AST.
|
|
394
|
+
if (hasProcSubst(file))
|
|
395
|
+
return "undecidable";
|
|
396
|
+
return classifyStmtList(file.Stmts);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* True iff classifyBashCommand(command) === "read-only" — the safe predicate a
|
|
400
|
+
* caller uses to decide "this Bash is provably an observation."
|
|
401
|
+
*/
|
|
402
|
+
function isReadOnlyBash(command) {
|
|
403
|
+
return classifyBashCommand(command) === "read-only";
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=bash-effects.js.map
|
package/dist/core/compile.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare function verifyHash(content: string): {
|
|
|
25
25
|
*/
|
|
26
26
|
/** @internal */ export declare function estimateTokens(text: string): number;
|
|
27
27
|
export interface CompileError {
|
|
28
|
-
type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway";
|
|
28
|
+
type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway" | "purity-violation" | "output-without-fork";
|
|
29
29
|
message: string;
|
|
30
30
|
path?: string;
|
|
31
31
|
}
|
|
@@ -33,6 +33,8 @@ export declare function validateFileRef(filePath: string, basePath: string): Com
|
|
|
33
33
|
export declare function readPackageScripts(basePath: string): Record<string, string> | null;
|
|
34
34
|
export declare function validateCommandRef(command: string, basePath: string): CompileError | null;
|
|
35
35
|
export declare function validateSymbolRef(file: string, name: string, basePath: string): CompileError | null;
|
|
36
|
+
export declare function validateDirRef(dirPath: string, basePath: string): CompileError | null;
|
|
37
|
+
export declare function validateGlobRef(pattern: string, basePath: string): CompileError | null;
|
|
36
38
|
export interface CompileClaudeResult {
|
|
37
39
|
markdown: string;
|
|
38
40
|
errors: CompileError[];
|