coding-agent-skills 0.2.8 → 0.2.10
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/CHANGELOG.md +38 -0
- package/README.md +6 -0
- package/ROADMAP.md +21 -15
- package/bin/coding-agent-skills +15 -1
- package/docs/adapters/README.md +34 -0
- package/docs/adapters/project-installation.md +25 -1
- package/docs/adapters/real-project-adoption.md +3 -2
- package/docs/architecture/README.md +5 -1
- package/docs/release/README.md +11 -8
- package/docs/release/npm-package.md +10 -4
- package/docs/safety/README.md +9 -1
- package/docs/testing/README.md +15 -0
- package/docs/usage/README.md +23 -5
- package/examples/command-policies/env-audit.json +73 -0
- package/examples/command-policies/route-trace.json +72 -0
- package/examples/evidence-packs/env-audit.json +55 -0
- package/examples/evidence-packs/route-trace.json +55 -0
- package/examples/manifests/env-audit.json +14 -0
- package/examples/manifests/route-trace.json +14 -0
- package/examples/workflows/env-audit.md +16 -0
- package/examples/workflows/route-trace.md +20 -0
- package/package.json +3 -1
- package/runs/skill-runs.md +37 -0
- package/schemas/project-adapter-installation.schema.json +7 -3
- package/schemas/project-adapter.schema.json +4 -0
- package/scripts/lib/env-audit.mjs +640 -0
- package/scripts/lib/pack-rules.mjs +20 -2
- package/scripts/lib/route-trace.mjs +785 -0
- package/scripts/render-env-audit.mjs +8 -0
- package/scripts/render-route-trace.mjs +8 -0
- package/scripts/test-pack.mjs +159 -1
- package/scripts/validate-pack.mjs +8 -2
- package/skills/env-audit/SKILL.md +58 -0
- package/skills/env-audit/adapter-interface.md +12 -0
- package/skills/env-audit/agents/openai.yaml +4 -0
- package/skills/env-audit/checklist.md +7 -0
- package/skills/env-audit/evidence-template.md +17 -0
- package/skills/env-audit/examples.md +28 -0
- package/skills/env-audit/failure-modes.md +5 -0
- package/skills/route-trace/SKILL.md +58 -0
- package/skills/route-trace/adapter-interface.md +20 -0
- package/skills/route-trace/agents/openai.yaml +4 -0
- package/skills/route-trace/checklist.md +11 -0
- package/skills/route-trace/evidence-template.md +18 -0
- package/skills/route-trace/examples.md +32 -0
- package/skills/route-trace/failure-modes.md +9 -0
- package/tests/fixtures/env-audit/adapter-project/.coding-agent/adapters/env-audit-fixture/adapter.json +56 -0
- package/tests/fixtures/env-audit/adapter-project/.coding-agent/skills.json +23 -0
- package/tests/fixtures/env-audit/adapter-project/README.md +3 -0
- package/tests/fixtures/env-audit/adapter-project/package.json +4 -0
- package/tests/fixtures/env-audit/adapter-project/src/config.ts +2 -0
- package/tests/fixtures/env-audit/static-project/.env.example +3 -0
- package/tests/fixtures/env-audit/static-project/README.md +3 -0
- package/tests/fixtures/env-audit/static-project/docs/setup.md +3 -0
- package/tests/fixtures/env-audit/static-project/package.json +4 -0
- package/tests/fixtures/env-audit/static-project/src/config.ts +4 -0
- package/tests/fixtures/env-audit/static-project/src/deno.ts +1 -0
- package/tests/fixtures/route-trace/adapter-project/.coding-agent/adapters/route-trace-fixture/adapter.json +59 -0
- package/tests/fixtures/route-trace/adapter-project/.coding-agent/skills.json +23 -0
- package/tests/fixtures/route-trace/adapter-project/README.md +3 -0
- package/tests/fixtures/route-trace/adapter-project/app/api/items/route.ts +3 -0
- package/tests/fixtures/route-trace/adapter-project/package.json +5 -0
- package/tests/fixtures/route-trace/adapter-project/pages/index.tsx +3 -0
- package/tests/fixtures/route-trace/adapter-project/src/routes.ts +3 -0
- package/tests/fixtures/route-trace/static-project/.env.example +1 -0
- package/tests/fixtures/route-trace/static-project/README.md +3 -0
- package/tests/fixtures/route-trace/static-project/app/api/users/route.ts +3 -0
- package/tests/fixtures/route-trace/static-project/app/blog/[slug]/page.tsx +3 -0
- package/tests/fixtures/route-trace/static-project/app/page.tsx +3 -0
- package/tests/fixtures/route-trace/static-project/package.json +5 -0
- package/tests/fixtures/route-trace/static-project/pages/about.tsx +3 -0
- package/tests/fixtures/route-trace/static-project/pages/api/hello.ts +3 -0
- package/tests/fixtures/route-trace/static-project/server/routes.ts +4 -0
- package/tests/fixtures/route-trace/static-project/src/route-config.ts +4 -0
- package/tests/fixtures/route-trace/static-project/src/router.tsx +10 -0
- package/tests/fixtures/triggers/cases.json +25 -1
- package/tests/trigger/README.md +3 -0
- package/work-ledger.md +35 -10
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ADAPTER_MANIFEST_FILENAME,
|
|
8
|
+
readSafeJsonFile,
|
|
9
|
+
} from "./adapter-discovery.mjs";
|
|
10
|
+
import { PILOT_VERSION } from "./pack-rules.mjs";
|
|
11
|
+
import {
|
|
12
|
+
readProjectAdapterDeclaration,
|
|
13
|
+
validateProjectAdapters,
|
|
14
|
+
} from "./project-adapter-installation.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CORE_ROOT = path.resolve(
|
|
17
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
"..",
|
|
19
|
+
"..",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const DEFAULT_IGNORED_PATHS = [
|
|
23
|
+
".git",
|
|
24
|
+
".next",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"coverage",
|
|
29
|
+
"out",
|
|
30
|
+
"validation-output",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const TEXT_EXTENSIONS = new Set([
|
|
34
|
+
".cjs",
|
|
35
|
+
".cts",
|
|
36
|
+
".js",
|
|
37
|
+
".jsx",
|
|
38
|
+
".json",
|
|
39
|
+
".md",
|
|
40
|
+
".mjs",
|
|
41
|
+
".mts",
|
|
42
|
+
".toml",
|
|
43
|
+
".ts",
|
|
44
|
+
".tsx",
|
|
45
|
+
".txt",
|
|
46
|
+
".yaml",
|
|
47
|
+
".yml",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const CONFIG_FILENAMES = new Set([
|
|
51
|
+
".env.example",
|
|
52
|
+
"Dockerfile",
|
|
53
|
+
"docker-compose.yml",
|
|
54
|
+
"docker-compose.yaml",
|
|
55
|
+
"next.config.js",
|
|
56
|
+
"next.config.mjs",
|
|
57
|
+
"next.config.ts",
|
|
58
|
+
"vite.config.js",
|
|
59
|
+
"vite.config.mjs",
|
|
60
|
+
"vite.config.ts",
|
|
61
|
+
"wrangler.toml",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const REFUSED_BEHAVIOR = [
|
|
65
|
+
"no .env reads",
|
|
66
|
+
"no secret-file reads",
|
|
67
|
+
"no value printing",
|
|
68
|
+
"no credential validation",
|
|
69
|
+
"no API calls",
|
|
70
|
+
"no target project builds",
|
|
71
|
+
"no target project tests",
|
|
72
|
+
"no package installs",
|
|
73
|
+
"no deployments",
|
|
74
|
+
"no migrations",
|
|
75
|
+
"no project writes",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const NOT_VERIFIED = [
|
|
79
|
+
"actual environment variable values",
|
|
80
|
+
"whether credentials are valid",
|
|
81
|
+
"runtime-injected environment",
|
|
82
|
+
"host, CI, platform, or cloud secret stores",
|
|
83
|
+
"variables assembled dynamically at runtime",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function inside(root, candidate) {
|
|
87
|
+
const relative = path.relative(root, candidate);
|
|
88
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toPosix(relativePath) {
|
|
92
|
+
return relativePath.split(path.sep).join("/");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function safeRelativePath(candidate) {
|
|
96
|
+
return (
|
|
97
|
+
typeof candidate === "string" &&
|
|
98
|
+
candidate.length > 0 &&
|
|
99
|
+
!candidate.startsWith("/") &&
|
|
100
|
+
!candidate.split(/[\\/]+/).includes("..")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function secretBearingPath(relativePath) {
|
|
105
|
+
const normalized = toPosix(relativePath);
|
|
106
|
+
const basename = path.posix.basename(normalized);
|
|
107
|
+
return (
|
|
108
|
+
basename === ".env" ||
|
|
109
|
+
(basename.startsWith(".env.") && basename !== ".env.example") ||
|
|
110
|
+
basename === ".npmrc" ||
|
|
111
|
+
/\.(?:pem|key|p12|pfx)$/i.test(basename) ||
|
|
112
|
+
/(?:^|\/)(?:secrets?|credentials?|private-key)(?:\/|$)/i.test(normalized)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ignoredBy(relativePath, ignoredPaths) {
|
|
117
|
+
const normalized = toPosix(relativePath);
|
|
118
|
+
return ignoredPaths.some((ignored) => {
|
|
119
|
+
const clean = toPosix(ignored).replace(/\/+$/g, "");
|
|
120
|
+
return normalized === clean || normalized.startsWith(`${clean}/`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function gitSummary(projectRoot) {
|
|
125
|
+
const summary = {
|
|
126
|
+
root: null,
|
|
127
|
+
branchState: null,
|
|
128
|
+
hasUncommittedChanges: false,
|
|
129
|
+
warnings: [],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const revParse = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
133
|
+
cwd: projectRoot,
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
+
});
|
|
137
|
+
if (revParse.status === 0) summary.root = revParse.stdout.trim();
|
|
138
|
+
|
|
139
|
+
const status = spawnSync("git", ["status", "--short", "--branch"], {
|
|
140
|
+
cwd: projectRoot,
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
+
});
|
|
144
|
+
if (status.status !== 0) {
|
|
145
|
+
summary.warnings.push("git status unavailable");
|
|
146
|
+
return summary;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lines = status.stdout.split(/\r?\n/).filter(Boolean);
|
|
150
|
+
summary.branchState = lines[0] ?? null;
|
|
151
|
+
summary.hasUncommittedChanges = lines.length > 1;
|
|
152
|
+
if (summary.hasUncommittedChanges) {
|
|
153
|
+
summary.warnings.push("working tree has local changes; filenames omitted");
|
|
154
|
+
}
|
|
155
|
+
if (/\[(?:ahead|behind|gone|diverged)[^\]]*\]/i.test(summary.branchState ?? "")) {
|
|
156
|
+
summary.warnings.push("branch state indicates remote divergence; revalidate after update");
|
|
157
|
+
}
|
|
158
|
+
return summary;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function discoverEnvAuditAdapters(loaded) {
|
|
162
|
+
const adapters = [];
|
|
163
|
+
const errors = [];
|
|
164
|
+
const declarations = loaded.declaration.adapters ?? [];
|
|
165
|
+
const container = path.resolve(loaded.projectRoot, loaded.declaration.adapterRoot);
|
|
166
|
+
const manifestCandidates = [];
|
|
167
|
+
|
|
168
|
+
if (!inside(loaded.projectRoot, container) || !fs.existsSync(container)) {
|
|
169
|
+
return { adapters, errors: ["adapter-root-not-found"] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const entry of fs.readdirSync(container, { withFileTypes: true })) {
|
|
173
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
174
|
+
const manifestPath = path.join(container, entry.name, ADAPTER_MANIFEST_FILENAME);
|
|
175
|
+
if (!inside(loaded.projectRoot, manifestPath) || !fs.existsSync(manifestPath)) continue;
|
|
176
|
+
const record = readSafeJsonFile(manifestPath);
|
|
177
|
+
if (!record.value) {
|
|
178
|
+
errors.push(...record.codes);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
manifestCandidates.push({
|
|
182
|
+
manifestPath,
|
|
183
|
+
manifest: record.value,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const declaration of declarations) {
|
|
188
|
+
if (!(declaration.skillIds ?? []).includes("env-audit")) continue;
|
|
189
|
+
const match = manifestCandidates.find(
|
|
190
|
+
(candidate) => candidate.manifest.adapterId === declaration.id,
|
|
191
|
+
);
|
|
192
|
+
if (!match) {
|
|
193
|
+
errors.push("declared-env-audit-adapter-not-found");
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
adapters.push({
|
|
197
|
+
declaration,
|
|
198
|
+
manifestPath: path.relative(loaded.projectRoot, match.manifestPath),
|
|
199
|
+
manifest: match.manifest,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { adapters, errors };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function adapterContext(projectRootInput, coreRoot) {
|
|
207
|
+
const loaded = readProjectAdapterDeclaration(projectRootInput);
|
|
208
|
+
if (!loaded.ok) {
|
|
209
|
+
if (loaded.codes.length === 1 && loaded.codes[0] === "missing-project-declaration") {
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
present: false,
|
|
213
|
+
enabled: false,
|
|
214
|
+
projectRoot: path.resolve(projectRootInput),
|
|
215
|
+
mode: "none",
|
|
216
|
+
codes: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
present: false,
|
|
222
|
+
enabled: false,
|
|
223
|
+
status: "failed",
|
|
224
|
+
codes: loaded.codes,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const validation = validateProjectAdapters(loaded.projectRoot, { coreRoot });
|
|
229
|
+
if (!validation.ok) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
present: true,
|
|
233
|
+
enabled: false,
|
|
234
|
+
status: "failed",
|
|
235
|
+
codes: validation.codes,
|
|
236
|
+
validation,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!validation.acceptedSkills.includes("env-audit")) {
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
present: true,
|
|
244
|
+
enabled: false,
|
|
245
|
+
projectRoot: loaded.projectRoot,
|
|
246
|
+
mode: "adapter-present-env-audit-not-enabled",
|
|
247
|
+
declarationPath: loaded.declarationPath,
|
|
248
|
+
declaration: loaded.declaration,
|
|
249
|
+
validation,
|
|
250
|
+
codes: ["env-audit-not-enabled-by-adapter"],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { adapters, errors } = discoverEnvAuditAdapters(loaded);
|
|
255
|
+
if (errors.length) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
present: true,
|
|
259
|
+
enabled: false,
|
|
260
|
+
status: "failed",
|
|
261
|
+
codes: errors,
|
|
262
|
+
validation,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
present: true,
|
|
269
|
+
enabled: true,
|
|
270
|
+
projectRoot: loaded.projectRoot,
|
|
271
|
+
mode: "adapter-enabled",
|
|
272
|
+
declarationPath: loaded.declarationPath,
|
|
273
|
+
declaration: loaded.declaration,
|
|
274
|
+
validation,
|
|
275
|
+
adapters,
|
|
276
|
+
codes: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function scanScopeFromAdapter(context) {
|
|
281
|
+
const safeReadPaths = new Set();
|
|
282
|
+
const ignoredPaths = new Set(DEFAULT_IGNORED_PATHS);
|
|
283
|
+
|
|
284
|
+
for (const adapter of context.adapters ?? []) {
|
|
285
|
+
for (const candidate of adapter.manifest.extensions?.safeReadPaths ?? []) {
|
|
286
|
+
if (safeRelativePath(candidate)) safeReadPaths.add(candidate);
|
|
287
|
+
}
|
|
288
|
+
for (const candidate of adapter.manifest.extensions?.ignoredPaths ?? []) {
|
|
289
|
+
if (safeRelativePath(candidate)) ignoredPaths.add(candidate);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
scopePaths: safeReadPaths.size ? [...safeReadPaths].sort() : ["."],
|
|
295
|
+
ignoredPaths: [...ignoredPaths].sort(),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function candidateFile(relativePath) {
|
|
300
|
+
const basename = path.posix.basename(toPosix(relativePath));
|
|
301
|
+
if (CONFIG_FILENAMES.has(basename)) return true;
|
|
302
|
+
return TEXT_EXTENSIONS.has(path.extname(basename));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function collectFiles(projectRoot, scopePaths, ignoredPaths) {
|
|
306
|
+
const files = [];
|
|
307
|
+
const skipped = [];
|
|
308
|
+
|
|
309
|
+
for (const scopePath of scopePaths) {
|
|
310
|
+
if (!safeRelativePath(scopePath) && scopePath !== ".") {
|
|
311
|
+
skipped.push({ path: scopePath, reason: "unsafe scope path" });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const absolute = path.resolve(projectRoot, scopePath);
|
|
315
|
+
if (!inside(projectRoot, absolute)) {
|
|
316
|
+
skipped.push({ path: scopePath, reason: "scope escapes project root" });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!fs.existsSync(absolute)) {
|
|
320
|
+
skipped.push({ path: scopePath, reason: "scope path not found" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
walkPath(projectRoot, absolute, ignoredPaths, files, skipped);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { files: [...new Set(files)].sort(), skipped };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function walkPath(projectRoot, absolute, ignoredPaths, files, skipped) {
|
|
330
|
+
const relative = toPosix(path.relative(projectRoot, absolute)) || ".";
|
|
331
|
+
if (relative !== "." && ignoredBy(relative, ignoredPaths)) {
|
|
332
|
+
skipped.push({ path: relative, reason: "ignored path" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (relative !== "." && secretBearingPath(relative)) {
|
|
336
|
+
skipped.push({ path: relative, reason: "secret-bearing path excluded" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const stat = fs.lstatSync(absolute);
|
|
341
|
+
if (stat.isSymbolicLink()) {
|
|
342
|
+
skipped.push({ path: relative, reason: "symbolic link skipped" });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (stat.isDirectory()) {
|
|
346
|
+
for (const entry of fs.readdirSync(absolute)) {
|
|
347
|
+
walkPath(projectRoot, path.join(absolute, entry), ignoredPaths, files, skipped);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!stat.isFile()) return;
|
|
352
|
+
if (stat.size > 512_000) {
|
|
353
|
+
skipped.push({ path: relative, reason: "file larger than bounded read limit" });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!candidateFile(relative)) {
|
|
357
|
+
skipped.push({ path: relative, reason: "not an env-audit candidate file" });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
files.push(relative);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function classifyReference(text, index, sourceKind) {
|
|
364
|
+
if (sourceKind === "sample") return "sample";
|
|
365
|
+
const window = text.slice(Math.max(0, index - 80), index + 120).toLowerCase();
|
|
366
|
+
if (/\b(required|must|throw|missing|requiredenv)\b|!\s*(?:[;),\]}]|$)/.test(window)) {
|
|
367
|
+
return "required";
|
|
368
|
+
}
|
|
369
|
+
if (/\b(optional|fallback|default)\b|\?\?|(?:\|\|)/.test(window)) {
|
|
370
|
+
return "optional";
|
|
371
|
+
}
|
|
372
|
+
return "inferred";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function addReference(map, name, record) {
|
|
376
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) return;
|
|
377
|
+
if (!map.has(name)) {
|
|
378
|
+
map.set(name, {
|
|
379
|
+
name,
|
|
380
|
+
classifications: new Set(),
|
|
381
|
+
references: [],
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const target = map.get(name);
|
|
385
|
+
target.classifications.add(record.classification);
|
|
386
|
+
const marker = `${record.path}:${record.source}`;
|
|
387
|
+
if (!target.references.some((reference) => `${reference.path}:${reference.source}` === marker)) {
|
|
388
|
+
target.references.push(record);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function scanEnvReferences(projectRoot, files) {
|
|
393
|
+
const variables = new Map();
|
|
394
|
+
const sampleFiles = [];
|
|
395
|
+
const warnings = [];
|
|
396
|
+
|
|
397
|
+
for (const relative of files) {
|
|
398
|
+
const absolute = path.join(projectRoot, relative);
|
|
399
|
+
let text = "";
|
|
400
|
+
try {
|
|
401
|
+
text = fs.readFileSync(absolute, "utf8");
|
|
402
|
+
} catch {
|
|
403
|
+
warnings.push(`could not read ${relative}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const basename = path.posix.basename(relative);
|
|
408
|
+
const sourceKind = basename === ".env.example" ? "sample" : "reference";
|
|
409
|
+
if (sourceKind === "sample") sampleFiles.push(relative);
|
|
410
|
+
|
|
411
|
+
const patterns = [
|
|
412
|
+
{ source: "process.env", regex: /\bprocess\.env\.([A-Z][A-Z0-9_]*)\b/g },
|
|
413
|
+
{ source: "process.env", regex: /\bprocess\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/g },
|
|
414
|
+
{ source: "import.meta.env", regex: /\bimport\.meta\.env\.([A-Z][A-Z0-9_]*)\b/g },
|
|
415
|
+
{ source: "deno.env", regex: /\bDeno\.env\.get\(['"]([A-Z][A-Z0-9_]*)['"]\)/g },
|
|
416
|
+
{ source: "env-function", regex: /\benv\(['"]([A-Z][A-Z0-9_]*)['"]\)/g },
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
if (sourceKind === "sample") {
|
|
420
|
+
patterns.push({
|
|
421
|
+
source: ".env.example",
|
|
422
|
+
regex: /^([A-Z][A-Z0-9_]*)\s*=/gm,
|
|
423
|
+
});
|
|
424
|
+
} else if (/\.(?:md|txt)$/i.test(relative)) {
|
|
425
|
+
patterns.push({
|
|
426
|
+
source: "documented-name",
|
|
427
|
+
regex:
|
|
428
|
+
/\b([A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|DATABASE_URL|URL|HOST|PORT|ENV|ID))\b/g,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const pattern of patterns) {
|
|
433
|
+
for (const match of text.matchAll(pattern.regex)) {
|
|
434
|
+
addReference(variables, match[1], {
|
|
435
|
+
path: relative,
|
|
436
|
+
source: pattern.source,
|
|
437
|
+
classification: classifyReference(text, match.index ?? 0, sourceKind),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
variables: [...variables.values()]
|
|
445
|
+
.map((record) => ({
|
|
446
|
+
...record,
|
|
447
|
+
classifications: [...record.classifications].sort(),
|
|
448
|
+
references: record.references.sort((a, b) => a.path.localeCompare(b.path)),
|
|
449
|
+
}))
|
|
450
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
451
|
+
sampleFiles: [...new Set(sampleFiles)].sort(),
|
|
452
|
+
warnings,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function buildEnvAuditReport(projectRootInput, options = {}) {
|
|
457
|
+
const coreRoot = options.coreRoot ?? DEFAULT_CORE_ROOT;
|
|
458
|
+
const context = adapterContext(projectRootInput, coreRoot);
|
|
459
|
+
if (!context.ok) {
|
|
460
|
+
return {
|
|
461
|
+
status: "failed",
|
|
462
|
+
coreVersion: PILOT_VERSION,
|
|
463
|
+
projectRoot: path.resolve(projectRootInput ?? "."),
|
|
464
|
+
adapter: context,
|
|
465
|
+
git: gitSummary(path.resolve(projectRootInput ?? ".")),
|
|
466
|
+
scopePaths: [],
|
|
467
|
+
ignoredPaths: DEFAULT_IGNORED_PATHS,
|
|
468
|
+
filesScanned: [],
|
|
469
|
+
variables: [],
|
|
470
|
+
sampleFiles: [],
|
|
471
|
+
skipped: [],
|
|
472
|
+
warnings: context.codes ?? [],
|
|
473
|
+
notVerified: NOT_VERIFIED,
|
|
474
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const projectRoot = context.projectRoot;
|
|
479
|
+
const git = gitSummary(projectRoot);
|
|
480
|
+
|
|
481
|
+
if (context.present && !context.enabled) {
|
|
482
|
+
return {
|
|
483
|
+
status: "partial",
|
|
484
|
+
coreVersion: PILOT_VERSION,
|
|
485
|
+
projectRoot,
|
|
486
|
+
adapter: context,
|
|
487
|
+
git,
|
|
488
|
+
scopePaths: [],
|
|
489
|
+
ignoredPaths: DEFAULT_IGNORED_PATHS,
|
|
490
|
+
filesScanned: [],
|
|
491
|
+
variables: [],
|
|
492
|
+
sampleFiles: [],
|
|
493
|
+
skipped: [
|
|
494
|
+
{
|
|
495
|
+
path: ".",
|
|
496
|
+
reason: "project adapter is present but does not enable env-audit",
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
warnings: [
|
|
500
|
+
"env-audit is not enabled by the project adapter; target files were not read",
|
|
501
|
+
...git.warnings,
|
|
502
|
+
],
|
|
503
|
+
notVerified: NOT_VERIFIED,
|
|
504
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const scope = context.enabled
|
|
509
|
+
? scanScopeFromAdapter(context)
|
|
510
|
+
: { scopePaths: ["."], ignoredPaths: DEFAULT_IGNORED_PATHS };
|
|
511
|
+
const collected = collectFiles(projectRoot, scope.scopePaths, scope.ignoredPaths);
|
|
512
|
+
const scanned = scanEnvReferences(projectRoot, collected.files);
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
status: "complete",
|
|
516
|
+
coreVersion: PILOT_VERSION,
|
|
517
|
+
projectRoot,
|
|
518
|
+
adapter: context,
|
|
519
|
+
git,
|
|
520
|
+
scopePaths: scope.scopePaths,
|
|
521
|
+
ignoredPaths: scope.ignoredPaths,
|
|
522
|
+
filesScanned: collected.files,
|
|
523
|
+
variables: scanned.variables,
|
|
524
|
+
sampleFiles: scanned.sampleFiles,
|
|
525
|
+
skipped: collected.skipped,
|
|
526
|
+
warnings: [
|
|
527
|
+
...git.warnings,
|
|
528
|
+
...scanned.warnings,
|
|
529
|
+
...(context.enabled ? ["env-audit used adapter-declared safe read paths only"] : []),
|
|
530
|
+
...(context.present ? [] : ["no project adapter declaration found; env-audit used generic bounded static scan"]),
|
|
531
|
+
],
|
|
532
|
+
notVerified: NOT_VERIFIED,
|
|
533
|
+
refusedBehavior: REFUSED_BEHAVIOR,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function renderEnvAuditReport(report) {
|
|
538
|
+
const lines = [
|
|
539
|
+
"# Env Audit Report",
|
|
540
|
+
"",
|
|
541
|
+
`Status: ${report.status}`,
|
|
542
|
+
`Core version: ${report.coreVersion}`,
|
|
543
|
+
`Project root: ${report.projectRoot}`,
|
|
544
|
+
"",
|
|
545
|
+
"## Git State",
|
|
546
|
+
`- Git root: ${report.git.root ?? "not detected"}`,
|
|
547
|
+
`- Branch state: ${report.git.branchState ?? "not detected"}`,
|
|
548
|
+
`- Local changes: ${report.git.hasUncommittedChanges ? "detected" : "not detected"}`,
|
|
549
|
+
"",
|
|
550
|
+
"## Adapter Scope",
|
|
551
|
+
`- Adapter present: ${report.adapter.present ? "yes" : "no"}`,
|
|
552
|
+
`- Env-audit enabled: ${report.adapter.enabled ? "yes" : "no"}`,
|
|
553
|
+
`- Mode: ${report.adapter.mode}`,
|
|
554
|
+
"",
|
|
555
|
+
"## Scope Paths",
|
|
556
|
+
...(report.scopePaths.length ? report.scopePaths.map((item) => `- ${item}`) : ["- none"]),
|
|
557
|
+
"",
|
|
558
|
+
"## Ignored Paths",
|
|
559
|
+
...report.ignoredPaths.map((item) => `- ${item}`),
|
|
560
|
+
"",
|
|
561
|
+
"## Summary",
|
|
562
|
+
`- Static files scanned: ${report.filesScanned.length}`,
|
|
563
|
+
`- Environment variable names found: ${report.variables.length}`,
|
|
564
|
+
`- Sample env files inspected: ${report.sampleFiles.length}`,
|
|
565
|
+
`- Skipped items: ${report.skipped.length}`,
|
|
566
|
+
"",
|
|
567
|
+
"## Environment Variables",
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
if (report.variables.length) {
|
|
571
|
+
for (const variable of report.variables) {
|
|
572
|
+
lines.push(`- ${variable.name} (${variable.classifications.join(", ")})`);
|
|
573
|
+
for (const reference of variable.references.slice(0, 8)) {
|
|
574
|
+
lines.push(` - ${reference.path} via ${reference.source}`);
|
|
575
|
+
}
|
|
576
|
+
if (variable.references.length > 8) {
|
|
577
|
+
lines.push(` - ${variable.references.length - 8} additional references omitted`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
lines.push("- none found");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
lines.push("", "## Sample Files Inspected");
|
|
585
|
+
if (report.sampleFiles.length) {
|
|
586
|
+
for (const file of report.sampleFiles) lines.push(`- ${file}`);
|
|
587
|
+
} else {
|
|
588
|
+
lines.push("- none");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
lines.push("", "## Skipped");
|
|
592
|
+
if (report.skipped.length) {
|
|
593
|
+
for (const skipped of report.skipped.slice(0, 40)) {
|
|
594
|
+
lines.push(`- ${skipped.path}: ${skipped.reason}`);
|
|
595
|
+
}
|
|
596
|
+
if (report.skipped.length > 40) {
|
|
597
|
+
lines.push(`- ${report.skipped.length - 40} additional skipped items omitted`);
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
lines.push("- none");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
lines.push("", "## Not Verified");
|
|
604
|
+
for (const item of report.notVerified) lines.push(`- ${item}`);
|
|
605
|
+
|
|
606
|
+
lines.push("", "## Warnings");
|
|
607
|
+
if (report.warnings.length) {
|
|
608
|
+
for (const warning of report.warnings) lines.push(`- ${warning}`);
|
|
609
|
+
} else {
|
|
610
|
+
lines.push("- none");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
lines.push("", "## Refused Behavior");
|
|
614
|
+
for (const item of report.refusedBehavior) lines.push(`- ${item}`);
|
|
615
|
+
|
|
616
|
+
lines.push(
|
|
617
|
+
"",
|
|
618
|
+
"No .env, secret-file, credential-store, API, build, test, deploy, migration, package-install, or project-write behavior was performed.",
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function envAuditCliResult(projectRootInput, options = {}) {
|
|
625
|
+
if (!projectRootInput) {
|
|
626
|
+
return {
|
|
627
|
+
exitCode: 2,
|
|
628
|
+
stream: "stderr",
|
|
629
|
+
lines: ["usage: node scripts/render-env-audit.mjs <project-root>"],
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const report = buildEnvAuditReport(projectRootInput, options);
|
|
634
|
+
return {
|
|
635
|
+
exitCode: report.status === "failed" ? 1 : 0,
|
|
636
|
+
stream: report.status === "failed" ? "stderr" : "stdout",
|
|
637
|
+
lines: renderEnvAuditReport(report).split("\n"),
|
|
638
|
+
report,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const PILOT_SKILLS = [
|
|
2
2
|
"repo-map",
|
|
3
|
+
"route-trace",
|
|
4
|
+
"env-audit",
|
|
3
5
|
"build-verify",
|
|
4
6
|
"git-preflight",
|
|
5
7
|
"runtime-truth",
|
|
@@ -11,6 +13,8 @@ export const PREVIOUS_PILOT_VERSION = "0.2.2";
|
|
|
11
13
|
|
|
12
14
|
export const AUDIT_ONLY_SKILLS = [
|
|
13
15
|
"repo-map",
|
|
16
|
+
"route-trace",
|
|
17
|
+
"env-audit",
|
|
14
18
|
"git-preflight",
|
|
15
19
|
"runtime-truth",
|
|
16
20
|
"llm-drift-control",
|
|
@@ -362,6 +366,20 @@ export function classifyTrigger(prompt) {
|
|
|
362
366
|
) {
|
|
363
367
|
return "build-verify";
|
|
364
368
|
}
|
|
369
|
+
if (
|
|
370
|
+
/\b(?:route trace|trace.*routes?|route surface|routes?.*static|api route files?|next\.js routes?|react router|express routes?|fastify routes?|hono routes?)\b/.test(
|
|
371
|
+
text,
|
|
372
|
+
)
|
|
373
|
+
) {
|
|
374
|
+
return "route-trace";
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
/\b(?:env audit|environment variables?|env vars?|process\.env|import\.meta\.env|\.env\.example|configuration requirements?)\b/.test(
|
|
378
|
+
text,
|
|
379
|
+
)
|
|
380
|
+
) {
|
|
381
|
+
return "env-audit";
|
|
382
|
+
}
|
|
365
383
|
if (
|
|
366
384
|
/\b(?:unfamiliar repository|canonical repository root|canonical repo|map the current packages|map this repository|identify its entry points|nested directory)\b/.test(
|
|
367
385
|
text,
|
|
@@ -503,7 +521,7 @@ function classifySegment(segment, options = {}) {
|
|
|
503
521
|
}
|
|
504
522
|
if (
|
|
505
523
|
executable === "node" &&
|
|
506
|
-
!/^node\s+(?:--check\b|--test\b|scripts\/(?:validate-pack|validate-maintainer-loop|validate-adapters|validate-project-adapters|check-adapter-upgrade|check-adapter-upgrade-chain|verify-evidence-bundle|render-evidence-archive-report|render-adapter-repo-map|test-pack)\.mjs\b)/.test(
|
|
524
|
+
!/^node\s+(?:--check\b|--test\b|scripts\/(?:validate-pack|validate-maintainer-loop|validate-adapters|validate-project-adapters|check-adapter-upgrade|check-adapter-upgrade-chain|verify-evidence-bundle|render-evidence-archive-report|render-adapter-repo-map|render-route-trace|render-env-audit|test-pack)\.mjs\b)/.test(
|
|
507
525
|
segment,
|
|
508
526
|
)
|
|
509
527
|
) {
|
|
@@ -513,7 +531,7 @@ function classifySegment(segment, options = {}) {
|
|
|
513
531
|
["coding-agent-skills", "bin/coding-agent-skills", "./bin/coding-agent-skills"].includes(
|
|
514
532
|
executable,
|
|
515
533
|
) &&
|
|
516
|
-
!/^(?:\.\/)?(?:bin\/)?coding-agent-skills\s+(?:validate-pack|validate-project\s+\S+|repo-map\s+\S+|validate-adapters\s+\S+|help|--help|-h)\s*$/.test(
|
|
534
|
+
!/^(?:\.\/)?(?:bin\/)?coding-agent-skills\s+(?:validate-pack|validate-project\s+\S+|repo-map\s+\S+|route-trace\s+\S+|env-audit\s+\S+|validate-adapters\s+\S+|help|--help|-h)\s*$/.test(
|
|
517
535
|
segment,
|
|
518
536
|
)
|
|
519
537
|
) {
|