nexus-agents 8.4.0 → 8.6.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/dist/ast-rule-runner-3N7DX6XO.js +29 -0
- package/dist/chunk-4OILK4VD.js +7 -0
- package/dist/{chunk-FURVUS67.js → chunk-6UWWVTQH.js} +6 -6
- package/dist/{chunk-HVPF7BBS.js → chunk-I33FKHUF.js} +2 -2
- package/dist/{chunk-2LQROHUE.js → chunk-QMUXBHKO.js} +3 -3
- package/dist/{chunk-CFGJGRVF.js → chunk-SNWGBSQ7.js} +37 -3
- package/dist/chunk-SNWGBSQ7.js.map +1 -0
- package/dist/chunk-WZZQI774.js +244 -0
- package/dist/chunk-WZZQI774.js.map +1 -0
- package/dist/{chunk-FBAEBGQ3.js → chunk-ZIZVA5OL.js} +2 -2
- package/dist/cli.js +88 -7
- package/dist/cli.js.map +1 -1
- package/dist/{consensus-vote-R5GXCSXV.js → consensus-vote-222ZEN5J.js} +4 -4
- package/dist/{improvement-review-UBDVNB55.js → improvement-review-GGYLCRGQ.js} +4 -4
- package/dist/index.js +149 -361
- package/dist/index.js.map +1 -1
- package/dist/{setup-data-dir-EYIPKPGP.js → setup-data-dir-T4S46VSI.js} +3 -3
- package/dist/setup-data-dir-T4S46VSI.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-CFGJGRVF.js.map +0 -1
- package/dist/chunk-M5DIGZIM.js +0 -7
- /package/dist/{consensus-vote-R5GXCSXV.js.map → ast-rule-runner-3N7DX6XO.js.map} +0 -0
- /package/dist/{chunk-M5DIGZIM.js.map → chunk-4OILK4VD.js.map} +0 -0
- /package/dist/{chunk-FURVUS67.js.map → chunk-6UWWVTQH.js.map} +0 -0
- /package/dist/{chunk-HVPF7BBS.js.map → chunk-I33FKHUF.js.map} +0 -0
- /package/dist/{chunk-2LQROHUE.js.map → chunk-QMUXBHKO.js.map} +0 -0
- /package/dist/{chunk-FBAEBGQ3.js.map → chunk-ZIZVA5OL.js.map} +0 -0
- /package/dist/{improvement-review-UBDVNB55.js.map → consensus-vote-222ZEN5J.js.map} +0 -0
- /package/dist/{setup-data-dir-EYIPKPGP.js.map → improvement-review-GGYLCRGQ.js.map} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SecurityError,
|
|
3
|
+
ValidationError,
|
|
4
|
+
createLogger,
|
|
5
|
+
formatZodError
|
|
6
|
+
} from "./chunk-DC2OJAIB.js";
|
|
7
|
+
|
|
8
|
+
// src/security/ast-rule-runner.ts
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
11
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { parse, registerDynamicLanguage } from "@ast-grep/napi";
|
|
14
|
+
import goLangRegistration from "@ast-grep/lang-go";
|
|
15
|
+
import pythonLangRegistration from "@ast-grep/lang-python";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var polyglotLangsRegistered = false;
|
|
19
|
+
function ensurePolyglotLangs() {
|
|
20
|
+
if (polyglotLangsRegistered) return;
|
|
21
|
+
registerDynamicLanguage({ python: pythonLangRegistration, go: goLangRegistration });
|
|
22
|
+
polyglotLangsRegistered = true;
|
|
23
|
+
}
|
|
24
|
+
var AST_RULE_LANGUAGES = ["python", "go"];
|
|
25
|
+
var AST_RULE_SEVERITIES = ["error", "warning", "info"];
|
|
26
|
+
var RuleFileSchema = z.object({
|
|
27
|
+
id: z.string().min(1),
|
|
28
|
+
language: z.enum(AST_RULE_LANGUAGES),
|
|
29
|
+
severity: z.enum(AST_RULE_SEVERITIES),
|
|
30
|
+
message: z.string().min(1),
|
|
31
|
+
rule: z.record(z.string(), z.unknown())
|
|
32
|
+
});
|
|
33
|
+
var MAX_SNIPPET_CHARS = 200;
|
|
34
|
+
var DEFAULT_AST_QA_LIMIT = 200;
|
|
35
|
+
var MAX_AST_QA_LIMIT = 2e3;
|
|
36
|
+
var AST_QA_MAX_DEPTH = 24;
|
|
37
|
+
var MAX_FILES_SCANNED = 5e3;
|
|
38
|
+
var logger = createLogger({ component: "ast-rule-runner" });
|
|
39
|
+
async function loadRules(dir) {
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = (await readdir(dir)).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).sort();
|
|
43
|
+
} catch (caught) {
|
|
44
|
+
const e = caught instanceof Error ? caught : new Error(String(caught));
|
|
45
|
+
throw new ValidationError(`ast-rule-runner: cannot read rules dir ${dir}: ${e.message}`, {
|
|
46
|
+
cause: e
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const rules = [];
|
|
50
|
+
for (const file of entries) {
|
|
51
|
+
const path = join(dir, file);
|
|
52
|
+
const raw = await readFile(path, "utf8");
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = parseYaml(raw);
|
|
56
|
+
} catch (caught) {
|
|
57
|
+
const e = caught instanceof Error ? caught : new Error(String(caught));
|
|
58
|
+
throw new ValidationError(`ast-rule-runner: malformed YAML in ${path}: ${e.message}`, {
|
|
59
|
+
cause: e
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const result = RuleFileSchema.safeParse(parsed);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
throw new ValidationError(
|
|
65
|
+
`ast-rule-runner: invalid rule file ${path}: ${formatZodError(result.error)}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
rules.push(result.data);
|
|
69
|
+
}
|
|
70
|
+
return rules;
|
|
71
|
+
}
|
|
72
|
+
function getBuiltInAstRulesPath() {
|
|
73
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
74
|
+
const currentDir = dirname(currentFile);
|
|
75
|
+
if (currentFile.includes(`${sep}dist${sep}`) || currentFile.endsWith(`${sep}dist`)) {
|
|
76
|
+
const distIndex = currentFile.lastIndexOf(`${sep}dist${sep}`);
|
|
77
|
+
if (distIndex !== -1) {
|
|
78
|
+
const distDir = currentFile.substring(0, distIndex + 5);
|
|
79
|
+
const bundledPath = join(distDir, "security", "ast-rules");
|
|
80
|
+
if (existsSync(bundledPath)) return bundledPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const possiblePaths = [
|
|
84
|
+
join(currentDir, "ast-rules"),
|
|
85
|
+
// Unbundled: sibling of this file in src/security/
|
|
86
|
+
join(currentDir, "security", "ast-rules"),
|
|
87
|
+
// Bundled chunk: dist/security sibling
|
|
88
|
+
join(dirname(currentDir), "security", "ast-rules")
|
|
89
|
+
];
|
|
90
|
+
for (const path of possiblePaths) {
|
|
91
|
+
if (existsSync(path)) return path;
|
|
92
|
+
}
|
|
93
|
+
return join(currentDir, "ast-rules");
|
|
94
|
+
}
|
|
95
|
+
function resolveWithinCwd(target) {
|
|
96
|
+
const resolved = resolve(target);
|
|
97
|
+
const cwdRoot = resolve(".");
|
|
98
|
+
if (resolved !== cwdRoot && !resolved.startsWith(cwdRoot + sep)) {
|
|
99
|
+
return { error: `Path traversal denied: scope must be within ${cwdRoot}` };
|
|
100
|
+
}
|
|
101
|
+
return { resolved };
|
|
102
|
+
}
|
|
103
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
104
|
+
var POLYGLOT_EXT_TO_LANG = {
|
|
105
|
+
".py": "python",
|
|
106
|
+
".go": "go"
|
|
107
|
+
};
|
|
108
|
+
function inferRuleLang(file) {
|
|
109
|
+
return POLYGLOT_EXT_TO_LANG[extname(file).toLowerCase()] ?? null;
|
|
110
|
+
}
|
|
111
|
+
async function findPolyglotFiles(dir, maxDepth) {
|
|
112
|
+
if (maxDepth <= 0) return [];
|
|
113
|
+
const out = [];
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
117
|
+
} catch (caught) {
|
|
118
|
+
const e = caught instanceof Error ? caught : new Error(String(caught));
|
|
119
|
+
logger.warn(`ast-rule-runner: skipped unreadable directory ${dir}: ${e.message}`);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const fullPath = join(dir, entry.name);
|
|
124
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
125
|
+
out.push(...await findPolyglotFiles(fullPath, maxDepth - 1));
|
|
126
|
+
} else if (entry.isFile() && inferRuleLang(entry.name) !== null) {
|
|
127
|
+
out.push(fullPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function langToParseArg(lang) {
|
|
133
|
+
ensurePolyglotLangs();
|
|
134
|
+
return lang;
|
|
135
|
+
}
|
|
136
|
+
function scanFile(src, lang, applicable, budget) {
|
|
137
|
+
const { rel, limit, alreadyFound } = budget;
|
|
138
|
+
const root = parse(langToParseArg(lang), src).root();
|
|
139
|
+
const lines = src.split("\n");
|
|
140
|
+
const findings = [];
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (const rule of applicable) {
|
|
143
|
+
const config = { rule: rule.rule };
|
|
144
|
+
for (const node of root.findAll(config)) {
|
|
145
|
+
total += 1;
|
|
146
|
+
if (alreadyFound + findings.length < limit) {
|
|
147
|
+
const { start } = node.range();
|
|
148
|
+
const rawLine = lines[start.line] ?? node.text();
|
|
149
|
+
findings.push({
|
|
150
|
+
ruleId: rule.id,
|
|
151
|
+
severity: rule.severity,
|
|
152
|
+
message: rule.message,
|
|
153
|
+
file: rel,
|
|
154
|
+
line: start.line + 1,
|
|
155
|
+
column: start.column + 1,
|
|
156
|
+
snippet: rawLine.trim().slice(0, MAX_SNIPPET_CHARS)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { findings, total };
|
|
162
|
+
}
|
|
163
|
+
async function assertScanRootIsDirectory(resolved) {
|
|
164
|
+
let rootStat;
|
|
165
|
+
try {
|
|
166
|
+
rootStat = await stat(resolved);
|
|
167
|
+
} catch (caught) {
|
|
168
|
+
const e = caught instanceof Error ? caught : new Error(String(caught));
|
|
169
|
+
throw new ValidationError(`ast-rule-runner: cannot read targetDir ${resolved}: ${e.message}`, {
|
|
170
|
+
cause: e
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (!rootStat.isDirectory()) {
|
|
174
|
+
throw new ValidationError(
|
|
175
|
+
`ast-rule-runner: targetDir must be a directory, got a non-directory path: ${resolved}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function resolveScanFiles(root) {
|
|
180
|
+
const discovered = await findPolyglotFiles(root, AST_QA_MAX_DEPTH);
|
|
181
|
+
const files = discovered.slice(0, MAX_FILES_SCANNED);
|
|
182
|
+
const filesSkipped = discovered.length - files.length;
|
|
183
|
+
const filesTruncated = filesSkipped > 0;
|
|
184
|
+
if (filesTruncated) {
|
|
185
|
+
logger.warn(
|
|
186
|
+
`ast-rule-runner: ${String(filesSkipped)} file(s) NOT scanned \u2014 file cap ${String(MAX_FILES_SCANNED)} exceeded (discovered=${String(discovered.length)}). Scan is PARTIAL; do not treat findings as exhaustive.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return { files, filesSkipped, filesTruncated };
|
|
190
|
+
}
|
|
191
|
+
async function collectAstQaFindings(opts) {
|
|
192
|
+
const guard = resolveWithinCwd(opts.targetDir);
|
|
193
|
+
if ("error" in guard) {
|
|
194
|
+
throw new SecurityError(guard.error);
|
|
195
|
+
}
|
|
196
|
+
await assertScanRootIsDirectory(guard.resolved);
|
|
197
|
+
const rulesDir = opts.rulesDir ?? getBuiltInAstRulesPath();
|
|
198
|
+
const rules = await loadRules(rulesDir);
|
|
199
|
+
const limit = Math.min(Math.max(opts.limit ?? DEFAULT_AST_QA_LIMIT, 1), MAX_AST_QA_LIMIT);
|
|
200
|
+
const { files, filesSkipped, filesTruncated } = await resolveScanFiles(guard.resolved);
|
|
201
|
+
const findings = [];
|
|
202
|
+
let total = 0;
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const lang = inferRuleLang(file);
|
|
205
|
+
if (lang === null) continue;
|
|
206
|
+
const applicable = rules.filter((r) => r.language === lang);
|
|
207
|
+
if (applicable.length === 0) continue;
|
|
208
|
+
let src;
|
|
209
|
+
try {
|
|
210
|
+
src = await readFile(file, "utf8");
|
|
211
|
+
} catch {
|
|
212
|
+
logger.warn(`ast-rule-runner: skipped unreadable file ${file}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const rel = relative(guard.resolved, file) || file;
|
|
216
|
+
const scanned = scanFile(src, lang, applicable, { rel, limit, alreadyFound: findings.length });
|
|
217
|
+
findings.push(...scanned.findings);
|
|
218
|
+
total += scanned.total;
|
|
219
|
+
}
|
|
220
|
+
if (total > findings.length) {
|
|
221
|
+
logger.warn(
|
|
222
|
+
`ast-rule-runner: ${String(total - findings.length)} finding(s) omitted beyond limit=${String(limit)} (total=${String(total)})`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return { findings, total, limit, filesScanned: files.length, filesSkipped, filesTruncated };
|
|
226
|
+
}
|
|
227
|
+
async function runAstQaRules(opts) {
|
|
228
|
+
const { findings } = await collectAstQaFindings(opts);
|
|
229
|
+
return findings;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
ensurePolyglotLangs,
|
|
234
|
+
AST_RULE_LANGUAGES,
|
|
235
|
+
AST_RULE_SEVERITIES,
|
|
236
|
+
DEFAULT_AST_QA_LIMIT,
|
|
237
|
+
MAX_AST_QA_LIMIT,
|
|
238
|
+
MAX_FILES_SCANNED,
|
|
239
|
+
loadRules,
|
|
240
|
+
getBuiltInAstRulesPath,
|
|
241
|
+
collectAstQaFindings,
|
|
242
|
+
runAstQaRules
|
|
243
|
+
};
|
|
244
|
+
//# sourceMappingURL=chunk-WZZQI774.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/security/ast-rule-runner.ts"],"sourcesContent":["/**\n * nexus-agents/security - Polyglot (Python/Go) AST QA/Security Rule Runner (#4249 child C)\n *\n * Runs YAML-defined `@ast-grep/napi` rules against Python/Go source, detecting\n * common QA/security anti-patterns (dynamic `eval`/`exec`, shell injection,\n * unchecked external command execution). Same engine already used by\n * `search_usages` (indexer/usage-ast.ts, #4265) and the ast-fixer rewrites\n * (agents/collaboration/ast-rewrites.ts, #4243/#4249 child B) — no new AST\n * dependency, just new language grammars for it.\n *\n * **Language support gap.** `Lang` in `@ast-grep/napi@0.44.1` ships only\n * Html/JavaScript/Tsx/Css/TypeScript — no Python, no Go. This module closes\n * that gap via napi's `@experimental registerDynamicLanguage` API, backed by\n * the `@ast-grep/lang-python`/`@ast-grep/lang-go` prebuilt tree-sitter\n * grammars. Both are EXACT-pinned in package.json (no caret): these are\n * early/experimental packages and a caret range could silently swap in an\n * incompatible native `.so` grammar build underneath a \"safe\" semver-minor\n * bump. `registerDynamicLanguage` throws if called more than once per\n * process (napi-rs's own documented constraint) — {@link ensurePolyglotLangs}\n * is a lazy, module-level, once-only guard so repeated calls (tests, repeat\n * runner invocations within one process) never trip it twice.\n *\n * **Read-only.** Reads rule YAML and target source files, walks their ASTs,\n * performs NO writes. ast-grep YAML rule files support a `fix:` key for\n * autofixing, but napi's `SgNode.findAll` (used here) never applies it —\n * only ast-grep's separate rewrite/CLI machinery does — so even a rule\n * author who adds `fix:` cannot turn this runner into a writer by accident.\n * MCP exposure is deliberately deferred (see the follow-up issue referenced\n * in the #4249 child C PR) to keep this surface a plain function until a\n * named consumer needs it wired through a tool.\n *\n * @module security/ast-rule-runner\n * @see Issue #4249 - epic: ast-grep adoption (Child C: polyglot QA/security rules)\n */\n\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { dirname, extname, join, relative, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parse, registerDynamicLanguage } from '@ast-grep/napi';\nimport type { NapiConfig } from '@ast-grep/napi';\n// Both packages are CJS `export = <LanguageRegistration>` modules — the\n// default import is the whole registration object (`libraryPath` getter,\n// `extensions`, `languageSymbol`, `expandoChar`). See their `index.d.ts`.\nimport goLangRegistration from '@ast-grep/lang-go';\nimport pythonLangRegistration from '@ast-grep/lang-python';\nimport { parse as parseYaml } from 'yaml';\nimport { z } from 'zod';\nimport { createLogger, SecurityError, ValidationError, formatZodError } from '../core/index.js';\n\n// ============================================================================\n// Dynamic language registration (@experimental, call-once)\n// ============================================================================\n\nlet polyglotLangsRegistered = false;\n\n/**\n * Lazily register the Python/Go tree-sitter grammars with ast-grep's\n * `registerDynamicLanguage`. That API throws if called more than once in a\n * process; this guard makes every call after the first a no-op so repeated\n * runner invocations (or a test suite that constructs the runner many times)\n * never trip the constraint.\n */\nexport function ensurePolyglotLangs(): void {\n if (polyglotLangsRegistered) return;\n registerDynamicLanguage({ python: pythonLangRegistration, go: goLangRegistration });\n polyglotLangsRegistered = true;\n}\n\n// ============================================================================\n// Rule schema (fail-closed)\n// ============================================================================\n\n/** Languages a rule file may declare. Deliberately narrowed to the two this\n * runner actually SCANS (python/go): admitting `typescript`/`javascript` here\n * would let a TS/JS rule file validate at load time and then silently never\n * fire (there is no TS/JS extension in {@link POLYGLOT_EXT_TO_LANG}), which is\n * a fail-OPEN gap for a security scanner where \"no findings\" reads as \"clean\".\n * TS/JS structural analysis is already covered by `search_usages` (#4265) and\n * the ast-fixer rewrites (#4243); a TS/JS rule here would fail LOUD at load\n * (unknown-language Zod error) instead of loading dead. */\nexport const AST_RULE_LANGUAGES = ['python', 'go'] as const;\nexport type AstRuleLanguage = (typeof AST_RULE_LANGUAGES)[number];\n\nexport const AST_RULE_SEVERITIES = ['error', 'warning', 'info'] as const;\nexport type AstRuleSeverity = (typeof AST_RULE_SEVERITIES)[number];\n\n/** Schema for one `*.yml` rule file. `rule` is passed through as ast-grep's\n * own `Rule` object (napi already validates it structurally at match time);\n * Zod only pins down the envelope so an unknown `language` or a missing\n * field FAILS CLOSED instead of silently loading a partial/broken rule. */\nconst RuleFileSchema = z.object({\n id: z.string().min(1),\n language: z.enum(AST_RULE_LANGUAGES),\n severity: z.enum(AST_RULE_SEVERITIES),\n message: z.string().min(1),\n rule: z.record(z.string(), z.unknown()),\n});\n\nexport type AstQaRuleFile = z.infer<typeof RuleFileSchema>;\n\n/** Per-match snippet char cap (matches indexer/usage-ast.ts's discipline). */\nconst MAX_SNIPPET_CHARS = 200;\n\n/** Default cap on emitted findings. Excess is counted + reported, never silently dropped. */\nexport const DEFAULT_AST_QA_LIMIT = 200;\n/** Hard upper bound a caller may request for the finding cap. */\nexport const MAX_AST_QA_LIMIT = 2000;\n/** Directory walk depth for the target-dir scan (not caller-configurable — no\n * named consumer needs a deeper/shallower walk yet; YAGNI). */\nconst AST_QA_MAX_DEPTH = 24;\n/** Upper bound on files parsed in a single run, to bound worst-case cost.\n * Exceeding it makes the scan PARTIAL — surfaced via {@link AstQaCollectResult}'s\n * `filesTruncated`/`filesSkipped` + a warn, never silently dropped. */\nexport const MAX_FILES_SCANNED = 5000;\n\nconst logger = createLogger({ component: 'ast-rule-runner' });\n\n// ============================================================================\n// Rule loading\n// ============================================================================\n\n/**\n * Load and Zod-validate every `*.yml` rule file in `dir`. FAILS CLOSED: a\n * single malformed YAML file, or a file with an unrecognized `language` or\n * missing field, throws a {@link ValidationError} — there is no\n * \"load what parsed, skip the rest\" partial mode.\n */\nexport async function loadRules(dir: string): Promise<AstQaRuleFile[]> {\n let entries: string[];\n try {\n entries = (await readdir(dir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')).sort();\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: cannot read rules dir ${dir}: ${e.message}`, {\n cause: e,\n });\n }\n\n const rules: AstQaRuleFile[] = [];\n for (const file of entries) {\n const path = join(dir, file);\n const raw = await readFile(path, 'utf8');\n\n let parsed: unknown;\n try {\n parsed = parseYaml(raw);\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: malformed YAML in ${path}: ${e.message}`, {\n cause: e,\n });\n }\n\n const result = RuleFileSchema.safeParse(parsed);\n if (!result.success) {\n throw new ValidationError(\n `ast-rule-runner: invalid rule file ${path}: ${formatZodError(result.error)}`\n );\n }\n rules.push(result.data);\n }\n return rules;\n}\n\n// ============================================================================\n// Built-in rules dir resolution (mirrors workflows/template-loader.ts)\n// ============================================================================\n\n/**\n * Resolve the built-in `ast-rules/` directory, handling both dev (unbundled,\n * `src/security/ast-rule-runner.ts`) and published (bundled,\n * `dist/index.js` + `dist/security/ast-rules/*.yml` copied by tsup's\n * `onSuccess` step) layouts — the same two-layout problem\n * `getBuiltInTemplatesPath` solves for workflow templates.\n */\nexport function getBuiltInAstRulesPath(): string {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n\n if (currentFile.includes(`${sep}dist${sep}`) || currentFile.endsWith(`${sep}dist`)) {\n const distIndex = currentFile.lastIndexOf(`${sep}dist${sep}`);\n if (distIndex !== -1) {\n const distDir = currentFile.substring(0, distIndex + 5); // +5 for \"/dist\"\n const bundledPath = join(distDir, 'security', 'ast-rules');\n if (existsSync(bundledPath)) return bundledPath;\n }\n }\n\n const possiblePaths = [\n join(currentDir, 'ast-rules'), // Unbundled: sibling of this file in src/security/\n join(currentDir, 'security', 'ast-rules'), // Bundled chunk: dist/security sibling\n join(dirname(currentDir), 'security', 'ast-rules'),\n ];\n for (const path of possiblePaths) {\n if (existsSync(path)) return path;\n }\n return join(currentDir, 'ast-rules'); // Fallback (for error reporting)\n}\n\n// ============================================================================\n// Path-traversal guard (copied from mcp/tools/search-usages-tool.ts —\n// keep in sync if that guard changes)\n// ============================================================================\n\n/** Resolve a caller path against a path-traversal guard (must stay within cwd). */\nfunction resolveWithinCwd(target: string): { resolved: string } | { error: string } {\n const resolved = resolve(target);\n const cwdRoot = resolve('.');\n // The `+ sep` is load-bearing: a sibling dir whose name starts with the cwd\n // basename bypasses a bare startsWith. Matches security/safe-path.ts.\n if (resolved !== cwdRoot && !resolved.startsWith(cwdRoot + sep)) {\n return { error: `Path traversal denied: scope must be within ${cwdRoot}` };\n }\n return { resolved };\n}\n\n// ============================================================================\n// File walk (LOCAL extension map — python/go only; does NOT reuse\n// indexer/usage-ast.ts's TS/JS inferLang or indexer/codebase-search.ts's\n// TS/JS-only findSourceFiles)\n// ============================================================================\n\nconst SKIP_DIRS = new Set(['node_modules', 'dist', '.git']);\n\n/** file-extension -> ast-rule language, for the two languages this runner scans. */\nconst POLYGLOT_EXT_TO_LANG: Readonly<Record<string, AstRuleLanguage>> = {\n '.py': 'python',\n '.go': 'go',\n};\n\n/** Infer the ast-rule language for a file from its extension, or `null` if unsupported. */\nfunction inferRuleLang(file: string): AstRuleLanguage | null {\n return POLYGLOT_EXT_TO_LANG[extname(file).toLowerCase()] ?? null;\n}\n\n/**\n * Recursively collect `.py`/`.go` files under `dir`, bounded by `maxDepth`.\n * A subdirectory whose `readdir` fails mid-walk (e.g. a `chmod 000` dir) is\n * WARNED about per-dir and skipped rather than silently swallowed — the scan\n * continues, but the partiality is surfaced in the log. The SCAN ROOT's own\n * readability is validated up front by {@link collectAstQaFindings} (fail\n * loud), so this soft-skip only ever applies to descendant dirs.\n */\nasync function findPolyglotFiles(dir: string, maxDepth: number): Promise<string[]> {\n if (maxDepth <= 0) return [];\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n logger.warn(`ast-rule-runner: skipped unreadable directory ${dir}: ${e.message}`);\n return out;\n }\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {\n out.push(...(await findPolyglotFiles(fullPath, maxDepth - 1)));\n } else if (entry.isFile() && inferRuleLang(entry.name) !== null) {\n out.push(fullPath);\n }\n }\n return out;\n}\n\n/** Map a rule's declared language to the `parse()` first argument, registering\n * the dynamic grammars on first use. Both supported languages (python/go) are\n * dynamically-registered CustomLangs, so the language name string IS the\n * `parse()` key set up in {@link ensurePolyglotLangs}. */\nfunction langToParseArg(lang: AstRuleLanguage): AstRuleLanguage {\n ensurePolyglotLangs();\n return lang; // the CustomLang string keys used in registerDynamicLanguage()\n}\n\n// ============================================================================\n// Findings\n// ============================================================================\n\n/** One ast-grep rule match against a source file. */\nexport interface AstRuleFinding {\n ruleId: string;\n severity: AstRuleSeverity;\n message: string;\n /** Path relative to the resolved `targetDir`. */\n file: string;\n /** 1-based line number. */\n line: number;\n /** 1-based column number. */\n column: number;\n /** Trimmed, length-capped source line for the match. */\n snippet: string;\n}\n\nexport interface RunAstQaRulesOptions {\n /** Directory containing `*.yml` rule files (default: the built-in bundled rules). */\n rulesDir?: string;\n /** Directory to scan for `.py`/`.go` source files (must stay within cwd). */\n targetDir: string;\n /** Max findings emitted (default {@link DEFAULT_AST_QA_LIMIT}). Excess is counted + reported. */\n limit?: number;\n}\n\n/** {@link collectAstQaFindings}'s full result — findings plus the true total,\n * so overflow beyond `limit` is counted and reported, never silently dropped. */\nexport interface AstQaCollectResult {\n findings: AstRuleFinding[];\n /** True count of matches found, before the `limit` cap was applied. */\n total: number;\n limit: number;\n /** Number of `.py`/`.go` files actually scanned (after the file cap). */\n filesScanned: number;\n /** Number of discovered files DROPPED because the file cap ({@link MAX_FILES_SCANNED})\n * was exceeded. Non-zero means the scan was PARTIAL — a downstream consumer\n * must not treat empty/low findings as a clean bill of health. */\n filesSkipped: number;\n /** True iff `filesSkipped > 0` — the scan did not cover every candidate file. */\n filesTruncated: boolean;\n}\n\n/** Findings + true-match-count from scanning ONE already-read source file. */\ninterface FileScanResult {\n findings: AstRuleFinding[];\n total: number;\n}\n\n/** Bundles {@link scanFile}'s cap-tracking inputs (max-params discipline). */\ninterface FileScanBudget {\n /** Path emitted on each finding (relative to the resolved `targetDir`). */\n rel: string;\n /** Max TOTAL findings across the whole run. */\n limit: number;\n /** Findings already accepted by earlier files in this run. */\n alreadyFound: number;\n}\n\n/**\n * Scan one already-read source file against the rules applicable to its\n * language, pushing findings only while the running total (`budget.alreadyFound`\n * + findings pushed so far) stays under `budget.limit` — while still counting\n * every true match, so the caller can report overflow instead of silently\n * dropping it.\n */\nfunction scanFile(\n src: string,\n lang: AstRuleLanguage,\n applicable: readonly AstQaRuleFile[],\n budget: FileScanBudget\n): FileScanResult {\n const { rel, limit, alreadyFound } = budget;\n const root = parse(langToParseArg(lang), src).root();\n const lines = src.split('\\n');\n const findings: AstRuleFinding[] = [];\n let total = 0;\n\n for (const rule of applicable) {\n const config: NapiConfig = { rule: rule.rule };\n for (const node of root.findAll(config)) {\n total += 1;\n if (alreadyFound + findings.length < limit) {\n const { start } = node.range();\n const rawLine = lines[start.line] ?? node.text();\n findings.push({\n ruleId: rule.id,\n severity: rule.severity,\n message: rule.message,\n file: rel,\n line: start.line + 1,\n column: start.column + 1,\n snippet: rawLine.trim().slice(0, MAX_SNIPPET_CHARS),\n });\n }\n }\n }\n return { findings, total };\n}\n\n/**\n * Fail LOUD on an unreadable / non-directory scan root. `loadRules` already\n * throws on an unreadable RULES dir; the TARGET traversal must not fail OPEN —\n * a lone file path, a bad path, or a `000` root would otherwise return an empty\n * finding set that reads as \"clean\" for a security scanner.\n */\nasync function assertScanRootIsDirectory(resolved: string): Promise<void> {\n let rootStat;\n try {\n rootStat = await stat(resolved);\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: cannot read targetDir ${resolved}: ${e.message}`, {\n cause: e,\n });\n }\n if (!rootStat.isDirectory()) {\n throw new ValidationError(\n `ast-rule-runner: targetDir must be a directory, got a non-directory path: ${resolved}`\n );\n }\n}\n\n/** Result of resolving the candidate file set under a scan root, with the\n * file-cap truncation signal surfaced (never a silent `.slice`). */\ninterface ScanFileSet {\n files: string[];\n filesSkipped: number;\n filesTruncated: boolean;\n}\n\n/** Discover candidate `.py`/`.go` files, applying + SURFACING the file cap. */\nasync function resolveScanFiles(root: string): Promise<ScanFileSet> {\n const discovered = await findPolyglotFiles(root, AST_QA_MAX_DEPTH);\n const files = discovered.slice(0, MAX_FILES_SCANNED);\n const filesSkipped = discovered.length - files.length;\n const filesTruncated = filesSkipped > 0;\n if (filesTruncated) {\n logger.warn(\n `ast-rule-runner: ${String(filesSkipped)} file(s) NOT scanned — file cap ` +\n `${String(MAX_FILES_SCANNED)} exceeded (discovered=${String(discovered.length)}). ` +\n `Scan is PARTIAL; do not treat findings as exhaustive.`\n );\n }\n return { files, filesSkipped, filesTruncated };\n}\n\n/**\n * Run every applicable rule in `rulesDir` against every `.py`/`.go` file under\n * `targetDir`, returning capped findings plus the true total (so callers that\n * need the overflow count — e.g. a future MCP tool wrapper — can report it).\n * {@link runAstQaRules} is the capped-array convenience wrapper over this.\n */\nexport async function collectAstQaFindings(\n opts: RunAstQaRulesOptions\n): Promise<AstQaCollectResult> {\n const guard = resolveWithinCwd(opts.targetDir);\n if ('error' in guard) {\n throw new SecurityError(guard.error);\n }\n await assertScanRootIsDirectory(guard.resolved);\n\n const rulesDir = opts.rulesDir ?? getBuiltInAstRulesPath();\n const rules = await loadRules(rulesDir);\n // Clamp into [1, MAX]: a limit <= 0 would report total > 0 with zero findings\n // (fail-open \"clean\" signal), so the floor is load-bearing, not cosmetic.\n const limit = Math.min(Math.max(opts.limit ?? DEFAULT_AST_QA_LIMIT, 1), MAX_AST_QA_LIMIT);\n\n const { files, filesSkipped, filesTruncated } = await resolveScanFiles(guard.resolved);\n\n const findings: AstRuleFinding[] = [];\n let total = 0;\n\n for (const file of files) {\n const lang = inferRuleLang(file);\n if (lang === null) continue;\n const applicable = rules.filter((r) => r.language === lang);\n if (applicable.length === 0) continue;\n\n let src: string;\n try {\n src = await readFile(file, 'utf8');\n } catch {\n logger.warn(`ast-rule-runner: skipped unreadable file ${file}`);\n continue;\n }\n\n const rel = relative(guard.resolved, file) || file;\n const scanned = scanFile(src, lang, applicable, { rel, limit, alreadyFound: findings.length });\n findings.push(...scanned.findings);\n total += scanned.total;\n }\n\n if (total > findings.length) {\n logger.warn(\n `ast-rule-runner: ${String(total - findings.length)} finding(s) omitted beyond ` +\n `limit=${String(limit)} (total=${String(total)})`\n );\n }\n\n return { findings, total, limit, filesScanned: files.length, filesSkipped, filesTruncated };\n}\n\n/**\n * Run the polyglot QA/security ast-grep rules and return the capped findings\n * array. Overflow beyond `limit` is counted and logged (never silently\n * dropped) — use {@link collectAstQaFindings} directly when the caller needs\n * the true total/overflow count programmatically.\n */\nexport async function runAstQaRules(opts: RunAstQaRulesOptions): Promise<AstRuleFinding[]> {\n const { findings } = await collectAstQaFindings(opts);\n return findings;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
toolStructuredError,
|
|
8
8
|
toolSuccessStructured,
|
|
9
9
|
wrapToolWithTimeout
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-I33FKHUF.js";
|
|
11
11
|
import {
|
|
12
12
|
JsonlStore,
|
|
13
13
|
SECURITY_KEYWORDS,
|
|
@@ -2159,4 +2159,4 @@ export {
|
|
|
2159
2159
|
runImprovementReview,
|
|
2160
2160
|
registerImprovementReviewTool
|
|
2161
2161
|
};
|
|
2162
|
-
//# sourceMappingURL=chunk-
|
|
2162
|
+
//# sourceMappingURL=chunk-ZIZVA5OL.js.map
|
package/dist/cli.js
CHANGED
|
@@ -153,10 +153,11 @@ import {
|
|
|
153
153
|
validateNexusEnv,
|
|
154
154
|
validateWorkflow,
|
|
155
155
|
wrapInMarkdownFence
|
|
156
|
-
} from "./chunk-
|
|
156
|
+
} from "./chunk-6UWWVTQH.js";
|
|
157
157
|
import "./chunk-PJAMCYKJ.js";
|
|
158
158
|
import "./chunk-DGWT7DXX.js";
|
|
159
159
|
import "./chunk-64GMIUM5.js";
|
|
160
|
+
import "./chunk-WZZQI774.js";
|
|
160
161
|
import {
|
|
161
162
|
CATEGORY_DISPLAY_NAMES,
|
|
162
163
|
DEFAULT_PR_REVIEW_CONFIG
|
|
@@ -172,7 +173,7 @@ import {
|
|
|
172
173
|
doctorCommand,
|
|
173
174
|
initDataDirectories,
|
|
174
175
|
runDoctor
|
|
175
|
-
} from "./chunk-
|
|
176
|
+
} from "./chunk-QMUXBHKO.js";
|
|
176
177
|
import {
|
|
177
178
|
buildOpenAICompatAdapters,
|
|
178
179
|
readOpenAICompatEnv
|
|
@@ -191,7 +192,7 @@ import {
|
|
|
191
192
|
recordAuthenticVote,
|
|
192
193
|
registerConsensusVoteTool,
|
|
193
194
|
toRecordDecision
|
|
194
|
-
} from "./chunk-
|
|
195
|
+
} from "./chunk-SNWGBSQ7.js";
|
|
195
196
|
import {
|
|
196
197
|
loadUsageEvents,
|
|
197
198
|
rollupByModel
|
|
@@ -227,15 +228,15 @@ import {
|
|
|
227
228
|
registerImprovementReviewTool,
|
|
228
229
|
runImprovementReview,
|
|
229
230
|
scanForSecrets
|
|
230
|
-
} from "./chunk-
|
|
231
|
+
} from "./chunk-ZIZVA5OL.js";
|
|
231
232
|
import {
|
|
232
233
|
createDefaultPolicyFirewall,
|
|
233
234
|
setGlobalPolicyFirewall,
|
|
234
235
|
stagePolicyFirewallForRollout
|
|
235
|
-
} from "./chunk-
|
|
236
|
+
} from "./chunk-I33FKHUF.js";
|
|
236
237
|
import {
|
|
237
238
|
VERSION
|
|
238
|
-
} from "./chunk-
|
|
239
|
+
} from "./chunk-4OILK4VD.js";
|
|
239
240
|
import "./chunk-B6HNAXBM.js";
|
|
240
241
|
import {
|
|
241
242
|
createUnifiedRegistry,
|
|
@@ -11811,6 +11812,68 @@ function parseValidationArgs(positionals, format, verbose) {
|
|
|
11811
11812
|
return options;
|
|
11812
11813
|
}
|
|
11813
11814
|
|
|
11815
|
+
// src/cli/doctor-native-grammars.ts
|
|
11816
|
+
var GRAMMAR_PROBES = Object.freeze([
|
|
11817
|
+
{ lang: "python", src: "x = 1\n", kind: "assignment" },
|
|
11818
|
+
{ lang: "go", src: "package m\nfunc f() {}\n", kind: "function_declaration" }
|
|
11819
|
+
]);
|
|
11820
|
+
function probeGrammars(register, parse6, probes = GRAMMAR_PROBES) {
|
|
11821
|
+
if (probes.length === 0) {
|
|
11822
|
+
return { available: false, error: "no grammar probes are defined", languages: [] };
|
|
11823
|
+
}
|
|
11824
|
+
try {
|
|
11825
|
+
register();
|
|
11826
|
+
} catch (error) {
|
|
11827
|
+
return {
|
|
11828
|
+
available: false,
|
|
11829
|
+
error: `grammar registration failed: ${getErrorMessage(error)}`,
|
|
11830
|
+
languages: []
|
|
11831
|
+
};
|
|
11832
|
+
}
|
|
11833
|
+
const working = [];
|
|
11834
|
+
const broken = [];
|
|
11835
|
+
for (const probe of probes) {
|
|
11836
|
+
try {
|
|
11837
|
+
const matches = parse6(probe.lang, probe.src).root().findAll({ rule: { kind: probe.kind } });
|
|
11838
|
+
if (matches.length > 0) {
|
|
11839
|
+
working.push(probe.lang);
|
|
11840
|
+
} else {
|
|
11841
|
+
broken.push(`${probe.lang} (parsed, but produced no ${probe.kind} node)`);
|
|
11842
|
+
}
|
|
11843
|
+
} catch (error) {
|
|
11844
|
+
broken.push(`${probe.lang} (${getErrorMessage(error)})`);
|
|
11845
|
+
}
|
|
11846
|
+
}
|
|
11847
|
+
if (broken.length > 0) {
|
|
11848
|
+
return {
|
|
11849
|
+
available: false,
|
|
11850
|
+
error: `grammars unusable: ${broken.join("; ")}`,
|
|
11851
|
+
languages: working
|
|
11852
|
+
};
|
|
11853
|
+
}
|
|
11854
|
+
return { available: true, error: null, languages: working };
|
|
11855
|
+
}
|
|
11856
|
+
async function loadGrammarModules() {
|
|
11857
|
+
const [{ ensurePolyglotLangs }, napi] = await Promise.all([
|
|
11858
|
+
import("./ast-rule-runner-3N7DX6XO.js"),
|
|
11859
|
+
import("@ast-grep/napi")
|
|
11860
|
+
]);
|
|
11861
|
+
return { register: ensurePolyglotLangs, parse: napi.parse };
|
|
11862
|
+
}
|
|
11863
|
+
async function checkNativeGrammars(load = loadGrammarModules) {
|
|
11864
|
+
let modules;
|
|
11865
|
+
try {
|
|
11866
|
+
modules = await load();
|
|
11867
|
+
} catch (error) {
|
|
11868
|
+
return {
|
|
11869
|
+
available: false,
|
|
11870
|
+
error: `ast-grep native grammars failed to load: ${getErrorMessage(error)}`,
|
|
11871
|
+
languages: []
|
|
11872
|
+
};
|
|
11873
|
+
}
|
|
11874
|
+
return probeGrammars(modules.register, modules.parse);
|
|
11875
|
+
}
|
|
11876
|
+
|
|
11814
11877
|
// src/cli/verify-command.ts
|
|
11815
11878
|
function checkNodeVersion() {
|
|
11816
11879
|
const version = process.version;
|
|
@@ -11923,6 +11986,23 @@ async function checkSqliteAvailability() {
|
|
|
11923
11986
|
fix: "Upgrade to Node >= 22.5.0 (node:sqlite is a builtin; nothing to install)"
|
|
11924
11987
|
};
|
|
11925
11988
|
}
|
|
11989
|
+
async function checkGrammarAvailability() {
|
|
11990
|
+
const result = await checkNativeGrammars();
|
|
11991
|
+
if (result.available) {
|
|
11992
|
+
return {
|
|
11993
|
+
name: "Native Grammars",
|
|
11994
|
+
passed: true,
|
|
11995
|
+
message: `ast-grep ${result.languages.join("/")} grammars parse (polyglot scanner available)`
|
|
11996
|
+
};
|
|
11997
|
+
}
|
|
11998
|
+
return {
|
|
11999
|
+
name: "Native Grammars",
|
|
12000
|
+
passed: false,
|
|
12001
|
+
severity: "warn",
|
|
12002
|
+
message: result.error ?? "ast-grep native grammars unavailable",
|
|
12003
|
+
fix: "Reinstall without blocking scripts, or verify @ast-grep/lang-{python,go} shipped their prebuilds/ directory"
|
|
12004
|
+
};
|
|
12005
|
+
}
|
|
11926
12006
|
function checkDataDirs() {
|
|
11927
12007
|
const result = checkDataDirectory();
|
|
11928
12008
|
const unwritable = result.subdirectories.filter((s) => !s.exists || !s.writable);
|
|
@@ -11989,6 +12069,7 @@ async function runVerify() {
|
|
|
11989
12069
|
checkConfigLoading(),
|
|
11990
12070
|
checkExpertSystem(),
|
|
11991
12071
|
await checkSqliteAvailability(),
|
|
12072
|
+
await checkGrammarAvailability(),
|
|
11992
12073
|
checkDataDirs(),
|
|
11993
12074
|
await checkAdapterAvailability()
|
|
11994
12075
|
];
|
|
@@ -19586,7 +19667,7 @@ function startImprovementReviewScheduler(options) {
|
|
|
19586
19667
|
let running = false;
|
|
19587
19668
|
const runOnce = async () => {
|
|
19588
19669
|
try {
|
|
19589
|
-
const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-
|
|
19670
|
+
const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-GGYLCRGQ.js");
|
|
19590
19671
|
const input = ImprovementReviewInputSchema2.parse({ fileIssues });
|
|
19591
19672
|
const result = await runImprovementReview2(input, { logger: logger22 });
|
|
19592
19673
|
logger22.info("Scheduled improvement_review complete", {
|