@herbertgao/pi-extensions 2026.9.15 → 2026.9.16
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 +5 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +8 -5
|
@@ -25,7 +25,8 @@ import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
|
|
|
25
25
|
import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
|
|
26
26
|
import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
|
|
27
27
|
import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
|
|
28
|
-
import { resolvePackagePath } from "./package-root.js";
|
|
28
|
+
import { getPackageRoot, resolvePackagePath } from "./package-root.js";
|
|
29
|
+
import { resolveWebTreeSitterPackageDir } from "../scripts/lib/web-tree-sitter-dir.mjs";
|
|
29
30
|
import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
|
|
30
31
|
import { escapeRegExp } from "./string-utils.js";
|
|
31
32
|
import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
|
|
@@ -73,6 +74,27 @@ function createParserCounters() {
|
|
|
73
74
|
parserFailures: 0,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The errno that blocks writing into `dir`, or undefined when a write can be
|
|
79
|
+
* attempted (#3409). The directory itself need not exist yet — the lazy fetch
|
|
80
|
+
* creates it — so the nearest EXISTING ancestor is what gets probed.
|
|
81
|
+
*/
|
|
82
|
+
function grammarWriteBlockedReason(dir) {
|
|
83
|
+
let probe = dir;
|
|
84
|
+
while (!fs.existsSync(probe)) {
|
|
85
|
+
const parent = path.dirname(probe);
|
|
86
|
+
if (parent === probe)
|
|
87
|
+
return "ENOENT";
|
|
88
|
+
probe = parent;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
fs.accessSync(probe, fs.constants.W_OK);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return err?.code ?? "EACCES";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
76
98
|
/**
|
|
77
99
|
* `size:mtimeMs` identity for a grammar candidate, or undefined when the path
|
|
78
100
|
* is absent or not a regular file. Doubles as the existence check on the
|
|
@@ -347,6 +369,7 @@ export class TreeSitterClient {
|
|
|
347
369
|
this.grammarDirResolutionDeps = grammarDirResolutionDeps ?? {
|
|
348
370
|
resolveAsset: (asset) => this.resolveWebTreeSitterAsset(asset),
|
|
349
371
|
resolvePackage: (specifier) => _require.resolve(specifier),
|
|
372
|
+
packageRoot: () => getPackageRoot(import.meta.url),
|
|
350
373
|
cwd: () => process.cwd(),
|
|
351
374
|
};
|
|
352
375
|
this.grammarsDir = this.findGrammarsDir();
|
|
@@ -469,38 +492,42 @@ export class TreeSitterClient {
|
|
|
469
492
|
}
|
|
470
493
|
}
|
|
471
494
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
495
|
+
* web-tree-sitter's installed package directory, through the one shared
|
|
496
|
+
* ladder in `scripts/lib/web-tree-sitter-dir.mjs` (#3409). Routed through
|
|
497
|
+
* `grammarDirResolutionDeps` so the resolver context — the thing that
|
|
498
|
+
* differs on a compiled host — stays the injected seam (#2112/#2136).
|
|
499
|
+
*/
|
|
500
|
+
webTreeSitterPackageDir() {
|
|
501
|
+
return resolveWebTreeSitterPackageDir({
|
|
502
|
+
resolve: (specifier) => this.grammarDirResolutionDeps.resolvePackage(specifier),
|
|
503
|
+
packageRoot: () => this.grammarDirResolutionDeps.packageRoot(),
|
|
504
|
+
cwd: () => this.grammarDirResolutionDeps.cwd(),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolve a web-tree-sitter asset path:
|
|
509
|
+
* 1. the asset's own subpath, authoritative when the package `exports` it
|
|
510
|
+
* (`tree-sitter.wasm` is exported; `grammars` is NOT in 0.25.10, so this
|
|
511
|
+
* rung is dead for the grammars dir — #3409's second symptom);
|
|
512
|
+
* 2. the asset inside the package directory the shared ladder resolves. That
|
|
513
|
+
* ladder's own last two rungs are the package-root walk (#20, for pi's
|
|
514
|
+
* temp-dir compile) and the cwd fallback this method used to spell out.
|
|
476
515
|
*/
|
|
477
516
|
resolveWebTreeSitterAsset(asset) {
|
|
478
|
-
// Strategy 1: Node module resolution (hoisted installs, pnpm workspaces)
|
|
479
517
|
try {
|
|
480
|
-
const resolved =
|
|
518
|
+
const resolved = this.grammarDirResolutionDeps.resolvePackage(`web-tree-sitter/${asset}`);
|
|
481
519
|
if (fs.existsSync(resolved))
|
|
482
520
|
return resolved;
|
|
483
521
|
}
|
|
484
522
|
catch {
|
|
485
523
|
/* fall through */
|
|
486
524
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// web-tree-sitter, but the package root (where package.json lives) still has
|
|
491
|
-
// the correct node_modules layout.
|
|
492
|
-
try {
|
|
493
|
-
const candidate = resolvePackagePath(import.meta.url, "node_modules", "web-tree-sitter", asset);
|
|
525
|
+
const pkgDir = this.webTreeSitterPackageDir();
|
|
526
|
+
if (pkgDir) {
|
|
527
|
+
const candidate = path.join(pkgDir, asset);
|
|
494
528
|
if (fs.existsSync(candidate))
|
|
495
529
|
return candidate;
|
|
496
530
|
}
|
|
497
|
-
catch {
|
|
498
|
-
/* fall through */
|
|
499
|
-
}
|
|
500
|
-
// Strategy 3: cwd fallback
|
|
501
|
-
const cwdCandidate = path.join(process.cwd(), "node_modules", "web-tree-sitter", asset);
|
|
502
|
-
if (fs.existsSync(cwdCandidate))
|
|
503
|
-
return cwdCandidate;
|
|
504
531
|
return undefined;
|
|
505
532
|
}
|
|
506
533
|
/**
|
|
@@ -764,22 +791,16 @@ export class TreeSitterClient {
|
|
|
764
791
|
* whether or not it exists yet — so we can create + populate it when the
|
|
765
792
|
* postinstall download was skipped (pnpm/bun). Returns undefined if
|
|
766
793
|
* web-tree-sitter itself can't be located.
|
|
794
|
+
*
|
|
795
|
+
* #3409: this used to resolve the BARE `web-tree-sitter` specifier and
|
|
796
|
+
* nothing else, which throws MODULE_NOT_FOUND inside the `bun
|
|
797
|
+
* build --compile` binary pi ships — so on every such host it returned
|
|
798
|
+
* undefined and NO non-core grammar could ever be fetched. It now shares the
|
|
799
|
+
* read path's ladder, whose first rung is a subpath that does resolve there.
|
|
767
800
|
*/
|
|
768
801
|
grammarsWriteDir() {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
while (path.basename(dir) !== "web-tree-sitter" &&
|
|
772
|
-
dir !== path.dirname(dir)) {
|
|
773
|
-
dir = path.dirname(dir);
|
|
774
|
-
}
|
|
775
|
-
if (path.basename(dir) === "web-tree-sitter") {
|
|
776
|
-
return path.join(dir, "grammars");
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
/* fall through */
|
|
781
|
-
}
|
|
782
|
-
return undefined;
|
|
802
|
+
const pkgDir = this.webTreeSitterPackageDir();
|
|
803
|
+
return pkgDir ? path.join(pkgDir, "grammars") : undefined;
|
|
783
804
|
}
|
|
784
805
|
/**
|
|
785
806
|
* Ensure a single grammar wasm is on disk, fetching it at runtime if the
|
|
@@ -847,7 +868,7 @@ export class TreeSitterClient {
|
|
|
847
868
|
// refactor that lets an exception out cannot regress to it.
|
|
848
869
|
const message = err instanceof Error ? err.message : String(err);
|
|
849
870
|
this.recordGrammarFailure(grammarFile, `The runtime grammar fetch threw an unexpected error (${message}).`,
|
|
850
|
-
/* retryable */ true);
|
|
871
|
+
/* retryable */ true, "download-failed");
|
|
851
872
|
return false;
|
|
852
873
|
}
|
|
853
874
|
})();
|
|
@@ -899,15 +920,31 @@ export class TreeSitterClient {
|
|
|
899
920
|
? this.grammarsDir
|
|
900
921
|
: this.grammarsWriteDir();
|
|
901
922
|
if (!dir) {
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
// unmemoized `false` that re-does
|
|
908
|
-
// sweep on every
|
|
909
|
-
|
|
910
|
-
|
|
923
|
+
// Nothing to write into: web-tree-sitter is not locatable by ANY rung
|
|
924
|
+
// of the shared ladder (this runtime's resolver, pi-lens's own package
|
|
925
|
+
// root, the working directory), so the package is missing from this
|
|
926
|
+
// install. Still an environment condition, not a CDN verdict — #1536
|
|
927
|
+
// review F2: it gets the same cooldown + notification treatment as a
|
|
928
|
+
// download failure rather than a silent unmemoized `false` that re-does
|
|
929
|
+
// the same failing resolution sweep on every demand.
|
|
930
|
+
//
|
|
931
|
+
// #3409: before the shared ladder, this arm ALSO caught every host
|
|
932
|
+
// whose runtime cannot resolve a bare specifier — where the package was
|
|
933
|
+
// present and the wasm downloadable — and told the user to approve
|
|
934
|
+
// build scripts or restore network access. Hence the cause.
|
|
935
|
+
this.recordGrammarFailure(grammarFile, "No grammars directory could be resolved for the runtime fetch: the web-tree-sitter package is not locatable from this runtime, from pi-lens's own package root, or from the working directory.",
|
|
936
|
+
/* retryable */ true, "write-dir-unresolvable");
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
// A destination that cannot be written to is not a download problem
|
|
940
|
+
// either (#3409): a root-owned `node_modules` under a global install
|
|
941
|
+
// reaches `downloadGrammarDetailed`'s catch-all, which reports no reason
|
|
942
|
+
// at all, so the user was sent to inspect their package manager and
|
|
943
|
+
// network for an EACCES.
|
|
944
|
+
const writeBlock = grammarWriteBlockedReason(dir);
|
|
945
|
+
if (writeBlock) {
|
|
946
|
+
this.recordGrammarFailure(grammarFile, `The grammars directory ${dir} cannot be written to (${writeBlock}), so the runtime fetch has nowhere to land.`,
|
|
947
|
+
/* retryable */ true, "write-dir-unwritable");
|
|
911
948
|
return false;
|
|
912
949
|
}
|
|
913
950
|
// Reuse the shared single-file downloader (same CDN/source as the
|
|
@@ -940,7 +977,7 @@ export class TreeSitterClient {
|
|
|
940
977
|
// and one that sends the user to inspect their package manager while a
|
|
941
978
|
// captive portal is the real cause.
|
|
942
979
|
this.recordGrammarFailure(grammarFile, reason ??
|
|
943
|
-
"The package manager skipped install scripts and the runtime download failed.", retryable);
|
|
980
|
+
"The package manager skipped install scripts and the runtime download failed.", retryable, "download-failed");
|
|
944
981
|
}
|
|
945
982
|
return ok;
|
|
946
983
|
}
|
|
@@ -951,8 +988,14 @@ export class TreeSitterClient {
|
|
|
951
988
|
* by the "no writable directory" and "download failed" arms of
|
|
952
989
|
* `ensureGrammar` — both are failures of the SAME ensure attempt, just
|
|
953
990
|
* with a different point of failure.
|
|
991
|
+
*
|
|
992
|
+
* `cause` is that point of failure (#3409). It picks the REMEDY the user is
|
|
993
|
+
* told and lands in the durable record, so `~/.pi-lens` forensics can tell
|
|
994
|
+
* "this host has nowhere to write" from "this download failed" — the two
|
|
995
|
+
* were one string, and the one it carried named build scripts and the
|
|
996
|
+
* network for a cause that is neither.
|
|
954
997
|
*/
|
|
955
|
-
recordGrammarFailure(grammarFile, detail, retryable) {
|
|
998
|
+
recordGrammarFailure(grammarFile, detail, retryable, cause) {
|
|
956
999
|
const attempts = (this.grammarFailureAttempts.get(grammarFile) ?? 0) + 1;
|
|
957
1000
|
this.grammarFailureAttempts.set(grammarFile, attempts);
|
|
958
1001
|
// A durable verdict (404/410) never expires -- matches the issue's "a
|
|
@@ -964,17 +1007,29 @@ export class TreeSitterClient {
|
|
|
964
1007
|
this.grammarRetryAtMs.set(grammarFile, retryDelayMs === undefined
|
|
965
1008
|
? Number.POSITIVE_INFINITY
|
|
966
1009
|
: Date.now() + retryDelayMs);
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1010
|
+
// The remedy the user is given, keyed to WHERE the attempt failed. The
|
|
1011
|
+
// last two arms are the pre-existing strings, byte for byte; the first
|
|
1012
|
+
// two exist because sending someone to `pnpm approve-builds` and their
|
|
1013
|
+
// network for a resolution or permission fault is the #3409 complaint.
|
|
1014
|
+
const remedy = cause === "write-dir-unresolvable"
|
|
1015
|
+
? `This is a packaging/module-resolution fault, not a download problem: reinstall pi-lens so its ` +
|
|
1016
|
+
`web-tree-sitter dependency is present, or drop the wasm into pi-lens's own grammars/ directory.`
|
|
1017
|
+
: cause === "write-dir-unwritable"
|
|
1018
|
+
? `This is a filesystem-permission fault, not a download problem: make that directory writable ` +
|
|
1019
|
+
`for the user pi-lens runs as, or reinstall pi-lens as that user.`
|
|
1020
|
+
: retryable
|
|
1021
|
+
? `if the problem persists, allow the package manager's build scripts ` +
|
|
1022
|
+
`(pnpm approve-builds / bun trustedDependencies) or restore network access.`
|
|
1023
|
+
: `Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
|
|
1024
|
+
`(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
|
|
1025
|
+
const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
|
|
1026
|
+
`module reports and structural rules for this language will be degraded. ` +
|
|
1027
|
+
`${detail} ` +
|
|
1028
|
+
(retryable
|
|
1029
|
+
? `pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; `
|
|
1030
|
+
: `The grammar source reports it does not exist (not a network problem), so this will not ` +
|
|
1031
|
+
`resolve on retry. `) +
|
|
1032
|
+
remedy;
|
|
978
1033
|
logTreeSitterDiagnostic({
|
|
979
1034
|
subsystem: "tree-sitter-client",
|
|
980
1035
|
message: unavailable,
|
|
@@ -984,6 +1039,12 @@ export class TreeSitterClient {
|
|
|
984
1039
|
retryable,
|
|
985
1040
|
retryDelayMs,
|
|
986
1041
|
attempts,
|
|
1042
|
+
// Omitted, not defaulted, for the arms that are neither a fetch
|
|
1043
|
+
// destination nor a download verdict (a vendored grammar is never
|
|
1044
|
+
// downloaded; a decode failure happened AFTER a successful one), so
|
|
1045
|
+
// those records keep their pre-#3409 shape rather than carrying a
|
|
1046
|
+
// cause that is not true of them.
|
|
1047
|
+
...(cause ? { cause } : {}),
|
|
987
1048
|
},
|
|
988
1049
|
});
|
|
989
1050
|
// #1536 review F1: incrementDegradationCount keeps ONE ring-buffer slot
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE interface every turn-end delivery lane implements (#1892).
|
|
3
|
+
*
|
|
4
|
+
* `clients/runtime-turn.ts`'s `handleTurnEnd` grew fourteen delivery lanes
|
|
5
|
+
* inline: each read its own store, applied its own freshness/disposition
|
|
6
|
+
* policy, and formatted its own section into `blockerParts`/`advisoryParts`,
|
|
7
|
+
* so the composer had to know every lane's rules and no lane could be read,
|
|
8
|
+
* tested, or reasoned about on its own. The lanes converged on ONE shape
|
|
9
|
+
* anyway — read a store, gate the rows, render a tier — and that shape is
|
|
10
|
+
* this module. A lane is a deep module: three narrow calls over whatever its
|
|
11
|
+
* store's rules happen to be, and the composer states none of them.
|
|
12
|
+
*
|
|
13
|
+
* ── The three stages, and why they are three ──────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* 1. `collect(ctx)` — read this lane's stores and declare, per STORE, the
|
|
16
|
+
* findings plus the freshness policy that store's rows need. It returns
|
|
17
|
+
* structured rows keyed by store name (the source identity the shared
|
|
18
|
+
* pass and its bounded records carry), never a rendered string.
|
|
19
|
+
*
|
|
20
|
+
* It does NOT call the freshness gate. #3264 folded the turn-end scanner
|
|
21
|
+
* stores into ONE `gateFindingsByPathFreshness` call so a file two stores
|
|
22
|
+
* cite is stat'd once, spends one stat budget, and writes one bounded
|
|
23
|
+
* decision record per delivery instead of up to six. A lane that gated
|
|
24
|
+
* itself would re-split that pass the moment a second lane appeared, so
|
|
25
|
+
* the pass is the COMPOSER's (it is a cross-lane resource — a filesystem
|
|
26
|
+
* pass, a budget and a record, not a per-lane rule), and a lane only
|
|
27
|
+
* DECLARES its sources. `tests/config/turn-end-lane-boundaries.test.ts`
|
|
28
|
+
* pins that: a lane that calls the gate itself reds.
|
|
29
|
+
*
|
|
30
|
+
* 2. `gate(gates, ctx)` — apply the per-lane policy the shared pass cannot:
|
|
31
|
+
* dispositions (`filterFindingsByDisposition`, the seam in
|
|
32
|
+
* `clients/dispatch/finding-policy.ts`), and whatever existence or
|
|
33
|
+
* lifecycle contract the lane's own store carries. Takes the freshness
|
|
34
|
+
* partition the composer got for THIS lane's sources and returns the
|
|
35
|
+
* lane's own kept shape, which no other module reads.
|
|
36
|
+
*
|
|
37
|
+
* 3. `render(kept, ctx)` — format the tiers. A lane returns its sections; it
|
|
38
|
+
* never pushes into the turn's arrays. The push stays in the composer so
|
|
39
|
+
* the `@delivery-surface:` seam scan in
|
|
40
|
+
* `tests/clients/finding-delivery-gate.test.ts` — which reads
|
|
41
|
+
* `clients/runtime-turn.ts` and `tools/lens-diagnostics.ts` — keeps seeing
|
|
42
|
+
* every agent-facing render seam. A lane that pushed from its own file
|
|
43
|
+
* would be invisible to that scan; the boundary test reds on it.
|
|
44
|
+
*
|
|
45
|
+
* The composer still owns everything that is not one lane's rule: the order
|
|
46
|
+
* the tiers are assembled in, the display and message caps, the turn
|
|
47
|
+
* signature dedupe, the `turn-end-findings` cache write, the git-guard
|
|
48
|
+
* record, and the one-per-turn disposition-suppressed notice that every
|
|
49
|
+
* lane's counts fold into.
|
|
50
|
+
*/
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn-end GOVULNCHECK lane (#1892): the session_start-cached Go CVE
|
|
3
|
+
* store, rendered as ONE advisory tier.
|
|
4
|
+
*
|
|
5
|
+
* One lane, one store, one tier — the straight second extraction against the
|
|
6
|
+
* `TurnEndLane` interface, and the first lane that renders an ADVISORY
|
|
7
|
+
* section (see `TurnEndLaneParts.advisoryParts` and ADR 0008's amendment for
|
|
8
|
+
* why that field arrives with this lane rather than with the interface).
|
|
9
|
+
*
|
|
10
|
+
* Registered surface (`clients/finding-delivery-gate.ts`):
|
|
11
|
+
* `runtime-turn:govulncheck-advisory` — mode `gated`, evidence
|
|
12
|
+
* `scannerGates.govulncheck` (the composer's own binding of the shared pass's
|
|
13
|
+
* result, unchanged by this extraction).
|
|
14
|
+
*
|
|
15
|
+
* History this file must not lose (each line below is a shipped defect or a
|
|
16
|
+
* review finding):
|
|
17
|
+
*
|
|
18
|
+
* - #1622 sibling sweep: govulncheck renders a call site as `file:line` off a
|
|
19
|
+
* session_start snapshot — the same stale-line shape as gitleaks, one tier
|
|
20
|
+
* lower. A file edited after the scan keeps its CVE and loses its line,
|
|
21
|
+
* plus the `STALE_LINE_MARKER`.
|
|
22
|
+
* - #1622 review H1: a CVE is pinned by `go.mod`, NOT by the call site, so a
|
|
23
|
+
* DELETED traced file may not drop it — `onMissing: "demote"` routes a
|
|
24
|
+
* vanished path into the same arm as an edited one. The first cut dropped
|
|
25
|
+
* it, and `citedPath` reads only the FIRST filename frame, so one deleted
|
|
26
|
+
* file in a long trace silently killed a CVE go.mod still pins.
|
|
27
|
+
* - #1694 F1: these findings pass through `filterFindingsByDisposition` with
|
|
28
|
+
* the SAME `ProjectDiagnostic` identity `lens_diagnostics mode=full`
|
|
29
|
+
* surfaces, over BOTH freshness arms — a mark made against what the agent
|
|
30
|
+
* was SHOWN is honoured here too.
|
|
31
|
+
* - #1627 + #1625 review: the post-gate row guard and the post-disposition
|
|
32
|
+
* row guard are the SAME guard — `render` tests the list it is about to
|
|
33
|
+
* print, never the raw cache length, or the header prints with zero rows
|
|
34
|
+
* beneath it whenever either filter empties the list.
|
|
35
|
+
*/
|
|
36
|
+
import { filterFindingsByDisposition } from "../../dispatch/finding-policy.js";
|
|
37
|
+
import { toRunnerDisplayPath } from "../../dispatch/runner-context.js";
|
|
38
|
+
import { govulncheckFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/govulncheck.js";
|
|
39
|
+
import { STALE_LINE_MARKER } from "../../stale-marker.js";
|
|
40
|
+
/**
|
|
41
|
+
* #1622 H1: `onMissing: "demote"`, not the default `"drop"`. A Go CVE is
|
|
42
|
+
* pinned by `go.mod`; the traced call site is only where it is reachable
|
|
43
|
+
* FROM, so a deleted trace file means "the line is unusable", never "the
|
|
44
|
+
* vulnerability is gone". `citedPath` reads the first filename frame, which
|
|
45
|
+
* is also the frame `render` prints — the path the verdict is about is the
|
|
46
|
+
* path the agent is shown.
|
|
47
|
+
*/
|
|
48
|
+
async function collect(ctx) {
|
|
49
|
+
const data = (await ctx.readScannerCache("govulncheck"))
|
|
50
|
+
?.data;
|
|
51
|
+
return {
|
|
52
|
+
govulncheck: {
|
|
53
|
+
findings: data?.findings ?? [],
|
|
54
|
+
scannedAt: data?.scannedAt,
|
|
55
|
+
citedPath: (finding) => finding.trace.find((frame) => frame.filename)?.filename,
|
|
56
|
+
onMissing: "demote",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* #1694 F1/#1625 review round: the freshness gate runs FIRST, so the
|
|
62
|
+
* disposition anchor is derived from each finding's post-demotion identity —
|
|
63
|
+
* this list is already the gate's live+stale partition, never the raw
|
|
64
|
+
* pre-gate cache.
|
|
65
|
+
*/
|
|
66
|
+
function gate(gates, ctx) {
|
|
67
|
+
const stale = new Set(gates.govulncheck.stale);
|
|
68
|
+
const filtered = filterFindingsByDisposition([...gates.govulncheck.live, ...gates.govulncheck.stale], ctx.cwd, (f) => govulncheckFindingToProjectDiagnostic(ctx.cwd, f));
|
|
69
|
+
return {
|
|
70
|
+
findings: filtered.kept,
|
|
71
|
+
stale,
|
|
72
|
+
suppressed: filtered.suppressed,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function render(kept, ctx) {
|
|
76
|
+
// Lane-local accumulator, deliberately NOT named after the turn's tier:
|
|
77
|
+
// `advisoryParts` is the composer's array, and a lane that pushed into one
|
|
78
|
+
// would be a render seam the `@delivery-surface:` tag scan cannot see
|
|
79
|
+
// (`tests/config/turn-end-lane-boundaries.test.ts`).
|
|
80
|
+
const advisorySections = [];
|
|
81
|
+
// #1627/#1625: guard the list that is about to be printed, never the raw
|
|
82
|
+
// cache length — the header must not print with zero rows beneath it.
|
|
83
|
+
if (kept.findings.length) {
|
|
84
|
+
const findings = kept.findings.slice(0, 5);
|
|
85
|
+
let report = "🛡️ Go CVEs reachable from this code (govulncheck) — upgrade where possible:\n";
|
|
86
|
+
for (const f of findings) {
|
|
87
|
+
const callSite = f.trace.find((t) => t.filename);
|
|
88
|
+
const stale = kept.stale.has(f);
|
|
89
|
+
const where = callSite?.filename
|
|
90
|
+
? `${toRunnerDisplayPath(ctx.cwd, callSite.filename)}${!stale && callSite.line ? `:${callSite.line}` : ""}${stale ? ` ${STALE_LINE_MARKER}` : ""}`
|
|
91
|
+
: (f.module ?? f.packageName ?? "(module)");
|
|
92
|
+
const fix = f.fixedVersion
|
|
93
|
+
? ` — upgrade to ${f.fixedVersion} or later`
|
|
94
|
+
: " — no fix yet, track upstream";
|
|
95
|
+
report += ` ${f.osv} (${where})${fix}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (kept.findings.length > findings.length) {
|
|
98
|
+
report += ` … and ${kept.findings.length - findings.length} more\n`;
|
|
99
|
+
}
|
|
100
|
+
advisorySections.push(report);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
advisoryParts: advisorySections,
|
|
104
|
+
dispositionSuppressed: { govulncheck: kept.suppressed },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The lane. Stateless — one object, no per-turn state to reset. */
|
|
108
|
+
export const govulncheckLane = {
|
|
109
|
+
collect,
|
|
110
|
+
gate,
|
|
111
|
+
render,
|
|
112
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn-end SECRETS lane (#1892): the gitleaks and trivy-secrets stores,
|
|
3
|
+
* their live 🔴 blocker tier and their demoted 🔑 tier.
|
|
4
|
+
*
|
|
5
|
+
* One lane, two stores, on purpose. The two secret scanners do not have a
|
|
6
|
+
* section each: #131 Mode 3 collapses them (and ast-grep's hardcoded-secret
|
|
7
|
+
* rules) BY LOCATION into one report with combined provenance, because a
|
|
8
|
+
* credential flagged by two tools needs rotating once, not twice — and the
|
|
9
|
+
* demoted tier merges the same two stores for the same reason. So "gitleaks"
|
|
10
|
+
* is not a separable delivery lane: splitting it out would leave every
|
|
11
|
+
* rendering rule (the dedupe, the ast-grep enrichment, the cap, both
|
|
12
|
+
* preambles) in the composer, which is the opposite of the extraction. The
|
|
13
|
+
* lane is the delivery contract; the stores are its sources.
|
|
14
|
+
*
|
|
15
|
+
* Registered surfaces (`clients/finding-delivery-gate.ts`):
|
|
16
|
+
* `runtime-turn:secrets-gitleaks`, `runtime-turn:secrets-trivy`,
|
|
17
|
+
* `runtime-turn:stale-secrets-tier`.
|
|
18
|
+
*
|
|
19
|
+
* History this file must not lose (each line below is a shipped defect):
|
|
20
|
+
*
|
|
21
|
+
* - #1617: these findings never passed through `applyDispositions`, so an
|
|
22
|
+
* agent-marked false-positive re-reported as a 🔴 STOP blocker every turn.
|
|
23
|
+
* - #1625 review: the disposition filter runs AFTER the freshness gate, over
|
|
24
|
+
* BOTH arms — an fp-marked finding that later goes stale must not reappear
|
|
25
|
+
* in the demoted tier.
|
|
26
|
+
* - #1622 review M1: a demoted secret keeps its rule id and source; only the
|
|
27
|
+
* line is withheld. An agent triages `aws-access-token` differently from a
|
|
28
|
+
* low-confidence `generic-api-key`, and cannot do that from a bare path.
|
|
29
|
+
* - #1622 review M2: the demoted tier is its OWN tier, never `advisoryParts`
|
|
30
|
+
* ("no action required this turn" directly above copy telling the agent to
|
|
31
|
+
* re-scan).
|
|
32
|
+
*/
|
|
33
|
+
import { recordDegradationOnce } from "../../degradation-ledger.js";
|
|
34
|
+
import { bounded } from "../../deadline-utils.js";
|
|
35
|
+
import { filterFindingsByDisposition } from "../../dispatch/finding-policy.js";
|
|
36
|
+
import { toRunnerDisplayPath } from "../../dispatch/runner-context.js";
|
|
37
|
+
import { classifyAndFilterFindings, } from "../../gitleaks-client.js";
|
|
38
|
+
import { HOOK_WALL_BUDGET_MS } from "../../hook-budgets.js";
|
|
39
|
+
import { gitleaksFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/gitleaks.js";
|
|
40
|
+
import { trivySecretFindingToProjectDiagnostic } from "../../project-diagnostics/runner-adapters/trivy.js";
|
|
41
|
+
import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "../../secret-findings.js";
|
|
42
|
+
import { STALE_LINE_MARKER } from "../../stale-marker.js";
|
|
43
|
+
/**
|
|
44
|
+
* Gitleaks deliberately scans gitignored local files and nested repositories
|
|
45
|
+
* so an explicit security audit can still inspect them. The adapter is the
|
|
46
|
+
* source of truth for whether a finding belongs in a blocking delivery lane;
|
|
47
|
+
* filter here before freshness handling so demoted findings cannot leak into
|
|
48
|
+
* either the blocker or the stale-secret turn context.
|
|
49
|
+
*/
|
|
50
|
+
async function blockingGitleaksFindings(data, ctx) {
|
|
51
|
+
const classification = await bounded(classifyAndFilterFindings(data?.findings ?? [], ctx.cwd), {
|
|
52
|
+
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
53
|
+
signal: ctx.signal,
|
|
54
|
+
hook: "turn_end",
|
|
55
|
+
label: "classifyAndFilterFindings",
|
|
56
|
+
});
|
|
57
|
+
if (classification === undefined) {
|
|
58
|
+
recordDegradationOnce({
|
|
59
|
+
kind: "gitleaks_classification_timeout",
|
|
60
|
+
subject: ctx.cwd,
|
|
61
|
+
reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return (classification ?? data?.findings ?? []).filter((finding) => gitleaksFindingToProjectDiagnostic(ctx.cwd, finding).semantic ===
|
|
65
|
+
"blocking");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* #1460/#1622: the gitleaks cache is TTL-only, so a finding for a file deleted
|
|
69
|
+
* after the scan was served as a 🔴 blocker for the rest of the 30-minute
|
|
70
|
+
* window — 119 of 126 findings in pi-lens's own cache. This lane is the single
|
|
71
|
+
* agent-facing consumer of that store (session_start's read only decides
|
|
72
|
+
* whether to re-scan; the project-diagnostics path re-scans fresh and
|
|
73
|
+
* reconciles at load), so the drop belongs here, before the findings enter the
|
|
74
|
+
* shared secret pipeline. A cited file EDITED after the scan keeps its finding
|
|
75
|
+
* but loses its line number: the credential may still be there, just not where
|
|
76
|
+
* the snapshot says. Dropping instead would let any edit — malicious or
|
|
77
|
+
* accidental — mute a real secret. Both stores take the default
|
|
78
|
+
* `onMissing: "drop"`: the finding IS the deleted file's content.
|
|
79
|
+
*/
|
|
80
|
+
async function collect(ctx) {
|
|
81
|
+
// One await for both stores: the two reads are independent, and the memo
|
|
82
|
+
// hands each of them the same promise the composer's own trivy read shares.
|
|
83
|
+
const [gitleaksEntry, trivyEntry] = await Promise.all([
|
|
84
|
+
ctx.readScannerCache("gitleaks"),
|
|
85
|
+
ctx.readScannerCache("trivy"),
|
|
86
|
+
]);
|
|
87
|
+
const gitleaksData = gitleaksEntry?.data;
|
|
88
|
+
const trivyData = trivyEntry?.data;
|
|
89
|
+
return {
|
|
90
|
+
gitleaks: {
|
|
91
|
+
findings: await blockingGitleaksFindings(gitleaksData, ctx),
|
|
92
|
+
citedPath: (finding) => finding.file,
|
|
93
|
+
scannedAt: gitleaksData?.scannedAt,
|
|
94
|
+
},
|
|
95
|
+
"trivy-secrets": {
|
|
96
|
+
findings: trivyData?.secrets ?? [],
|
|
97
|
+
citedPath: (finding) => finding.file,
|
|
98
|
+
scannedAt: trivyData?.scannedAt,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* #1617/#1625/#1628: filter through the SAME `ProjectDiagnostic` identity
|
|
104
|
+
* `lens_diagnostics mode=full` surfaces (tool="gitleaks",
|
|
105
|
+
* rule="gitleaks:<ruleId>" / tool="trivy", rule="trivy-secret:<ruleId>"), so a
|
|
106
|
+
* mark made against what the agent was SHOWN is honored here too — and over
|
|
107
|
+
* both arms, never live-only.
|
|
108
|
+
*
|
|
109
|
+
* ast-grep secret findings need no filtering here: they already went through
|
|
110
|
+
* dispatch's `applyDispositions` before reaching `peekActionableWarnings()`.
|
|
111
|
+
*/
|
|
112
|
+
function gate(gates, ctx) {
|
|
113
|
+
const gitleaksLive = filterFindingsByDisposition(gates.gitleaks.live, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
|
|
114
|
+
const gitleaksStale = filterFindingsByDisposition(gates.gitleaks.stale, ctx.cwd, (f) => gitleaksFindingToProjectDiagnostic(ctx.cwd, f));
|
|
115
|
+
const trivyLive = filterFindingsByDisposition(gates["trivy-secrets"].live, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
|
|
116
|
+
const trivyStale = filterFindingsByDisposition(gates["trivy-secrets"].stale, ctx.cwd, (f) => trivySecretFindingToProjectDiagnostic(ctx.cwd, f));
|
|
117
|
+
return {
|
|
118
|
+
gitleaksLive: gitleaksLive.kept,
|
|
119
|
+
gitleaksStale: gitleaksStale.kept,
|
|
120
|
+
trivyLive: trivyLive.kept,
|
|
121
|
+
trivyStale: trivyStale.kept,
|
|
122
|
+
suppressed: {
|
|
123
|
+
gitleaks: gitleaksLive.suppressed + gitleaksStale.suppressed,
|
|
124
|
+
"trivy-secrets": trivyLive.suppressed + trivyStale.suppressed,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function render(kept, ctx) {
|
|
129
|
+
const sessionSecrets = dedupeSecretFindings([
|
|
130
|
+
...fromGitleaks(kept.gitleaksLive),
|
|
131
|
+
...fromTrivySecrets(kept.trivyLive),
|
|
132
|
+
]);
|
|
133
|
+
// Locations already surfaced as session-scan secret blockers — used to
|
|
134
|
+
// enrich provenance where ast-grep agrees, and handed back so the
|
|
135
|
+
// actionable-warnings advisory can suppress its duplicate ast-grep copy.
|
|
136
|
+
const deliveredLocationKeys = new Set(sessionSecrets.map((f) => secretLocationKey(f.file, f.line)));
|
|
137
|
+
// Lane-local accumulators, deliberately NOT named after the turn's tiers:
|
|
138
|
+
// `blockerParts`/`staleSecretParts` are the composer's arrays, and a lane
|
|
139
|
+
// that pushed into one would be a render seam the `@delivery-surface:` tag
|
|
140
|
+
// scan cannot see (`tests/config/turn-end-lane-boundaries.test.ts`).
|
|
141
|
+
const blockerSections = [];
|
|
142
|
+
if (sessionSecrets.length) {
|
|
143
|
+
// Fold in ast-grep provenance ONLY where it coincides with a session
|
|
144
|
+
// secret — don't promote ast-grep-only findings out of their advisory
|
|
145
|
+
// tier.
|
|
146
|
+
const enriched = dedupeSecretFindings([
|
|
147
|
+
...sessionSecrets,
|
|
148
|
+
...fromAstGrepWarnings(ctx.peekActionableWarnings().filter(isSecretWarning)).filter((a) => deliveredLocationKeys.has(secretLocationKey(a.file, a.line))),
|
|
149
|
+
]);
|
|
150
|
+
const shown = enriched.slice(0, 5);
|
|
151
|
+
let report = "🔴 STOP — hardcoded secrets detected. Rotate the credentials and remove them from source:\n";
|
|
152
|
+
for (const f of shown) {
|
|
153
|
+
const where = `${toRunnerDisplayPath(ctx.cwd, f.file)}:${f.line}`;
|
|
154
|
+
report += ` ${where} — ${f.rule} [${f.sources.join(" + ")}]${f.description ? `: ${f.description}` : ""}\n`;
|
|
155
|
+
}
|
|
156
|
+
if (enriched.length > shown.length) {
|
|
157
|
+
report += ` … and ${enriched.length - shown.length} more\n`;
|
|
158
|
+
}
|
|
159
|
+
blockerSections.push(report);
|
|
160
|
+
}
|
|
161
|
+
// Demoted secrets are addressed by FILE, never by line — the line is the one
|
|
162
|
+
// field the edit invalidated. Deduped on file+rule+source so a file with
|
|
163
|
+
// twenty stale hits of one rule is named once.
|
|
164
|
+
const staleSecretEntries = [
|
|
165
|
+
...kept.gitleaksStale.map((f) => ({
|
|
166
|
+
file: toRunnerDisplayPath(ctx.cwd, f.file),
|
|
167
|
+
rule: f.ruleId,
|
|
168
|
+
source: "gitleaks",
|
|
169
|
+
})),
|
|
170
|
+
...kept.trivyStale.map((f) => ({
|
|
171
|
+
file: toRunnerDisplayPath(ctx.cwd, f.file),
|
|
172
|
+
rule: f.ruleId,
|
|
173
|
+
source: "trivy",
|
|
174
|
+
})),
|
|
175
|
+
];
|
|
176
|
+
const staleSecrets = [
|
|
177
|
+
...new Map(staleSecretEntries.map((e) => [`${e.file}|${e.rule}|${e.source}`, e])).values(),
|
|
178
|
+
];
|
|
179
|
+
const staleSections = [];
|
|
180
|
+
if (staleSecrets.length) {
|
|
181
|
+
const shown = staleSecrets.slice(0, 5);
|
|
182
|
+
let report = `🔑 ACTION NEEDED — secrets were flagged in files that changed after the scan. ${STALE_LINE_MARKER}\n` +
|
|
183
|
+
"The cached line numbers are no longer trustworthy, so they are withheld. Re-run a secrets scan to confirm or clear these:\n";
|
|
184
|
+
for (const entry of shown) {
|
|
185
|
+
report += ` ${entry.file} — ${entry.rule} [${entry.source}]\n`;
|
|
186
|
+
}
|
|
187
|
+
if (staleSecrets.length > shown.length) {
|
|
188
|
+
report += ` … and ${staleSecrets.length - shown.length} more\n`;
|
|
189
|
+
}
|
|
190
|
+
staleSections.push(report);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
blockerParts: blockerSections,
|
|
194
|
+
staleSecretParts: staleSections,
|
|
195
|
+
dispositionSuppressed: kept.suppressed,
|
|
196
|
+
deliveredLocationKeys,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** The lane. Stateless — one object, no per-turn state to reset. */
|
|
200
|
+
export const secretsLane = {
|
|
201
|
+
collect,
|
|
202
|
+
gate,
|
|
203
|
+
render,
|
|
204
|
+
};
|