gitnexus 1.6.8-rc.8 → 1.6.8
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 +3 -3
- package/dist/_shared/graph/types.d.ts +31 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +11 -1
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +6 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +6 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +31 -1
- package/dist/cli/ai-context.js +18 -16
- package/dist/cli/analyze-config.js +11 -0
- package/dist/cli/analyze.d.ts +14 -0
- package/dist/cli/analyze.js +98 -0
- package/dist/cli/embedding-dims.d.ts +33 -0
- package/dist/cli/embedding-dims.js +39 -0
- package/dist/cli/eval-server.d.ts +8 -0
- package/dist/cli/eval-server.js +242 -2
- package/dist/cli/help-i18n.js +19 -0
- package/dist/cli/i18n/en.d.ts +15 -1
- package/dist/cli/i18n/en.js +15 -1
- package/dist/cli/i18n/resources.d.ts +29 -1
- package/dist/cli/i18n/zh-CN.d.ts +14 -0
- package/dist/cli/i18n/zh-CN.js +15 -1
- package/dist/cli/index.js +89 -1
- package/dist/cli/mcp.d.ts +6 -1
- package/dist/cli/mcp.js +26 -1
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/setup.d.ts +3 -1
- package/dist/cli/setup.js +67 -17
- package/dist/cli/skill-gen.js +2 -1
- package/dist/cli/tool.d.ts +19 -1
- package/dist/cli/tool.js +96 -3
- package/dist/core/embeddings/hf-env.d.ts +0 -66
- package/dist/core/embeddings/http-client.d.ts +7 -0
- package/dist/core/embeddings/http-client.js +5 -3
- package/dist/core/embeddings/server-mapping.d.ts +1 -1
- package/dist/core/embeddings/server-mapping.js +7 -3
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +5 -1
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +22 -4
- package/dist/core/group/extractors/include-extractor.js +22 -13
- package/dist/core/group/sync.d.ts +27 -1
- package/dist/core/group/sync.js +0 -0
- package/dist/core/incremental/subgraph-extract.js +18 -1
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +114 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +196 -0
- package/dist/core/ingestion/cfg/collect.d.ts +51 -0
- package/dist/core/ingestion/cfg/collect.js +77 -0
- package/dist/core/ingestion/cfg/control-dependence.d.ts +58 -0
- package/dist/core/ingestion/cfg/control-dependence.js +182 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +106 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +124 -0
- package/dist/core/ingestion/cfg/emit.d.ts +293 -0
- package/dist/core/ingestion/cfg/emit.js +646 -0
- package/dist/core/ingestion/cfg/post-dominators.d.ts +79 -0
- package/dist/core/ingestion/cfg/post-dominators.js +176 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.d.ts +18 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.js +312 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +162 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +785 -0
- package/dist/core/ingestion/cfg/synthetic-escape.d.ts +124 -0
- package/dist/core/ingestion/cfg/synthetic-escape.js +300 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +274 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.d.ts +111 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +541 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.d.ts +74 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.js +584 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +195 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +350 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +124 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +589 -0
- package/dist/core/ingestion/cfg/visitors/csharp.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/csharp.js +867 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +278 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.js +870 -0
- package/dist/core/ingestion/cfg/visitors/dart.d.ts +119 -0
- package/dist/core/ingestion/cfg/visitors/dart.js +836 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.d.ts +160 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.js +621 -0
- package/dist/core/ingestion/cfg/visitors/go.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/go.js +638 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.js +513 -0
- package/dist/core/ingestion/cfg/visitors/java.d.ts +101 -0
- package/dist/core/ingestion/cfg/visitors/java.js +812 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +253 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +719 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +121 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.js +809 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +172 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.js +626 -0
- package/dist/core/ingestion/cfg/visitors/php.d.ts +96 -0
- package/dist/core/ingestion/cfg/visitors/php.js +721 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +212 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.js +739 -0
- package/dist/core/ingestion/cfg/visitors/python.d.ts +103 -0
- package/dist/core/ingestion/cfg/visitors/python.js +558 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +223 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +587 -0
- package/dist/core/ingestion/cfg/visitors/ruby.d.ts +112 -0
- package/dist/core/ingestion/cfg/visitors/ruby.js +756 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +324 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.js +873 -0
- package/dist/core/ingestion/cfg/visitors/rust.d.ts +95 -0
- package/dist/core/ingestion/cfg/visitors/rust.js +558 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.js +116 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +255 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.js +679 -0
- package/dist/core/ingestion/cfg/visitors/swift.d.ts +116 -0
- package/dist/core/ingestion/cfg/visitors/swift.js +787 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +149 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1056 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +583 -0
- package/dist/core/ingestion/import-resolvers/utils.js +2 -0
- package/dist/core/ingestion/language-provider.d.ts +23 -2
- package/dist/core/ingestion/languages/c-cpp.js +17 -5
- package/dist/core/ingestion/languages/cobol.js +4 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +41 -2
- package/dist/core/ingestion/languages/cpp/captures.js +50 -1
- package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +2 -0
- package/dist/core/ingestion/languages/cpp/conversion-rank.js +89 -0
- package/dist/core/ingestion/languages/cpp/header-scan.d.ts +1 -1
- package/dist/core/ingestion/languages/cpp/header-scan.js +2 -2
- package/dist/core/ingestion/languages/cpp/inline-namespaces.js +7 -2
- package/dist/core/ingestion/languages/cpp/member-lookup.js +2 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +2 -1
- package/dist/core/ingestion/languages/csharp.js +2 -0
- package/dist/core/ingestion/languages/dart.js +2 -0
- package/dist/core/ingestion/languages/go.js +2 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +8 -1
- package/dist/core/ingestion/languages/php.js +2 -0
- package/dist/core/ingestion/languages/python.js +2 -0
- package/dist/core/ingestion/languages/ruby.js +2 -0
- package/dist/core/ingestion/languages/rust.js +2 -0
- package/dist/core/ingestion/languages/swift.js +2 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/languages/vue.js +6 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/mro-processor.js +109 -0
- package/dist/core/ingestion/parsing-processor.js +44 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/index.js +2 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +0 -21
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
- package/dist/core/ingestion/pipeline-phases/taint-summaries.d.ts +30 -0
- package/dist/core/ingestion/pipeline-phases/taint-summaries.js +81 -0
- package/dist/core/ingestion/pipeline.d.ts +92 -10
- package/dist/core/ingestion/pipeline.js +11 -2
- package/dist/core/ingestion/scope-extractor.js +12 -2
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +9 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +9 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +28 -2
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +4 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +9 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +174 -40
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +24 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +276 -190
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +349 -1
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
- package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
- package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
- package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
- package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
- package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
- package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
- package/dist/core/ingestion/taint/call-summary-model.js +25 -0
- package/dist/core/ingestion/taint/emit.d.ts +125 -0
- package/dist/core/ingestion/taint/emit.js +208 -0
- package/dist/core/ingestion/taint/interproc-emit.d.ts +50 -0
- package/dist/core/ingestion/taint/interproc-emit.js +88 -0
- package/dist/core/ingestion/taint/interproc-solver.d.ts +110 -0
- package/dist/core/ingestion/taint/interproc-solver.js +312 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +659 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +107 -23
- package/dist/core/ingestion/taint/source-sink-config.js +35 -12
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +80 -0
- package/dist/core/ingestion/taint/summary-harvest-driver.js +179 -0
- package/dist/core/ingestion/taint/summary-harvest.d.ts +89 -0
- package/dist/core/ingestion/taint/summary-harvest.js +537 -0
- package/dist/core/ingestion/taint/summary-model.d.ts +200 -0
- package/dist/core/ingestion/taint/summary-model.js +86 -0
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +21 -0
- package/dist/core/ingestion/utils/env.d.ts +7 -0
- package/dist/core/ingestion/utils/env.js +12 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -0
- package/dist/core/ingestion/workers/parse-worker.js +81 -53
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -40
- package/dist/core/ingestion/workers/worker-pool.js +43 -16
- package/dist/core/lbug/csv-generator.d.ts +51 -3
- package/dist/core/lbug/csv-generator.js +369 -255
- package/dist/core/lbug/lbug-adapter.d.ts +58 -18
- package/dist/core/lbug/lbug-adapter.js +346 -84
- package/dist/core/lbug/lbug-config.d.ts +19 -0
- package/dist/core/lbug/lbug-config.js +29 -0
- package/dist/core/lbug/pdg-emit-sink.d.ts +127 -0
- package/dist/core/lbug/pdg-emit-sink.js +358 -0
- package/dist/core/lbug/pool-adapter.d.ts +35 -0
- package/dist/core/lbug/pool-adapter.js +90 -1
- package/dist/core/lbug/rel-pair-routing.d.ts +54 -0
- package/dist/core/lbug/rel-pair-routing.js +140 -0
- package/dist/core/lbug/schema.d.ts +2 -2
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/logger.d.ts +0 -5
- package/dist/core/run-analyze.d.ts +96 -0
- package/dist/core/run-analyze.js +207 -3
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/safe-parse.d.ts +0 -5
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/core/wiki/generator.js +3 -1
- package/dist/core/wiki/graph-queries.d.ts +6 -0
- package/dist/core/wiki/graph-queries.js +9 -1
- package/dist/mcp/http-transport.d.ts +134 -0
- package/dist/mcp/http-transport.js +503 -0
- package/dist/mcp/local/local-backend.d.ts +110 -24
- package/dist/mcp/local/local-backend.js +1225 -19
- package/dist/mcp/local/pdg-impact.d.ts +384 -0
- package/dist/mcp/local/pdg-impact.js +1972 -0
- package/dist/mcp/resources.js +13 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.js +214 -7
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/analyze-worker.js +7 -1
- package/dist/server/api.d.ts +17 -0
- package/dist/server/api.js +132 -168
- package/dist/server/git-clone.d.ts +43 -2
- package/dist/server/git-clone.js +211 -16
- package/dist/server/mcp-http.d.ts +5 -6
- package/dist/server/mcp-http.js +9 -82
- package/dist/server/middleware.d.ts +40 -0
- package/dist/server/middleware.js +92 -0
- package/dist/server/private-ip.d.ts +1 -0
- package/dist/server/private-ip.js +15 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +37 -0
- package/dist/server/upload-paths.js +57 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/git.js +5 -8
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +44 -10
- package/dist/storage/repo-manager.d.ts +121 -10
- package/dist/storage/repo-manager.js +43 -5
- package/dist/types/pipeline.d.ts +9 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +110 -10
- package/hooks/claude/gitnexus-hook.cjs +110 -13
- package/hooks/claude/hook-db-lock-probe.cjs +513 -26
- package/package.json +8 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/scripts/cross-platform-tests.ts +1 -0
- package/skills/gitnexus-debugging.md +16 -4
- package/skills/gitnexus-exploring.md +3 -3
- package/skills/gitnexus-guide.md +33 -0
- package/skills/gitnexus-pdg-query.md +89 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/skills/gitnexus-taint-analysis.md +178 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-D3Xd7zZE.js} +123 -123
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-ChH9xW-N.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-B4niSLvR.js → blockDiagram-7IZFK4PR-BkWhpzWB.js} +1 -1
- package/web/assets/{c4Diagram-Y2BXMSZH-DIzEXalR.js → c4Diagram-Y2BXMSZH-DtBvpzWD.js} +1 -1
- package/web/assets/{chunk-3SSMPTDK-BzGeeheI.js → chunk-3SSMPTDK-B4_etUhr.js} +2 -2
- package/web/assets/{chunk-5IMINLNL-CnTLsXIV.js → chunk-5IMINLNL-DOdTeQri.js} +1 -1
- package/web/assets/{chunk-5VCL7Z4A-w1BCx98Z.js → chunk-5VCL7Z4A-CQ-2dYWj.js} +1 -1
- package/web/assets/chunk-6764PJDD-DpIS7hxD.js +1 -0
- package/web/assets/{chunk-67TQ5CYL-CChC2cAv.js → chunk-67TQ5CYL-4sNPJU1q.js} +3 -3
- package/web/assets/{chunk-7W6UQGC5-CgFyfZ6k.js → chunk-7W6UQGC5-KyEG0HQg.js} +1 -1
- package/web/assets/{chunk-AZZRMDJM-5E9ARdDQ.js → chunk-AZZRMDJM-y-xJBuMz.js} +1 -1
- package/web/assets/{chunk-INKRHTLW-ELpfuk9h.js → chunk-INKRHTLW-Nu4ri9P-.js} +1 -1
- package/web/assets/{chunk-JQRUD6KW-Dcq_Qnyy.js → chunk-JQRUD6KW-Dbva2Z17.js} +1 -1
- package/web/assets/{chunk-KGFNY3KK-W-VR57xb.js → chunk-KGFNY3KK-JGMJvKCK.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +161 -0
- package/web/assets/{chunk-KNLZD3CH-BuZBOcso.js → chunk-KNLZD3CH-BGlP6X9R.js} +1 -1
- package/web/assets/{chunk-KRXBNO2N-DH2NPM7I.js → chunk-KRXBNO2N-BW5o0KUN.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-zoomRtWX.js} +1 -1
- package/web/assets/{chunk-LII3EMHJ-3X33tCpU.js → chunk-LII3EMHJ-D-jm-dLa.js} +1 -1
- package/web/assets/{chunk-QA3QBVWF-D5Ha3GgB.js → chunk-QA3QBVWF-BgRW3SXd.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-DD0B2N2B.js} +1 -1
- package/web/assets/{chunk-T2UQINTJ-kGYG7ppP.js → chunk-T2UQINTJ-DSBqkzH7.js} +1 -1
- package/web/assets/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
- package/web/assets/{chunk-UY5QBCOK-Du-c56gs.js → chunk-UY5QBCOK-DyF0vyGd.js} +1 -1
- package/web/assets/{chunk-W44A43WB-lrCpuupA.js → chunk-W44A43WB-JqMLIpR0.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CSZRiyOf.js → chunk-ZXARS5L4-CxN8oiwI.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-DJm9BgF-.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-DJm9BgF-.js +1 -0
- package/web/assets/{context-builder-_HS0v2ma.js → context-builder-BHiFUA8O.js} +10 -2
- package/web/assets/{cose-bilkent-UX7MHV2Q-BHv2bbtl.js → cose-bilkent-UX7MHV2Q-D71wNYRJ.js} +1 -1
- package/web/assets/dagre-ND4H6XIP-BIkxmhcu.js +4 -0
- package/web/assets/diagram-3NCE3AQN-BTa6iH0T.js +43 -0
- package/web/assets/diagram-GF46GFSD-DZDV0vBj.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-fUWreZzx.js → diagram-HNR7UZ2L-DFCfd5LI.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-BzM--Jj0.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-DFFwz0k4.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-CfHMJJqo.js} +1 -1
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +1 -0
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-Be805QiJ.js} +3 -3
- package/web/assets/{ganttDiagram-JCBTUEKG-BHQssTz6.js → ganttDiagram-JCBTUEKG-D3iL2Aet.js} +1 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +106 -0
- package/web/assets/index-BvYev1UJ.js +635 -0
- package/web/assets/index-_lgn7hs5.css +2 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-Y7BKSoke.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CT4SHFuI.js} +1 -1
- package/web/assets/{journeyDiagram-M6C3CM3L-DNPYCpvq.js → journeyDiagram-M6C3CM3L-CvArszLo.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-C6yDxr7N.js} +3 -3
- package/web/assets/{katex-K3KEBU37-CrVLz8l7.js → katex-K3KEBU37-CTc5BslQ.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-BOc2WEne.js} +1 -1
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +1 -0
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-Cvc0f_Pd.js} +1 -1
- package/web/assets/{quadrantDiagram-VICAPDV7-bAm6Hi_5.js → quadrantDiagram-VICAPDV7-yXSZ2lLz.js} +1 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BRcZvHs4.js} +1 -1
- package/web/assets/{sankeyDiagram-URQDO5SZ-LFJz53u5.js → sankeyDiagram-URQDO5SZ-1qEjlFIB.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-PcSCuvrB.js} +3 -3
- package/web/assets/stateDiagram-7D4R322I-D6qYSV1E.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-Bg-ImOSX.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-QWkh01xw.js} +1 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +1 -0
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-ak4DfwuM.js} +3 -3
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-CTzR4Loq.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-DA1BEaQz.js} +1 -1
- package/web/index.html +13 -15
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BETj2x4g.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-D1EXI0zA.js +0 -36
- package/web/assets/chunk-2T2R6R2M-n6s9JZqv.js +0 -4
- package/web/assets/chunk-2UTLFMKG-CMBB1TMN.js +0 -1
- package/web/assets/chunk-4R4BOZG6-mKhL59ul.js +0 -159
- package/web/assets/chunk-6764PJDD-ChwMM2z4.js +0 -1
- package/web/assets/chunk-7J6CGLKN-DT-b53FT.js +0 -10
- package/web/assets/chunk-C62D2QBJ-CDAWj26E.js +0 -1
- package/web/assets/chunk-CEXFNPSA-D68Tk6ls.js +0 -1
- package/web/assets/chunk-CilyBKbf.js +0 -1
- package/web/assets/chunk-J5EP6P6S-DxWW0yvu.js +0 -1
- package/web/assets/chunk-KGYTTC2M-C6PHeuay.js +0 -1
- package/web/assets/chunk-RERM46MO-DQNbXtfw.js +0 -1
- package/web/assets/chunk-RKZBBQEN-BmTPLSyv.js +0 -1
- package/web/assets/chunk-RLI5ZMPA-DExu2DOK.js +0 -1
- package/web/assets/chunk-T5OCTHI4--9wWpVws.js +0 -1
- package/web/assets/chunk-UP6H54XL-DsKdC6jC.js +0 -1
- package/web/assets/chunk-UXSXWOXI-DR81EqLr.js +0 -1
- package/web/assets/classDiagram-KGZ6W3CR-Bv93af_b.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CLfEqHUa.js +0 -1
- package/web/assets/dagre-ND4H6XIP-DsIr9MOv.js +0 -4
- package/web/assets/diagram-3NCE3AQN-BqAtKUpW.js +0 -43
- package/web/assets/diagram-GF46GFSD-CaBG6n6o.js +0 -24
- package/web/assets/diagram-QXG6HAR7-CZ-O3rcV.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-P4lSL4GH.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-B3myHUst.js +0 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DF3rtf4A.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CCwVLT__.js +0 -106
- package/web/assets/index-BKWA-m7o.css +0 -2
- package/web/assets/index-CG6q8eTs.js +0 -626
- package/web/assets/info-J43DQDTF-KCYPFFUO-R4XV_akJ.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-DmLICZx1.js +0 -2
- package/web/assets/packet-YPE3B663-LP52Z2RK-CgQPdNZa.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BzWOwc_c.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-B7KG0FN7.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-CB2nABwH.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-COBGd2RL.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CRSxEwLO.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-a0PfKoVn.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-BKQ6e5mC.js +0 -1
- /package/web/assets/{chunk-AQ6EADP3-CCFje6lL.js → chunk-AQ6EADP3-CZhslHi-.js} +0 -0
|
@@ -3,14 +3,57 @@
|
|
|
3
3
|
* with a command line that looks like a GitNexus MCP/serve server?
|
|
4
4
|
*
|
|
5
5
|
* Backends (no user-installed Sysinternals):
|
|
6
|
-
* - Linux:
|
|
7
|
-
*
|
|
6
|
+
* - Linux: cmdline-first procfs scan under /proc, no lsof at all (#2180). Three
|
|
7
|
+
* phases, cheapest first: (0) read /proc/<pid>/comm — a tiny task->comm read
|
|
8
|
+
* that never touches the target's mm — and keep only PIDs whose comm is a
|
|
9
|
+
* plausible node/gitnexus server; (1) read up to GITNEXUS_HOOK_PROC_CMDLINE_MAX
|
|
10
|
+
* bytes of /proc/<pid>/cmdline via openSync+readSync (bounded, so a D-state
|
|
11
|
+
* holder stuck on mmap_lock or a giant argv can't wedge the hook) and prefilter
|
|
12
|
+
* with isGitNexusServerCommand; (2) only for the 0..N survivors, stat their
|
|
13
|
+
* /proc/<pid>/fd/* and compare dev+inode against the target lbug. The lbug
|
|
14
|
+
* handle is fd-visible (a @ladybugdb/core property), so this finds every real
|
|
15
|
+
* owner without scanning every fd of every process.
|
|
8
16
|
* - macOS / *BSD / etc.: trusted lsof + ps (absolute paths first).
|
|
9
17
|
* - Windows: Restart Manager (rstrtmgr) via bundled PowerShell script +
|
|
10
18
|
* Win32_Process for command lines; trusted powershell.exe under %SystemRoot%.
|
|
11
19
|
*
|
|
12
|
-
* Fail
|
|
13
|
-
*
|
|
20
|
+
* Fail matrix:
|
|
21
|
+
* - Linux proc scan: owner found -> fail-closed (skip augment); budget exhausted
|
|
22
|
+
* (GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS) -> fail-CLOSED (#2180). This is a
|
|
23
|
+
* deliberate change from the old "timeout -> fail-open then try lsof" path.
|
|
24
|
+
* End-to-end the busy-host outcome is unchanged: the old code's lsof fallback
|
|
25
|
+
* ETIMEDOUT'd on the very hosts where the scan ran out of budget and ALSO
|
|
26
|
+
* failed closed there — the lsof leg only ever added 1-2s of dead work plus
|
|
27
|
+
* the orphan-storm risk it caused (#2163). What changes is that an overloaded
|
|
28
|
+
* host now self-throttles immediately (the throttle the incident needed)
|
|
29
|
+
* instead of paying for a doomed lsof. Mid-load hosts that used to fall
|
|
30
|
+
* through to a successful lsof now answer from the scan directly (faster) or,
|
|
31
|
+
* if even the scan can't finish in budget, fail closed (self-throttle) — a
|
|
32
|
+
* bounded, documented tradeoff, never an orphan.
|
|
33
|
+
* - macOS / other Unix: fail-open on most errors; fail-closed only on lsof
|
|
34
|
+
* ETIMEDOUT, matching the hook contract.
|
|
35
|
+
* - Windows: fail-closed only on PowerShell ETIMEDOUT.
|
|
36
|
+
*
|
|
37
|
+
* Unix subprocess containment contract (#2163):
|
|
38
|
+
* - lsof/ps are wrapped in coreutils `timeout`/`gtimeout` when a working
|
|
39
|
+
* wrapper is found (`timeout -k 1 <budget> lsof ...`). If this hook process
|
|
40
|
+
* is itself SIGKILLed (e.g. by the runner's 10s hook timeout) the wrapper
|
|
41
|
+
* survives, SIGTERMs its child at the budget (2s lsof / 1s ps) and SIGKILLs
|
|
42
|
+
* it 1s later — orphan lifetime is bounded at ~3s instead of unbounded.
|
|
43
|
+
* - GITNEXUS_HOOK_TIMEOUT_PATH: the sentinel value `disabled` switches the
|
|
44
|
+
* wrapper off deterministically; any other value is adopted only when it
|
|
45
|
+
* exists AND passes a one-shot `-k` exit-propagation self-test — otherwise
|
|
46
|
+
* resolution FALLS THROUGH to the built-in candidate list (first self-test
|
|
47
|
+
* pass wins), so no malformed value of any shape can silently disable
|
|
48
|
+
* orphan containment.
|
|
49
|
+
* - The gitnexus server is lazy-open + sticky-hold: an idle MCP server holds
|
|
50
|
+
* ZERO lbug fds until the repo's first MCP query, then keeps the fd open.
|
|
51
|
+
* A probe before that first query is therefore always false — a known,
|
|
52
|
+
* pre-existing race, not a bug in this probe.
|
|
53
|
+
* - resolveUnixGuardTimeout is exported so the hook adapters can wrap the
|
|
54
|
+
* `gitnexus augment` CLI child — the longest-lived hook subprocess (7s
|
|
55
|
+
* local / 12s npx inner budgets) — in the same guard; see runGitNexusCli
|
|
56
|
+
* in the adapters (#2163 follow-up).
|
|
14
57
|
*/
|
|
15
58
|
|
|
16
59
|
const fs = require('fs');
|
|
@@ -25,6 +68,16 @@ function isGitNexusServerCommand(command) {
|
|
|
25
68
|
return hasServerMode && hasGitNexus;
|
|
26
69
|
}
|
|
27
70
|
|
|
71
|
+
// GITNEXUS_DEBUG-gated stderr diagnostics. Reuses the exact gating predicate the
|
|
72
|
+
// Windows ps1-load warning already uses (===' 1' / ==='true') so there is one
|
|
73
|
+
// debug convention in this file, and writes via process.stderr.write (NOT a
|
|
74
|
+
// spawn) so it never perturbs the windowsHide spawn-count invariant.
|
|
75
|
+
function debugLog(msg) {
|
|
76
|
+
if (process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true') {
|
|
77
|
+
process.stderr.write(`[GitNexus hook] ${msg}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
28
81
|
function resolveHookBinary(tool) {
|
|
29
82
|
const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
|
|
30
83
|
const fromEnv = process.env[envKey];
|
|
@@ -46,6 +99,106 @@ function resolveHookBinary(tool) {
|
|
|
46
99
|
return tool;
|
|
47
100
|
}
|
|
48
101
|
|
|
102
|
+
function hasMissingHookBinaryOverride(tool) {
|
|
103
|
+
const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
|
|
104
|
+
const fromEnv = process.env[envKey];
|
|
105
|
+
if (!fromEnv || !String(fromEnv).trim()) return false;
|
|
106
|
+
try {
|
|
107
|
+
return !fs.existsSync(String(fromEnv).trim());
|
|
108
|
+
} catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sentinel:
|
|
114
|
+
// undefined = not resolved yet (resolve lazily, on first lsof/ps fallback)
|
|
115
|
+
// string = self-tested coreutils timeout/gtimeout path (use as wrapper)
|
|
116
|
+
// null = no usable wrapper (disabled, none found, or self-test failed)
|
|
117
|
+
let unixGuardTimeoutCache;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a coreutils `timeout`/`gtimeout` binary to wrap lsof/ps with
|
|
121
|
+
* (#2163). Unix-only by contract: the probe's win32 dispatch returns before
|
|
122
|
+
* reaching it, and the exported callers (the adapters' runGitNexusCli,
|
|
123
|
+
* #2163 follow-up) must check the platform first — the self-test below
|
|
124
|
+
* spawns /bin/sh. The memoized result is module-wide, so probe and adapter
|
|
125
|
+
* share one lazy self-test per hook process.
|
|
126
|
+
*
|
|
127
|
+
* GITNEXUS_HOOK_TIMEOUT_PATH semantics: the sentinel `disabled` turns the
|
|
128
|
+
* wrapper off; any other value is only a CANDIDATE — an existing file path
|
|
129
|
+
* is tried first, but it must pass the `-k` exit-propagation self-test to
|
|
130
|
+
* be adopted. On any failure (non-existent path, directory, non-executable
|
|
131
|
+
* file, wrapper without `-k` support, always-exit-0 stub, …) resolution
|
|
132
|
+
* falls through to the built-in candidates below, tried in order, first
|
|
133
|
+
* self-test pass wins. This is strictly stronger than the sibling
|
|
134
|
+
* GITNEXUS_HOOK_LSOF_PATH / GITNEXUS_HOOK_PS_PATH overrides (which only
|
|
135
|
+
* check existence): no bad env value of ANY shape can silently disable
|
|
136
|
+
* orphan containment.
|
|
137
|
+
*
|
|
138
|
+
* Lazy self-test: candidates are probed only when the lsof/ps fallback is
|
|
139
|
+
* first reached, and the result is memoized. A candidate is adopted only
|
|
140
|
+
* when `timeout -k 1 1 /bin/sh -c 'exit 42'` exits 42 — i.e. it must RUN
|
|
141
|
+
* the wrapped command AND PROPAGATE its exit status. This rejects two
|
|
142
|
+
* failure shapes: wrappers without the coreutils `-k` flag — busybox <1.34,
|
|
143
|
+
* toybox, broken symlinks — which would exit with a usage error without
|
|
144
|
+
* ever running lsof, silently converting the lsof-ETIMEDOUT fail-closed
|
|
145
|
+
* contract into fail-open (#1492 regression); and always-exit-0 stubs
|
|
146
|
+
* (/bin/true shapes), which would otherwise be adopted and "succeed" every
|
|
147
|
+
* wrapped spawn instantly without running it — a constant no-owner probe
|
|
148
|
+
* answer and, worse, a silently dead augment (status 0, empty stderr passes
|
|
149
|
+
* the adapters' success check with no context; #2163 follow-up review).
|
|
150
|
+
* Only when EVERY candidate fails does the probe fall back to the unwrapped
|
|
151
|
+
* status quo (memoized null). busybox ≥1.34 passes the test and is fully
|
|
152
|
+
* usable for everything THIS file spawns (lsof/ps are the guard's direct
|
|
153
|
+
* children) and for the adapters' direct-exec arm. The adapters' npx arm
|
|
154
|
+
* additionally relies on coreutils' process-GROUP signalling for its
|
|
155
|
+
* `-s KILL` grandchild reaping; busybox signals only its direct child, and
|
|
156
|
+
* this self-test deliberately does not probe that capability — see the
|
|
157
|
+
* adapter docblocks for the residual-gap statement.
|
|
158
|
+
*/
|
|
159
|
+
function passesGuardSelfTest(guard) {
|
|
160
|
+
try {
|
|
161
|
+
const selfTest = spawnSync(guard, ['-k', '1', '1', '/bin/sh', '-c', 'exit 42'], {
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
timeout: 3000,
|
|
164
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
});
|
|
167
|
+
return !selfTest.error && selfTest.status === 42;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveUnixGuardTimeout() {
|
|
174
|
+
if (unixGuardTimeoutCache !== undefined) return unixGuardTimeoutCache;
|
|
175
|
+
unixGuardTimeoutCache = null;
|
|
176
|
+
const fromEnv = process.env.GITNEXUS_HOOK_TIMEOUT_PATH;
|
|
177
|
+
const trimmed = fromEnv ? String(fromEnv).trim() : '';
|
|
178
|
+
if (trimmed === 'disabled') return unixGuardTimeoutCache;
|
|
179
|
+
const candidates = [];
|
|
180
|
+
if (trimmed && fs.existsSync(trimmed)) candidates.push(trimmed);
|
|
181
|
+
for (const builtin of [
|
|
182
|
+
'/usr/bin/timeout',
|
|
183
|
+
'/bin/timeout',
|
|
184
|
+
'/opt/homebrew/bin/gtimeout',
|
|
185
|
+
'/usr/local/bin/gtimeout',
|
|
186
|
+
]) {
|
|
187
|
+
try {
|
|
188
|
+
if (fs.existsSync(builtin)) candidates.push(builtin);
|
|
189
|
+
} catch {
|
|
190
|
+
/* ignore */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const candidate of candidates) {
|
|
194
|
+
if (passesGuardSelfTest(candidate)) {
|
|
195
|
+
unixGuardTimeoutCache = candidate;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return unixGuardTimeoutCache;
|
|
200
|
+
}
|
|
201
|
+
|
|
49
202
|
function resolveWindowsPowerShellPath() {
|
|
50
203
|
const fromEnv = process.env.GITNEXUS_HOOK_POWERSHELL_PATH;
|
|
51
204
|
if (fromEnv && String(fromEnv).trim() && fs.existsSync(String(fromEnv).trim())) {
|
|
@@ -132,76 +285,374 @@ function hasGitNexusServerOwnerWindows(dbPathAbs, myPid) {
|
|
|
132
285
|
return false;
|
|
133
286
|
}
|
|
134
287
|
|
|
135
|
-
|
|
288
|
+
// The procfs root every Linux scan path reads from. Production is always /proc;
|
|
289
|
+
// GITNEXUS_HOOK_PROC_ROOT only exists so unit tests can inject a fixture tree
|
|
290
|
+
// (comm + cmdline + fd symlinks) and assert the three-phase logic without
|
|
291
|
+
// scanning the real, ~hundreds-of-process /proc of the test host.
|
|
292
|
+
//
|
|
293
|
+
// Test-only gate (F4): the override is honored ONLY under a test runner —
|
|
294
|
+
// vitest injects VITEST="true" and NODE_ENV="test" into every worker (verified;
|
|
295
|
+
// a production hook is `node <file>.cjs` with neither set). Without the gate, a
|
|
296
|
+
// production env that accidentally leaked GITNEXUS_HOOK_PROC_ROOT (pointing at an
|
|
297
|
+
// empty/bad tree) would make readdirSync find no pids -> 'not-owned' -> Linux
|
|
298
|
+
// owner detection silently OFF (fail-OPEN: augment races the real server for the
|
|
299
|
+
// lbug, the #1492 class). Gating to the test signal makes that leak inert in
|
|
300
|
+
// production (always /proc) while the fake-procfs unit tests, which run under
|
|
301
|
+
// vitest, still inject freely. Unset env (or non-test context) => /proc, so the
|
|
302
|
+
// production path is byte-for-byte the historical behavior.
|
|
303
|
+
function isTestContext() {
|
|
304
|
+
return (
|
|
305
|
+
process.env.VITEST === 'true' || process.env.VITEST === '1' || process.env.NODE_ENV === 'test'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
function getProcRoot() {
|
|
309
|
+
if (!isTestContext()) return '/proc';
|
|
310
|
+
const raw = process.env.GITNEXUS_HOOK_PROC_ROOT;
|
|
311
|
+
return raw && String(raw).trim() ? String(raw) : '/proc';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Max bytes read from /proc/<pid>/cmdline in Phase 1. Bounded by default so a
|
|
315
|
+
// D-state holder wedged on mmap_lock, or a process with a pathological multi-MB
|
|
316
|
+
// argv, can't stall the hook. 16 KiB comfortably clears a realistic
|
|
317
|
+
// `node <abs path to .../node_modules/gitnexus/dist/cli/index.js> mcp` line
|
|
318
|
+
// (the `mcp`/`serve` mode token lives at the very tail, so the cap must be large
|
|
319
|
+
// enough to reach it — see PROC_CMDLINE_FLOOR escalation below). Overridable for
|
|
320
|
+
// tests; never goes below PROC_CMDLINE_FLOOR.
|
|
321
|
+
const PROC_CMDLINE_FLOOR = 4096;
|
|
322
|
+
function getCmdlineMaxBytes() {
|
|
323
|
+
const raw = process.env.GITNEXUS_HOOK_PROC_CMDLINE_MAX;
|
|
324
|
+
// Number() (not parseInt) so "8e3" reads as 8000, not 8 (parseInt stops at
|
|
325
|
+
// 'e'). The `raw && String(raw).trim()` guard keeps empty/whitespace on the
|
|
326
|
+
// default; trailing garbage ("8abc") now -> NaN -> default (stricter).
|
|
327
|
+
const n = raw && String(raw).trim() ? Number(String(raw).trim()) : NaN;
|
|
328
|
+
if (Number.isFinite(n) && n >= PROC_CMDLINE_FLOOR) return n;
|
|
329
|
+
return 16384;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Phase 0 comm prefilter. /proc/<pid>/comm is the kernel task->comm string,
|
|
333
|
+
// capped at 16 bytes INCLUDING the trailing NUL — i.e. at most 15 visible
|
|
334
|
+
// chars, truncated by the kernel with no marker. So a process whose real name
|
|
335
|
+
// is longer than 15 chars shows a 15-char prefix here. The match below is
|
|
336
|
+
// therefore truncation-safe in BOTH directions (a whitelist name that is a
|
|
337
|
+
// prefix of comm, or comm that is a prefix of a whitelist name, both count) to
|
|
338
|
+
// guarantee we never drop a real owner at this cheap stage — Phase 2's dev+ino
|
|
339
|
+
// fd check is the real authority; Phase 0/1 only exist to skip the overwhelming
|
|
340
|
+
// majority (kernel threads, shells, editors) cheaply.
|
|
341
|
+
//
|
|
342
|
+
// The whitelist is calibrated against what a real `gitnexus mcp`/`serve` server
|
|
343
|
+
// actually reports for comm. Observed on production hosts: the server renames
|
|
344
|
+
// its main thread, so comm reads `MainThread` (via @ladybugdb/core's
|
|
345
|
+
// worker_threads setup), NOT `node` — omitting it would blind the probe to
|
|
346
|
+
// every real server (#1492-class owner miss). We also keep the plausible
|
|
347
|
+
// launcher/runtime basenames in case a future build does not rename the thread.
|
|
348
|
+
// Conservative by design: over-collecting a few extra candidates only costs a
|
|
349
|
+
// bounded number of Phase 1 cmdline reads.
|
|
350
|
+
const COMM_CANDIDATES = ['node', 'gitnexus', 'bun', 'deno', 'npm', 'npx', 'MainThread'];
|
|
351
|
+
function commLooksLikeServer(comm) {
|
|
352
|
+
const c = comm.trim();
|
|
353
|
+
if (!c) return false;
|
|
354
|
+
for (const name of COMM_CANDIDATES) {
|
|
355
|
+
if (name === c || name.startsWith(c) || c.startsWith(name)) return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readProcComm(procRoot, pidStr) {
|
|
136
361
|
try {
|
|
137
|
-
return fs
|
|
362
|
+
return fs
|
|
363
|
+
.readFileSync(path.join(procRoot, pidStr, 'comm'), 'utf8')
|
|
364
|
+
.replace(/\0+/g, '')
|
|
365
|
+
.trim();
|
|
138
366
|
} catch {
|
|
139
367
|
return '';
|
|
140
368
|
}
|
|
141
369
|
}
|
|
142
370
|
|
|
143
|
-
|
|
371
|
+
// Timeout sentinel for readLinuxCmdline (F3). MUST be distinct from the
|
|
372
|
+
// "unreadable/empty" return value (''): '' flows through isGitNexusServerCommand
|
|
373
|
+
// as a NON-candidate (both regexes are false on ''), so the Phase 1 caller
|
|
374
|
+
// `continue`s past it — correct for a raced/openSync-failed pid, but a FAIL-OPEN
|
|
375
|
+
// bug if it ever meant "I ran out of budget mid-read" (a real owner whose
|
|
376
|
+
// escalation timed out would be silently dropped, racing the lbug -> #1492). A
|
|
377
|
+
// unique Symbol can never collide with any cmdline string, so the caller can
|
|
378
|
+
// branch on it explicitly and map a mid-read timeout to the tri-state 'timeout'
|
|
379
|
+
// (fail-CLOSED) instead of swallowing it as a non-candidate.
|
|
380
|
+
const CMDLINE_TIMEOUT = Symbol('gitnexus.cmdline.timeout');
|
|
381
|
+
|
|
382
|
+
// Bounded /proc/<pid>/cmdline read for Phase 1. openSync+readSync (not
|
|
383
|
+
// readFileSync) so a D-state holder cannot stall the hook on a huge or
|
|
384
|
+
// never-EOF argv: we read at most `cap` bytes and stop. cmdline separates argv
|
|
385
|
+
// with NULs; convert to spaces for isGitNexusServerCommand.
|
|
386
|
+
//
|
|
387
|
+
// Owner-miss guard for the 4 KB cap: the `gitnexus` token usually sits in the
|
|
388
|
+
// first path component while the `mcp`/`serve` mode token is the LAST argv, so
|
|
389
|
+
// a naive 4 KB read could clip the mode token off a server launched with a very
|
|
390
|
+
// long interpreter path and silently miss a real owner. We mitigate two ways:
|
|
391
|
+
// (a) the default cap (16 KiB) already clears realistic lines; (b) if the first
|
|
392
|
+
// read fills the cap AND already contains the `gitnexus` token but no mode
|
|
393
|
+
// token yet, we keep reading in bounded chunks (up to a hard ceiling) until the
|
|
394
|
+
// mode token appears or the file ends — so a genuine server is never missed for
|
|
395
|
+
// want of a few more bytes, while non-candidates still pay only the initial
|
|
396
|
+
// bounded read.
|
|
397
|
+
//
|
|
398
|
+
// Budget (F3): the escalation loop above is the one place a SINGLE pathological
|
|
399
|
+
// candidate could read up to HARD_CEIL (256 KiB) before the next scan-level
|
|
400
|
+
// budget check, weakening the timeout contract. `outOfBudget` (the scan's shared
|
|
401
|
+
// deadline callback) is checked once per escalation iteration; on expiry we
|
|
402
|
+
// return CMDLINE_TIMEOUT (NOT '') so the caller can fail-closed honestly rather
|
|
403
|
+
// than mistake the partial read for a non-candidate. Reads that simply can't
|
|
404
|
+
// open / error out still return '' (genuinely "not a readable candidate").
|
|
405
|
+
function readLinuxCmdline(procRoot, pidStr, cap, outOfBudget) {
|
|
406
|
+
const file = path.join(procRoot, pidStr, 'cmdline');
|
|
407
|
+
let fd;
|
|
408
|
+
try {
|
|
409
|
+
fd = fs.openSync(file, 'r');
|
|
410
|
+
} catch {
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const HARD_CEIL = 262144; // 256 KiB absolute ceiling for the escalation path
|
|
415
|
+
let collected = Buffer.alloc(0);
|
|
416
|
+
let offset = 0;
|
|
417
|
+
let chunkCap = cap;
|
|
418
|
+
for (;;) {
|
|
419
|
+
// allocUnsafe is safe here: readSync fills exactly [0, bytes), only
|
|
420
|
+
// buf.subarray(0, bytes) is consumed, and Buffer.concat deep-copies that
|
|
421
|
+
// slice into `collected`, so the uninitialized tail never reaches decode.
|
|
422
|
+
const buf = Buffer.allocUnsafe(chunkCap);
|
|
423
|
+
const bytes = fs.readSync(fd, buf, 0, chunkCap, offset);
|
|
424
|
+
if (bytes <= 0) break;
|
|
425
|
+
collected = Buffer.concat([collected, buf.subarray(0, bytes)]);
|
|
426
|
+
offset += bytes;
|
|
427
|
+
const text = collected.toString('utf8').replace(/\0+/g, ' ');
|
|
428
|
+
// Stop early when we can already decide "owner": has both the gitnexus
|
|
429
|
+
// token and a mode token. Keep going only when gitnexus is present but
|
|
430
|
+
// the mode token might be just past the boundary.
|
|
431
|
+
const hasGitNexus =
|
|
432
|
+
/(?:^|[/\\\s])gitnexus(?:\.cmd)?(?:\s|$)/.test(text) ||
|
|
433
|
+
/node_modules[/\\]gitnexus[/\\]/.test(text);
|
|
434
|
+
const hasMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(text);
|
|
435
|
+
if (hasMode) break; // decided (positive); isGitNexusServerCommand re-checks below
|
|
436
|
+
if (bytes < chunkCap) break; // EOF: full cmdline read, definitive
|
|
437
|
+
if (!hasGitNexus) break; // not a candidate; do not escalate the read
|
|
438
|
+
if (offset >= HARD_CEIL) break; // bounded escalation only
|
|
439
|
+
// Budget gate the escalation: a single huge-argv candidate must not burn
|
|
440
|
+
// the whole scan deadline before we re-check. Return the timeout sentinel
|
|
441
|
+
// (never '') so the caller fails closed instead of treating us as a
|
|
442
|
+
// non-candidate. The sole caller (linuxProcScanFindGitNexusServer) always
|
|
443
|
+
// passes outOfBudget, so no presence guard is needed.
|
|
444
|
+
if (outOfBudget()) return CMDLINE_TIMEOUT;
|
|
445
|
+
chunkCap = cap; // keep reading more in cap-sized chunks
|
|
446
|
+
}
|
|
447
|
+
return collected.toString('utf8').replace(/\0+/g, ' ').trim();
|
|
448
|
+
} catch {
|
|
449
|
+
return '';
|
|
450
|
+
} finally {
|
|
451
|
+
try {
|
|
452
|
+
fs.closeSync(fd);
|
|
453
|
+
} catch {
|
|
454
|
+
/* ignore */
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function resolveLinuxProcBudgetMs() {
|
|
144
460
|
const raw = process.env.GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS;
|
|
145
|
-
|
|
461
|
+
// Gate on the STRING's emptiness, NOT the parsed number's truthiness — the
|
|
462
|
+
// old `Number(raw && trim()) ? ... : 1200` form treated "0" as falsy and
|
|
463
|
+
// silently fell back to 1200 (#2180). Use Number() (not parseInt) so "16e3"
|
|
464
|
+
// reads as 16000, not 16 (parseInt stops at 'e'). The `&& String(raw).trim()`
|
|
465
|
+
// guard is load-bearing: without it a set-but-empty/whitespace value would be
|
|
466
|
+
// `Number("")===0` => budget 0 => immediate fail-CLOSED timeout (augment
|
|
467
|
+
// permanently skipped). With it, ''/whitespace => NaN => 1200 default, while a
|
|
468
|
+
// finite "0" still parses to an explicit, deterministic "no budget" =>
|
|
469
|
+
// immediate timeout. Non-numeric / unset => default 1200.
|
|
470
|
+
const n = raw != null && String(raw).trim() ? Number(String(raw).trim()) : NaN;
|
|
471
|
+
if (!Number.isFinite(n)) return 1200;
|
|
472
|
+
return n; // may be <= 0, meaning "out of budget on the first check"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Returns one of: 'owned' (a non-self process with a GitNexus-server cmdline
|
|
476
|
+
// holds the target lbug fd), 'not-owned' (scan completed, no such owner), or
|
|
477
|
+
// 'timeout' (the per-scan budget was exhausted before a verdict). The name is
|
|
478
|
+
// pinned by a source-contract test; only the return TYPE changed (#2180:
|
|
479
|
+
// boolean -> tri-state, so the dispatcher can fail-closed on 'timeout').
|
|
480
|
+
function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
|
|
481
|
+
const budget = resolveLinuxProcBudgetMs();
|
|
482
|
+
// A non-positive budget is an explicit, deterministic "no time to scan" =>
|
|
483
|
+
// immediate timeout (the #2180 test vector, and the only correct reading of
|
|
484
|
+
// the fixed parse: "0" must NOT mean 1200). Returning before any procfs read
|
|
485
|
+
// keeps it instantaneous regardless of host load.
|
|
486
|
+
if (budget <= 0) return 'timeout';
|
|
487
|
+
const procRoot = getProcRoot();
|
|
488
|
+
const cmdlineCap = getCmdlineMaxBytes();
|
|
146
489
|
const start = Date.now();
|
|
490
|
+
const outOfBudget = () => Date.now() - start > budget;
|
|
491
|
+
|
|
147
492
|
let targetStat;
|
|
148
493
|
try {
|
|
149
494
|
targetStat = fs.statSync(dbPathAbs);
|
|
150
495
|
} catch {
|
|
151
|
-
|
|
496
|
+
// Caller already existsSync'd the path; a stat failure here is a transient
|
|
497
|
+
// race, treat as no owner (historical semantics).
|
|
498
|
+
return 'not-owned';
|
|
152
499
|
}
|
|
500
|
+
|
|
153
501
|
let procEntries;
|
|
154
502
|
try {
|
|
155
|
-
procEntries = fs.readdirSync(
|
|
503
|
+
procEntries = fs.readdirSync(procRoot, { withFileTypes: true });
|
|
156
504
|
} catch {
|
|
157
|
-
return
|
|
505
|
+
return 'not-owned';
|
|
158
506
|
}
|
|
507
|
+
|
|
508
|
+
// Phase 0 + Phase 1: collect the few PIDs whose comm AND cmdline look like a
|
|
509
|
+
// GitNexus server, without touching any fd yet.
|
|
510
|
+
const candidates = [];
|
|
159
511
|
for (const ent of procEntries) {
|
|
160
|
-
if (
|
|
512
|
+
if (outOfBudget()) return 'timeout';
|
|
161
513
|
if (!ent.isDirectory() || !/^\d+$/.test(ent.name)) continue;
|
|
162
514
|
const pid = Number.parseInt(ent.name, 10);
|
|
163
515
|
if (!Number.isFinite(pid) || pid === myPid) continue;
|
|
164
|
-
|
|
516
|
+
|
|
517
|
+
// Phase 0: cheap comm prefilter.
|
|
518
|
+
const comm = readProcComm(procRoot, ent.name);
|
|
519
|
+
if (!comm) continue; // unreadable comm (kernel thread, raced exit) -> skip
|
|
520
|
+
if (!commLooksLikeServer(comm)) continue;
|
|
521
|
+
|
|
522
|
+
// Phase 1: bounded cmdline read + isGitNexusServerCommand prefilter.
|
|
523
|
+
if (outOfBudget()) return 'timeout';
|
|
524
|
+
const cmdline = readLinuxCmdline(procRoot, ent.name, cmdlineCap, outOfBudget);
|
|
525
|
+
// F3: a mid-read budget timeout returns the CMDLINE_TIMEOUT sentinel (a
|
|
526
|
+
// Symbol, never a string). Fail CLOSED on it rather than letting it fall
|
|
527
|
+
// through isGitNexusServerCommand as a non-candidate — a real owner whose
|
|
528
|
+
// escalation timed out must not be silently dropped (would fail-OPEN).
|
|
529
|
+
if (cmdline === CMDLINE_TIMEOUT) return 'timeout';
|
|
530
|
+
if (!isGitNexusServerCommand(cmdline)) continue;
|
|
531
|
+
candidates.push(ent.name);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Phase 2: only now stat the fds of the (typically 0-2) survivors.
|
|
535
|
+
for (const pidStr of candidates) {
|
|
536
|
+
if (outOfBudget()) return 'timeout';
|
|
537
|
+
const fdDir = path.join(procRoot, pidStr, 'fd');
|
|
165
538
|
let fds;
|
|
166
539
|
try {
|
|
167
540
|
fds = fs.readdirSync(fdDir);
|
|
168
|
-
} catch {
|
|
541
|
+
} catch (err) {
|
|
542
|
+
// F1: the old code returned 'owned' for EVERY non-ENOENT error. That was
|
|
543
|
+
// a correctness bug: /proc/<pid>/fd is owner-only (mode 0500), so a
|
|
544
|
+
// cross-user/root `gitnexus mcp` serving a DIFFERENT repo passes Phase 0+1
|
|
545
|
+
// (its cmdline matches) and then EACCES'es here — yet its dev+ino was
|
|
546
|
+
// NEVER compared against THIS lbug. Claiming 'owned' lets it permanently,
|
|
547
|
+
// silently suppress augment for a repo it does not actually lock. We now
|
|
548
|
+
// distinguish the failure shapes (all still fail-closed where we can't
|
|
549
|
+
// prove non-ownership, but 'timeout' is the HONEST verdict for
|
|
550
|
+
// "inconclusive", not the false-positive 'owned'):
|
|
551
|
+
const code = err && err.code;
|
|
552
|
+
if (code === 'ENOENT') {
|
|
553
|
+
// Process raced away between the candidate scan and now -> genuinely no
|
|
554
|
+
// longer an owner. Move on.
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
558
|
+
// Permission-denied fd dir: cannot read fds, so ownership is
|
|
559
|
+
// UNVERIFIABLE. Fail closed honestly via 'timeout' (the dispatcher maps
|
|
560
|
+
// timeout -> true, same protective skip as before) WITHOUT lying that we
|
|
561
|
+
// confirmed ownership. Do NOT degrade to not-owned/fail-open: if this
|
|
562
|
+
// really is the owner, fail-open re-opens the #1492 lbug race; augment
|
|
563
|
+
// is optional context, so a conservative skip costs little.
|
|
564
|
+
debugLog(
|
|
565
|
+
`fd dir unreadable for candidate pid ${pidStr} (${code}); ownership ` +
|
|
566
|
+
`unverifiable, probe inconclusive -> fail-closed (timeout)`,
|
|
567
|
+
);
|
|
568
|
+
return 'timeout';
|
|
569
|
+
}
|
|
570
|
+
if (code === 'EIO' || code === 'ESTALE') {
|
|
571
|
+
// Genuine transient I/O against this candidate's fd dir — not evidence
|
|
572
|
+
// it does NOT hold the lbug. Treat as inconclusive and fail closed
|
|
573
|
+
// (timeout) rather than continue, so a real owner mid-I/O-blip is not
|
|
574
|
+
// dropped (would fail-open).
|
|
575
|
+
debugLog(
|
|
576
|
+
`fd dir transient I/O error for candidate pid ${pidStr} (${code}); ` +
|
|
577
|
+
`probe inconclusive -> fail-closed (timeout)`,
|
|
578
|
+
);
|
|
579
|
+
return 'timeout';
|
|
580
|
+
}
|
|
581
|
+
// Any other shape (ENOTDIR — fd path is not a directory at all, so this
|
|
582
|
+
// is not a plausible live-procfs owner — and the long tail) is treated as
|
|
583
|
+
// "this candidate is not an owner": move to the next candidate instead of
|
|
584
|
+
// the old blanket 'owned'. If no other candidate owns the lbug the scan
|
|
585
|
+
// ends not-owned (dispatcher fail-open) — acceptable because ENOTDIR means
|
|
586
|
+
// the fd entry is structurally not a real /proc/<pid>/fd.
|
|
587
|
+
debugLog(
|
|
588
|
+
`fd dir not a readable directory for candidate pid ${pidStr} ` +
|
|
589
|
+
`(${code || 'unknown'}); treating candidate as non-owner -> continue`,
|
|
590
|
+
);
|
|
169
591
|
continue;
|
|
170
592
|
}
|
|
171
|
-
let holds = false;
|
|
172
593
|
for (const fd of fds) {
|
|
173
|
-
if (
|
|
594
|
+
if (outOfBudget()) return 'timeout';
|
|
174
595
|
try {
|
|
175
596
|
const st = fs.statSync(path.join(fdDir, fd));
|
|
176
597
|
if (st.dev === targetStat.dev && st.ino === targetStat.ino) {
|
|
177
|
-
|
|
178
|
-
break;
|
|
598
|
+
return 'owned';
|
|
179
599
|
}
|
|
180
600
|
} catch {
|
|
181
|
-
/* ignore */
|
|
601
|
+
/* fd raced closed; ignore */
|
|
182
602
|
}
|
|
183
603
|
}
|
|
184
|
-
if (!holds) continue;
|
|
185
|
-
if (isGitNexusServerCommand(readLinuxCmdline(ent.name))) return true;
|
|
186
604
|
}
|
|
187
|
-
|
|
605
|
+
|
|
606
|
+
return 'not-owned';
|
|
188
607
|
}
|
|
189
608
|
|
|
190
609
|
function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
|
|
610
|
+
const guard = resolveUnixGuardTimeout();
|
|
611
|
+
// An explicit missing override models ENOENT and must fail open instead of
|
|
612
|
+
// falling through to a host binary with different process-table visibility.
|
|
613
|
+
if (hasMissingHookBinaryOverride('lsof')) return false;
|
|
191
614
|
const lsofPath = resolveHookBinary('lsof');
|
|
192
|
-
|
|
615
|
+
// The spawnSync timeouts below (lsof 1000ms / ps 500ms) are deliberately
|
|
616
|
+
// SHORTER than the wrapper budgets (2s / 1s): on the supervised path Node's
|
|
617
|
+
// SIGTERM always fires first, so `error.code === 'ETIMEDOUT'` and the
|
|
618
|
+
// fail-closed contract are untouched. The wrapper only matters once this
|
|
619
|
+
// hook process has been SIGKILLed and can no longer deliver that SIGTERM.
|
|
620
|
+
const [lsofCmd, lsofArgs] = guard
|
|
621
|
+
? [guard, ['-k', '1', '2', lsofPath, '-nP', '-t', '--', dbPathAbs]]
|
|
622
|
+
: [lsofPath, ['-nP', '-t', '--', dbPathAbs]];
|
|
623
|
+
const lsof = spawnSync(lsofCmd, lsofArgs, {
|
|
193
624
|
encoding: 'utf-8',
|
|
194
625
|
timeout: 1000,
|
|
195
626
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
196
627
|
windowsHide: true,
|
|
197
628
|
});
|
|
198
629
|
if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
|
|
630
|
+
// Guard-mediated deaths map to "unresponsive holder" (fail-closed). Three
|
|
631
|
+
// result shapes, verified against coreutils 9.1:
|
|
632
|
+
// - signal-death: when `-k` escalates to SIGKILL, coreutils timeout
|
|
633
|
+
// SELF-RAISES the signal, so spawnSync reports {status: null, signal}
|
|
634
|
+
// with no .error (spawnSync's own ETIMEDOUT was handled above). The
|
|
635
|
+
// same shape appears when this hook is frozen >2s (SIGSTOP, laptop
|
|
636
|
+
// suspend) and the guard expires while it sleeps. By construction, a
|
|
637
|
+
// guard-wrapped probe that died by signal without spawnSync ETIMEDOUT
|
|
638
|
+
// is a budget/kill outcome.
|
|
639
|
+
// - 124: budget expired and the child exited after the plain SIGTERM.
|
|
640
|
+
// - 137: NOT the coreutils -k path — only exit-code-propagating wrappers,
|
|
641
|
+
// or a child SIGKILLed externally (e.g. the OOM killer).
|
|
642
|
+
if (guard && lsof.status === null && lsof.signal) return true;
|
|
643
|
+
if (guard && (lsof.status === 124 || lsof.status === 137)) return true;
|
|
199
644
|
|
|
200
645
|
const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
|
|
646
|
+
const psMissing = hasMissingHookBinaryOverride('ps');
|
|
201
647
|
const psPath = resolveHookBinary('ps');
|
|
202
648
|
for (const pid of pids) {
|
|
203
649
|
if (Number(pid) === myPid) continue;
|
|
204
|
-
|
|
650
|
+
// Missing ps means we cannot verify that this pid is a GitNexus server.
|
|
651
|
+
if (psMissing) continue;
|
|
652
|
+
const [psCmd, psArgs] = guard
|
|
653
|
+
? [guard, ['-k', '1', '1', psPath, '-p', pid, '-o', 'command=']]
|
|
654
|
+
: [psPath, ['-p', pid, '-o', 'command=']];
|
|
655
|
+
const ps = spawnSync(psCmd, psArgs, {
|
|
205
656
|
encoding: 'utf-8',
|
|
206
657
|
timeout: 500,
|
|
207
658
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -211,6 +662,11 @@ function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
|
|
|
211
662
|
if (ps.error.code === 'ETIMEDOUT') return true;
|
|
212
663
|
continue;
|
|
213
664
|
}
|
|
665
|
+
// Same guard-mediated-death mapping as the lsof call above (signal-death
|
|
666
|
+
// from the -k escalation or a frozen hook; 124 budget expiry; 137 only
|
|
667
|
+
// for exit-code-propagating wrappers / external SIGKILL).
|
|
668
|
+
if (guard && ps.status === null && ps.signal) return true;
|
|
669
|
+
if (guard && (ps.status === 124 || ps.status === 137)) return true;
|
|
214
670
|
if (isGitNexusServerCommand(ps.stdout || '')) return true;
|
|
215
671
|
}
|
|
216
672
|
return false;
|
|
@@ -229,8 +685,13 @@ function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
|
|
|
229
685
|
}
|
|
230
686
|
|
|
231
687
|
if (process.platform === 'linux') {
|
|
232
|
-
|
|
233
|
-
|
|
688
|
+
// #2180: cmdline-first procfs scan, no lsof. 'timeout' fails CLOSED
|
|
689
|
+
// (overloaded host self-throttles — the throttle the orphan-storm incident
|
|
690
|
+
// needed; the old lsof fallback ETIMEDOUT'd and failed closed on these same
|
|
691
|
+
// hosts anyway, only slower and with the orphan risk). 'not-owned' is the
|
|
692
|
+
// only false. See the fail matrix in the file header.
|
|
693
|
+
const verdict = linuxProcScanFindGitNexusServer(dbPathAbs, myPid);
|
|
694
|
+
return verdict !== 'not-owned';
|
|
234
695
|
}
|
|
235
696
|
|
|
236
697
|
return unixLsofPsFindGitNexusServer(dbPathAbs, myPid);
|
|
@@ -238,4 +699,30 @@ function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
|
|
|
238
699
|
|
|
239
700
|
module.exports = {
|
|
240
701
|
hasGitNexusDbLockedByGitNexusServer,
|
|
702
|
+
// Exported for white-box unit tests that must assert the tri-state verdict
|
|
703
|
+
// ('owned' | 'not-owned' | 'timeout') directly — the dispatcher collapses
|
|
704
|
+
// timeout and owned to the same boolean true, so the boolean API alone cannot
|
|
705
|
+
// distinguish the F1 EACCES->timeout fix from the old EACCES->owned bug. The
|
|
706
|
+
// Probe interface already declares this optional. Linux-only by contract; the
|
|
707
|
+
// name is pinned by a source-contract test.
|
|
708
|
+
linuxProcScanFindGitNexusServer,
|
|
709
|
+
// #2163 follow-up: the hook adapters wrap the augment CLI in the same
|
|
710
|
+
// guard. Returns a self-tested wrapper path — the built-in candidates are
|
|
711
|
+
// always absolute; a GITNEXUS_HOOK_TIMEOUT_PATH override is adopted as the
|
|
712
|
+
// exact string that passed the self-test. Same string is also the same
|
|
713
|
+
// RESOLUTION for absolute paths and for slashless names (PATH lookup is
|
|
714
|
+
// cwd-independent); a slash-containing RELATIVE override, however, is
|
|
715
|
+
// existsSync-checked and self-tested against this process's cwd while the
|
|
716
|
+
// adapters spawn the CLI with a `cwd` option (chdir-before-exec), so such
|
|
717
|
+
// a value can pass here yet ENOENT at the augment call site — set the
|
|
718
|
+
// override to an absolute path. Returns null when the wrapper is
|
|
719
|
+
// disabled/unavailable. Never call on win32 (see its JSDoc).
|
|
720
|
+
resolveUnixGuardTimeout,
|
|
721
|
+
// Exported for white-box unit tests of the numeric-env parsing (#2183 review):
|
|
722
|
+
// Number()-not-parseInt so "16e3" reads as 16000, plus the empty/whitespace
|
|
723
|
+
// guard that keeps a set-but-empty budget on the 1200 default instead of an
|
|
724
|
+
// immediate fail-closed timeout. Tested directly because the values are
|
|
725
|
+
// otherwise only observable indirectly through scan timing/escalation.
|
|
726
|
+
getCmdlineMaxBytes,
|
|
727
|
+
resolveLinuxProcBudgetMs,
|
|
241
728
|
};
|