vigiles 14.13.3 → 14.13.5
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.
|
@@ -83,9 +83,21 @@ function discoverScripts(patterns, defaultGlob, cwd) {
|
|
|
83
83
|
found.add(p);
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
|
+
// 🔴 `dot: true`, because the harness for a Claude Code harness lives in `.claude/`.
|
|
87
|
+
// Without it, `vigiles test` and `vigiles eval` print "no files found" in a repository
|
|
88
|
+
// that has them, and `Tested` then measures visibility rather than coverage — the author
|
|
89
|
+
// reads "you have no tests" when the honest reading is "I looked in the wrong place".
|
|
90
|
+
// Observed 2026-08-07 on a repo with two harnesses under `.claude/`, both invisible; it
|
|
91
|
+
// stayed hidden because that repo's CI happened to pass explicit paths.
|
|
92
|
+
//
|
|
93
|
+
// This codebase already fixed the same defect elsewhere and missed it here:
|
|
94
|
+
// `test-coverage.ts` and `cli.ts` both pass `dot: true` with comments saying why, and
|
|
95
|
+
// `test-coverage.test.ts` records "glob without `dot:true` never found it and the surface
|
|
96
|
+
// looked untested". Coverage learned it; the runner did not.
|
|
86
97
|
for (const m of (0, glob_1.globSync)(p, {
|
|
87
98
|
cwd,
|
|
88
99
|
ignore: ["node_modules/**", "dist/**"],
|
|
100
|
+
dot: true,
|
|
89
101
|
})) {
|
|
90
102
|
found.add(m);
|
|
91
103
|
}
|
|
@@ -75,6 +75,7 @@ export interface TrifectaFinding {
|
|
|
75
75
|
/** A ready-to-show, actionable message. */
|
|
76
76
|
readonly message: string;
|
|
77
77
|
}
|
|
78
|
+
export declare function bashGrantIsUnbounded(raw: string): boolean;
|
|
78
79
|
/**
|
|
79
80
|
* Classify each tool in a declared contract into the trifecta legs it supplies.
|
|
80
81
|
* A single tool may land in MULTIPLE legs (`Bash` → A+C, `WebFetch` → B+C). An
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bashGrantIsUnbounded = bashGrantIsUnbounded;
|
|
3
4
|
exports.classifyTrifectaLegs = classifyTrifectaLegs;
|
|
4
5
|
exports.lethalTrifectaIssues = lethalTrifectaIssues;
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -71,6 +72,100 @@ const EXFIL_MCP_TOOLS = [
|
|
|
71
72
|
function baseTool(raw) {
|
|
72
73
|
return raw.split("(")[0].trim();
|
|
73
74
|
}
|
|
75
|
+
/** The restriction inside `Tool(...)`, or null when the grant is unrestricted. */
|
|
76
|
+
function restriction(raw) {
|
|
77
|
+
const open = raw.indexOf("(");
|
|
78
|
+
if (open === -1)
|
|
79
|
+
return null;
|
|
80
|
+
const close = raw.lastIndexOf(")");
|
|
81
|
+
if (close <= open)
|
|
82
|
+
return null;
|
|
83
|
+
return raw.slice(open + 1, close).trim();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Does a `Bash(...)` grant still supply the shell's legs — reading a secret and
|
|
87
|
+
* curling it out — or has it been narrowed to a command that cannot?
|
|
88
|
+
*
|
|
89
|
+
* 🔴 WHY THIS EXISTS. Narrowing the grant is the remedy this tool RECOMMENDS: drop a
|
|
90
|
+
* leg, allow at most two. Before this, `Bash(node ./scripts/log.mjs:*)` was read as
|
|
91
|
+
* plain `Bash` — the whole shell, in both leg A and leg C — so an author who took the
|
|
92
|
+
* advice saw their score not move, and the reasonable next conclusion is that
|
|
93
|
+
* narrowing is pointless. A diagnosis blind to its own prescription teaches the wrong
|
|
94
|
+
* lesson. Observed 2026-08-07 on a repo that narrowed nine skills to two named ledger
|
|
95
|
+
* commands and stayed at "17 units can read data, reach the web, and run commands".
|
|
96
|
+
*
|
|
97
|
+
* HIGH-PRECISION, deliberately. The grant loses the legs ONLY when the pattern pins a
|
|
98
|
+
* program AND at least one concrete argument, e.g. `node ./x.mjs:*`. These stay full
|
|
99
|
+
* shell, because each still runs whatever the caller likes:
|
|
100
|
+
*
|
|
101
|
+
* Bash no restriction at all
|
|
102
|
+
* Bash(*) Bash(:*) the wildcard forms
|
|
103
|
+
* Bash(node:*) program pinned, arguments free — `node -e "..."` is a shell
|
|
104
|
+
* Bash(sh ...) Bash(bash ...) Bash(eval ...) the shell itself, however pinned
|
|
105
|
+
* Bash(curl ...) Bash(scp ...) a pinned program whose whole job IS exfiltration
|
|
106
|
+
*
|
|
107
|
+
* When in doubt it returns true (keeps the legs): a false "you are exposed" costs the
|
|
108
|
+
* author an argument, a false "you are safe" costs them the finding.
|
|
109
|
+
*/
|
|
110
|
+
const SHELL_LIKE = new Set([
|
|
111
|
+
"sh",
|
|
112
|
+
"bash",
|
|
113
|
+
"zsh",
|
|
114
|
+
"dash",
|
|
115
|
+
"ksh",
|
|
116
|
+
"fish",
|
|
117
|
+
"eval",
|
|
118
|
+
"exec",
|
|
119
|
+
"env",
|
|
120
|
+
"xargs",
|
|
121
|
+
"sudo",
|
|
122
|
+
"doas",
|
|
123
|
+
"nohup",
|
|
124
|
+
"setsid",
|
|
125
|
+
"script",
|
|
126
|
+
"ssh",
|
|
127
|
+
"docker",
|
|
128
|
+
"podman",
|
|
129
|
+
"make",
|
|
130
|
+
]);
|
|
131
|
+
const EXFIL_PROGRAMS = new Set([
|
|
132
|
+
"curl",
|
|
133
|
+
"wget",
|
|
134
|
+
"scp",
|
|
135
|
+
"sftp",
|
|
136
|
+
"rsync",
|
|
137
|
+
"nc",
|
|
138
|
+
"ncat",
|
|
139
|
+
"netcat",
|
|
140
|
+
"telnet",
|
|
141
|
+
"ftp",
|
|
142
|
+
"git",
|
|
143
|
+
"gh",
|
|
144
|
+
"aws",
|
|
145
|
+
"gcloud",
|
|
146
|
+
"az",
|
|
147
|
+
"kubectl",
|
|
148
|
+
"npm",
|
|
149
|
+
"npx",
|
|
150
|
+
"pip",
|
|
151
|
+
]);
|
|
152
|
+
function bashGrantIsUnbounded(raw) {
|
|
153
|
+
const r = restriction(raw);
|
|
154
|
+
if (r === null)
|
|
155
|
+
return true; // bare `Bash`
|
|
156
|
+
const pattern = r.replace(/^["']|["']$/g, "").trim();
|
|
157
|
+
if (pattern === "" || pattern === "*" || pattern === ":*")
|
|
158
|
+
return true;
|
|
159
|
+
// `Bash(node ./x.mjs:*)` — the trailing `:*` is Claude Code's prefix marker, not an
|
|
160
|
+
// argument. Strip it before deciding whether any concrete argument was pinned.
|
|
161
|
+
const words = pattern.replace(/:\*$/, "").trim().split(/\s+/).filter(Boolean);
|
|
162
|
+
if (words.length < 2)
|
|
163
|
+
return true; // program pinned, arguments free
|
|
164
|
+
const program = (words[0].split("/").pop() ?? words[0]).toLowerCase();
|
|
165
|
+
if (SHELL_LIKE.has(program) || EXFIL_PROGRAMS.has(program))
|
|
166
|
+
return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
74
169
|
/** Returns true for the wildcard sentinels that mean "inherits-all". */
|
|
75
170
|
function isWildcard(tool) {
|
|
76
171
|
return tool === "" || tool === "*";
|
|
@@ -118,10 +213,14 @@ function classifyTrifectaLegs(tools, dialect) {
|
|
|
118
213
|
const base = baseTool(raw);
|
|
119
214
|
if (isWildcard(base))
|
|
120
215
|
continue; // handled by the issues fn, not a named leg
|
|
121
|
-
// Dual-role shell: leg A AND leg C
|
|
216
|
+
// Dual-role shell: leg A AND leg C — unless the grant has been narrowed to a
|
|
217
|
+
// command that is neither a shell nor an exfiltration tool, which is the remedy
|
|
218
|
+
// this checker itself recommends. See {@link bashGrantIsUnbounded}.
|
|
122
219
|
if (LEG_BASH_DUAL.has(base)) {
|
|
123
|
-
|
|
124
|
-
|
|
220
|
+
if (bashGrantIsUnbounded(raw)) {
|
|
221
|
+
priv.add(base);
|
|
222
|
+
exfil.add(base);
|
|
223
|
+
}
|
|
125
224
|
continue;
|
|
126
225
|
}
|
|
127
226
|
if (PRIVATE_BUILTINS.has(base))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "14.13.
|
|
3
|
+
"version": "14.13.5",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|