vibeqm-check 0.4.2 → 0.4.3
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 +1 -1
- package/dist/src/checks/basics.js +34 -11
- package/dist/src/checks/level2.js +12 -17
- package/dist/src/fixes.js +1 -1
- package/package.json +1 -1
- package/templates/level-2/.claude/settings.json +1 -1
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ that caused it. The report appears on the run's summary page.
|
|
|
130
130
|
```yaml
|
|
131
131
|
- uses: actions/checkout@v4
|
|
132
132
|
with: { fetch-depth: 0 } # the checks sample history
|
|
133
|
-
- uses: vibeqm/vibeqm-check@v0.4.
|
|
133
|
+
- uses: vibeqm/vibeqm-check@v0.4.3
|
|
134
134
|
with: { min-level: 2 } # optional: fail the run below this level
|
|
135
135
|
```
|
|
136
136
|
|
|
@@ -342,9 +342,30 @@ function environmentsShown(root, files, unfilled = []) {
|
|
|
342
342
|
}
|
|
343
343
|
return shown;
|
|
344
344
|
}
|
|
345
|
+
/** Claude Code settings files that count as the repository's: committed in HEAD, read as
|
|
346
|
+
* committed (uncommitted edits are not in any clone). The user's ~/.claude/settings.json
|
|
347
|
+
* and an uncommitted project file limit this machine only. A file that can't be read
|
|
348
|
+
* has text null. */
|
|
349
|
+
export function committedSettings(root) {
|
|
350
|
+
const project = [".claude/settings.json", ".claude/settings.local.json"];
|
|
351
|
+
const inHead = new Set((git(root, ["ls-tree", "-r", "--name-only", "HEAD", "--", ...project]) ?? "").split("\n").filter(Boolean));
|
|
352
|
+
const disk = (file) => {
|
|
353
|
+
try {
|
|
354
|
+
return readFileSync(file, "utf8");
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
return [
|
|
361
|
+
...project.map((f) => (inHead.has(f) ? { file: join(root, f), committed: true, text: git(root, ["show", `HEAD:${f}`]) } : { file: join(root, f), committed: false, text: disk(join(root, f)) })),
|
|
362
|
+
{ file: join(homedir(), ".claude", "settings.json"), committed: false, text: disk(join(homedir(), ".claude", "settings.json")) },
|
|
363
|
+
].filter((s) => s.committed || existsSync(s.file));
|
|
364
|
+
}
|
|
345
365
|
/** Where the agent is told, or made, to ask before publishing live. */
|
|
346
366
|
function confirmationRule(root) {
|
|
347
367
|
const found = [];
|
|
368
|
+
const machineOnly = [];
|
|
348
369
|
const rules = RULES_FILES.filter((f) => existsSync(join(root, f)));
|
|
349
370
|
for (const f of rules) {
|
|
350
371
|
const line = read(root, f)
|
|
@@ -353,22 +374,24 @@ function confirmationRule(root) {
|
|
|
353
374
|
if (line)
|
|
354
375
|
found.push(`${f}: "${line.replace(/^[-*\s]+/, "").trim().slice(0, 100)}"`);
|
|
355
376
|
}
|
|
356
|
-
|
|
357
|
-
for (const file of
|
|
358
|
-
if (!existsSync(file))
|
|
359
|
-
continue;
|
|
377
|
+
// Only settings committed to the repository count; this machine's settings are named, not counted.
|
|
378
|
+
for (const { file, committed, text } of committedSettings(root)) {
|
|
360
379
|
try {
|
|
361
|
-
const p = JSON.parse(
|
|
380
|
+
const p = JSON.parse(text ?? "")?.permissions ?? {};
|
|
362
381
|
const hits = [...(p.ask ?? []), ...(p.deny ?? [])].filter((x) => typeof x === "string" && /^Bash\(/.test(x) && DEPLOY_COMMAND.test(x));
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
382
|
+
if (!hits.length)
|
|
383
|
+
continue;
|
|
384
|
+
const list = `${hits.slice(0, 3).join(", ")}${hits.length > 3 ? "…" : ""}`;
|
|
385
|
+
if (committed)
|
|
386
|
+
found.push(`publish commands that stop for the person or are refused in ${file.slice(root.length + 1)}: ${list}`);
|
|
387
|
+
else
|
|
388
|
+
machineOnly.push(`${file} (this machine's settings, not committed): ${list}`);
|
|
366
389
|
}
|
|
367
390
|
catch {
|
|
368
391
|
/* unreadable settings: agentPermissions reports it */
|
|
369
392
|
}
|
|
370
393
|
}
|
|
371
|
-
return found;
|
|
394
|
+
return { found, machineOnly };
|
|
372
395
|
}
|
|
373
396
|
export function devApartFromProd(root) {
|
|
374
397
|
const gate = "Development apart from production";
|
|
@@ -377,13 +400,13 @@ export function devApartFromProd(root) {
|
|
|
377
400
|
const envs = environmentsShown(root, files, unfilled);
|
|
378
401
|
// "<the live address, or "nothing is live yet">" is a template's choice, not a statement.
|
|
379
402
|
const notLive = [...RULES_FILES, "README.md"].map((f) => read(root, f).split("\n").find((l) => NOT_LIVE.test(l) && !placeholders(l).length)).find(Boolean);
|
|
380
|
-
const confirm = confirmationRule(root);
|
|
403
|
+
const { found: confirm, machineOnly } = confirmationRule(root);
|
|
381
404
|
const shownEnvs = envs.length ? envs : notLive ? [`not live yet: "${notLive.trim().slice(0, 80)}"`] : [];
|
|
382
405
|
const ev = [
|
|
383
406
|
shownEnvs.length
|
|
384
407
|
? `environments: ${shownEnvs.slice(0, 2).join("; ")}`
|
|
385
408
|
: `no development and production environments named in the repository (in the rules file, the README or a note) or shown by config files${unfilled.length ? `: ${unfilled[0]}` : ""}`,
|
|
386
|
-
confirm.length ? `confirmation before publishing live: ${confirm.slice(0, 2).join("; ")}` :
|
|
409
|
+
confirm.length ? `confirmation before publishing live: ${confirm.slice(0, 2).join("; ")}` : `no rule that the agent publishes live only after the person confirms (a line in the rules file, or an ask or deny rule on publish commands in the agent's settings committed to the repository)${machineOnly.length ? `; found only in ${machineOnly[0]}, which other clones and CI don't have` : ""}`,
|
|
387
410
|
].join("; ");
|
|
388
411
|
return r(2, gate, shownEnvs.length && confirm.length ? "met" : "not-met", ev);
|
|
389
412
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// Level 2 (Guarded) fact checks, from ASSESSOR.md v0.3 §2. Judgment questions are
|
|
2
2
|
// left to the AI assessor; this file only reports what a script can prove.
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { join, relative } from "node:path";
|
|
6
5
|
import { commitSizes, git, median } from "../git.js";
|
|
7
6
|
import { defaultBranchRuns, mergedPullRequests } from "../host.js";
|
|
8
|
-
import { blanksNote, CHECK_COMMANDS, code, delegated, placeholders, RULES_FILES } from "./basics.js";
|
|
7
|
+
import { blanksNote, CHECK_COMMANDS, code, committedSettings, delegated, placeholders, RULES_FILES } from "./basics.js";
|
|
9
8
|
const SMALL_BATCH_LIMIT = 400; // ASSESSOR §2: median changed lines per merged change
|
|
10
9
|
const SAMPLE = 100;
|
|
11
10
|
export function agentRulesFile(root) {
|
|
@@ -144,42 +143,38 @@ export function ownerPerChange(root) {
|
|
|
144
143
|
: `all ${authors.length} sampled commits have a human author: ${[...people].slice(0, 3).join(", ")}${agentWritten ? `; ${agentWritten} of them written with an AI agent (per their trailers), owned by that author` : ""}`,
|
|
145
144
|
};
|
|
146
145
|
}
|
|
147
|
-
/** Claude Code deny rules or a sandbox, in
|
|
146
|
+
/** Claude Code deny rules or a sandbox, in settings committed to the repository. */
|
|
148
147
|
export function agentPermissions(root) {
|
|
149
148
|
const gate = "Agent permissions";
|
|
150
|
-
const sources = [
|
|
151
|
-
join(root, ".claude", "settings.json"),
|
|
152
|
-
join(root, ".claude", "settings.local.json"),
|
|
153
|
-
join(homedir(), ".claude", "settings.json"),
|
|
154
|
-
];
|
|
155
149
|
const found = [];
|
|
150
|
+
const machine = [];
|
|
156
151
|
const unreadable = [];
|
|
157
|
-
for (const file of
|
|
158
|
-
if (!existsSync(file))
|
|
159
|
-
continue;
|
|
152
|
+
for (const { file, committed, text } of committedSettings(root)) {
|
|
160
153
|
try {
|
|
161
|
-
const s = JSON.parse(
|
|
154
|
+
const s = JSON.parse(text ?? "");
|
|
162
155
|
const deny = s?.permissions?.deny ?? [];
|
|
163
|
-
const
|
|
156
|
+
const into = committed ? found : machine;
|
|
164
157
|
if (deny.length)
|
|
165
|
-
|
|
158
|
+
into.push(`${deny.length} deny rules in ${file}`);
|
|
166
159
|
if (s?.sandbox?.enabled)
|
|
167
|
-
|
|
160
|
+
into.push(`sandbox enabled in ${file}`);
|
|
168
161
|
}
|
|
169
162
|
catch {
|
|
170
163
|
unreadable.push(`unreadable settings: ${file}`);
|
|
171
164
|
}
|
|
172
165
|
}
|
|
173
166
|
if (!found.length) {
|
|
167
|
+
// Limits on this machine are real here, but clones and CI don't have them: a person can attest them.
|
|
168
|
+
const local = machine.length ? [`${machine.join("; ")}: this machine's settings, not committed, so other clones and CI don't have them; commit .claude/settings.json, or attach proof that every machine working on this repository has them`] : [];
|
|
174
169
|
return {
|
|
175
170
|
level: 2,
|
|
176
171
|
gate,
|
|
177
172
|
status: "not-visible",
|
|
178
173
|
// An unreadable file is not a limit: it says so, and never meets the gate.
|
|
179
|
-
evidence: [...unreadable, "no Claude Code deny rules or sandbox
|
|
174
|
+
evidence: [...unreadable, ...local, ...(local.length ? [] : ["no Claude Code deny rules or sandbox committed; other agents' settings are not read yet. Attach proof of limits"])].join("; "),
|
|
180
175
|
};
|
|
181
176
|
}
|
|
182
|
-
return { level: 2, gate, status: "met", evidence: [...found, ...unreadable].join("; ") };
|
|
177
|
+
return { level: 2, gate, status: "met", evidence: [...found, ...machine.map((m) => `also ${m} (this machine only)`), ...unreadable].join("; ") };
|
|
183
178
|
}
|
|
184
179
|
/** A CI run on the default branch that failed, then a later run of the same workflow
|
|
185
180
|
* that passed: the checks refused a change and it was fixed. */
|
package/dist/src/fixes.js
CHANGED
|
@@ -205,7 +205,7 @@ const FIXES = {
|
|
|
205
205
|
"Agent permissions": () => ({
|
|
206
206
|
why: "An agent that can run anything can delete, leak or force-push anything.",
|
|
207
207
|
steps: [
|
|
208
|
-
'Add deny rules to `.claude/settings.json` (
|
|
208
|
+
'Add deny rules to `.claude/settings.json` and commit it (the user\'s `~/.claude/settings.json` limits one machine only; clones and CI don\'t have it): `"permissions": {"deny": ["Bash(git push --force*)", "Bash(git push -f*)", "Bash(git commit --no-verify*)", "Bash(rm -rf /*)", "Bash(rm -rf ~*)", "Read(./.env*)", "Read(**/.env*)"]}`, plus the paths of any credentials this project uses.',
|
|
209
209
|
"Keep production credentials and secrets out of the agent's environment.",
|
|
210
210
|
],
|
|
211
211
|
verify: "Try one denied command (e.g. `git commit --no-verify --dry-run`) and confirm it is refused. " + RERUN,
|
package/package.json
CHANGED