From ab70f599ef8e0b95c9b4e0c6f9beeaac77af5769 Mon Sep 17 00:00:00 2001 From: Kodaxa Date: Wed, 20 May 2026 13:58:33 -0700 Subject: [PATCH] feat: add lint.exclude_paths and secrets.allowlist to codewarden.json Adds two new config fields that let consuming repos control which paths are checked by the governance report: - lint.exclude_paths: path prefixes excluded from file-length checks (e.g. docs directories, generated files, vendored code) - secrets.allowlist: path prefixes excluded from credential scanning (e.g. files with known-safe localhost dev database URLs) Also adds --config= CLI flag to governance-report.js and a config input to action.yml so repos can provide their own codewarden.json without modifying the skill installation. All 32 existing tests pass unchanged. Co-Authored-By: Claude Opus 4.6 --- action.yml | 24 +++++++++++++++++--- code-warden/CONFIGURE.md | 8 +++++++ code-warden/codewarden.json | 6 +++++ code-warden/tools/governance-report.js | 31 +++++++++++++++++--------- code-warden/tools/lib/config.js | 12 ++++++++-- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/action.yml b/action.yml index e3b9381..d7a05db 100644 --- a/action.yml +++ b/action.yml @@ -30,6 +30,10 @@ inputs: description: Code scanning category for Code-Warden SARIF results. required: false default: code-warden + config: + description: Path to a codewarden.json config file in the repo. Overrides built-in defaults. + required: false + default: '' outputs: report-path: @@ -60,18 +64,32 @@ runs: id: governance continue-on-error: true shell: bash - run: node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" + run: | + CONFIG_FLAG="" + if [ -n "${{ inputs.config }}" ] && [ -f "${{ inputs.config }}" ]; then + CONFIG_FLAG="--config=${{ inputs.config }}" + fi + node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" $CONFIG_FLAG - name: Publish governance summary if: ${{ always() && inputs.summary == 'true' }} shell: bash - run: node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" --format=md >> "$GITHUB_STEP_SUMMARY" || true + run: | + CONFIG_FLAG="" + if [ -n "${{ inputs.config }}" ] && [ -f "${{ inputs.config }}" ]; then + CONFIG_FLAG="--config=${{ inputs.config }}" + fi + node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" --format=md $CONFIG_FLAG >> "$GITHUB_STEP_SUMMARY" || true - name: Generate SARIF report if: ${{ always() && inputs.sarif == 'true' }} shell: bash run: | - node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" --format=sarif > "${{ steps.sarif-path.outputs.path }}" || true + CONFIG_FLAG="" + if [ -n "${{ inputs.config }}" ] && [ -f "${{ inputs.config }}" ]; then + CONFIG_FLAG="--config=${{ inputs.config }}" + fi + node "${{ github.action_path }}/code-warden/tools/governance-report.js" "${{ inputs.path }}" --format=sarif $CONFIG_FLAG > "${{ steps.sarif-path.outputs.path }}" || true test -s "${{ steps.sarif-path.outputs.path }}" - name: Upload SARIF report diff --git a/code-warden/CONFIGURE.md b/code-warden/CONFIGURE.md index 2e00043..fea81ef 100644 --- a/code-warden/CONFIGURE.md +++ b/code-warden/CONFIGURE.md @@ -18,6 +18,12 @@ Located at the root of the skill folder. Default configuration: }, "safety": { "exempt_from_blast_radius": ["tests/", "docs/", "scripts/"] + }, + "lint": { + "exclude_paths": [] + }, + "secrets": { + "allowlist": [] } } ``` @@ -28,6 +34,8 @@ Located at the root of the skill folder. Default configuration: | `pre_flight_trigger_lines` | 150 | Forces a JSON manifest before large outputs. | | `human_checkpoint_files` | 2 | Requires human `[AWAITING CONFIRMATION]` before modifying this many files simultaneously. | | `exempt_from_blast_radius` | (list) | Skips strict rewriting rollback plans on these file directories. | +| `lint.exclude_paths` | `[]` | Path prefixes excluded from file-length checks. Use for docs, generated files, or vendored code (e.g. `["Documents/", "generated/"]`). | +| `secrets.allowlist` | `[]` | Path prefixes excluded from hardcoded-credential scanning. Use for files with known-safe localhost dev URLs or test fixtures (e.g. `["scripts/indexer.config.toml"]`). | --- diff --git a/code-warden/codewarden.json b/code-warden/codewarden.json index 182098b..2940ea3 100644 --- a/code-warden/codewarden.json +++ b/code-warden/codewarden.json @@ -11,6 +11,12 @@ "scripts/" ] }, + "lint": { + "exclude_paths": [] + }, + "secrets": { + "allowlist": [] + }, "reference_selection": { "rules": [ { diff --git a/code-warden/tools/governance-report.js b/code-warden/tools/governance-report.js index e848f2d..b46beb5 100644 --- a/code-warden/tools/governance-report.js +++ b/code-warden/tools/governance-report.js @@ -24,10 +24,12 @@ function parseArgs(argv) { const args = argv.slice(2); const formatArg = args.find(a => a.startsWith('--format=')); const outArg = args.find(a => a.startsWith('--out=')); + const configArg = args.find(a => a.startsWith('--config=')); const format = formatArg ? formatArg.split('=')[1] : null; const out = outArg ? outArg.slice('--out='.length) : null; + const configPath = configArg ? configArg.slice('--config='.length) : null; const scanPath = args.find(a => !a.startsWith('--')) || '.'; - return { format, out, scanPath }; + return { format, out, scanPath, configPath }; } // --------------------------------------------------------------------------- @@ -49,8 +51,13 @@ function gitInfo() { // File length + secrets (single pass over all files) // --------------------------------------------------------------------------- -function runScans(scanPath) { - const { maxFileLength } = loadConfig(); +function matchesAnyPrefix(filePath, prefixes) { + const normalized = filePath.replace(/\\/g, '/'); + return prefixes.some(p => normalized.startsWith(p) || normalized === p.replace(/\/$/, '')); +} + +function runScans(scanPath, configPath) { + const { maxFileLength, lintExcludePaths, secretsAllowlist } = loadConfig(configPath); const resolved = path.resolve(scanPath); if (!fs.existsSync(resolved)) { @@ -75,13 +82,15 @@ function runScans(scanPath) { const rel = scanRootIsDirectory ? path.relative(resolved, f) : path.basename(f); - const lineCount = countLines(content); - if (lineCount > maxFileLength) { - lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength }); + if (!matchesAnyPrefix(rel, lintExcludePaths)) { + const lineCount = countLines(content); + if (lineCount > maxFileLength) { + lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength }); + } } const hit = scanForSecrets(content); - if (hit) { + if (hit && !matchesAnyPrefix(rel, secretsAllowlist)) { secretViolations.push({ file: rel, pattern: hit.label, line: hit.line, column: hit.column }); } } @@ -212,9 +221,9 @@ function checkRuntimeHooks() { // Report assembly // --------------------------------------------------------------------------- -function generateReport(scanPath) { +function generateReport(scanPath, configPath) { const repo = gitInfo(); - const { fileLength, secrets } = runScans(scanPath); + const { fileLength, secrets } = runScans(scanPath, configPath); const behavioralTests = checkTests(); const installHealth = checkInstallHealth(); const runtimeHooks = checkRuntimeHooks(); @@ -315,8 +324,8 @@ function writeReport(outPath, content) { // Main // --------------------------------------------------------------------------- -const { format, out, scanPath } = parseArgs(process.argv); -const report = generateReport(scanPath); +const { format, out, scanPath, configPath } = parseArgs(process.argv); +const report = generateReport(scanPath, configPath); if (out) { writeReport(out, formatReport(report, format)); diff --git a/code-warden/tools/lib/config.js b/code-warden/tools/lib/config.js index 00a86bb..5236357 100644 --- a/code-warden/tools/lib/config.js +++ b/code-warden/tools/lib/config.js @@ -24,11 +24,13 @@ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'codewarden.json'); * Falls back to defaults silently if the file is missing or unparseable. * * @param {string} [configPath] - Override the config file location - * @returns {{ maxFileLength: number }} + * @returns {{ maxFileLength: number, lintExcludePaths: string[], secretsAllowlist: string[] }} */ function loadConfig(configPath) { const target = configPath || DEFAULT_CONFIG_PATH; let maxFileLength = 400; + let lintExcludePaths = []; + let secretsAllowlist = []; try { const raw = fs.readFileSync(target, 'utf8'); @@ -39,11 +41,17 @@ function loadConfig(configPath) { if (typeof configured === 'number' && configured > 0) { maxFileLength = configured; } + if (Array.isArray(cfg?.lint?.exclude_paths)) { + lintExcludePaths = cfg.lint.exclude_paths.filter(p => typeof p === 'string'); + } + if (Array.isArray(cfg?.secrets?.allowlist)) { + secretsAllowlist = cfg.secrets.allowlist.filter(p => typeof p === 'string'); + } } catch { // Missing or invalid config — use defaults } - return { maxFileLength }; + return { maxFileLength, lintExcludePaths, secretsAllowlist }; } module.exports = { loadConfig, DEFAULT_CONFIG_PATH };