clinkx 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +10 -0
- package/README.md +82 -26
- package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
- package/clinkx-workflows/dist/allowed-roots.js +5 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
- package/clinkx-workflows/dist/artifacts.d.ts +20 -1
- package/clinkx-workflows/dist/artifacts.js +38 -9
- package/clinkx-workflows/dist/artifacts.js.map +1 -1
- package/clinkx-workflows/dist/backend.d.ts +4 -0
- package/clinkx-workflows/dist/citation-check.d.ts +40 -0
- package/clinkx-workflows/dist/citation-check.js +348 -0
- package/clinkx-workflows/dist/citation-check.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +2 -1
- package/clinkx-workflows/dist/clink-client.js +17 -4
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +145 -16
- package/clinkx-workflows/dist/config.js +384 -26
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.d.ts +172 -0
- package/clinkx-workflows/dist/contained-file.js +483 -0
- package/clinkx-workflows/dist/contained-file.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
- package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
- package/clinkx-workflows/dist/context-cache-key.js +91 -0
- package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
- package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
- package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
- package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
- package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
- package/clinkx-workflows/dist/context-preflight.js +91 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +1 -1
- package/clinkx-workflows/dist/council-expander.js +2 -1
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
- package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
- package/clinkx-workflows/dist/dashboard/generation.js +41 -0
- package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/index.js +16 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
- package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
- package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
- package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
- package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
- package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
- package/clinkx-workflows/dist/delegate-validate.js +97 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
- package/clinkx-workflows/dist/engine.d.ts +141 -0
- package/clinkx-workflows/dist/engine.js +882 -28
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +2 -0
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
- package/clinkx-workflows/dist/fingerprint.js +1 -1
- package/clinkx-workflows/dist/index.d.ts +19 -0
- package/clinkx-workflows/dist/index.js +15 -0
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/loader.d.ts +9 -3
- package/clinkx-workflows/dist/loader.js +92 -21
- package/clinkx-workflows/dist/loader.js.map +1 -1
- package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
- package/clinkx-workflows/dist/plan-review/admission.js +290 -0
- package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
- package/clinkx-workflows/dist/plan-review/contract.js +289 -0
- package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
- package/clinkx-workflows/dist/plan-review/digest.js +241 -0
- package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
- package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
- package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
- package/clinkx-workflows/dist/plan-review/expander.js +243 -0
- package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
- package/clinkx-workflows/dist/plan-review/projector.js +437 -0
- package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
- package/clinkx-workflows/dist/plan-review/publication.js +182 -0
- package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
- package/clinkx-workflows/dist/plan-review/resume.js +122 -0
- package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
- package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
- package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
- package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
- package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
- package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
- package/clinkx-workflows/dist/post-processors/index.js +21 -0
- package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
- package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
- package/clinkx-workflows/dist/prompt-budget.js +7 -1
- package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.js +2 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
- package/clinkx-workflows/dist/run-summary.d.ts +13 -0
- package/clinkx-workflows/dist/run-summary.js +29 -1
- package/clinkx-workflows/dist/run-summary.js.map +1 -1
- package/clinkx-workflows/dist/schema.d.ts +76 -21
- package/clinkx-workflows/dist/schema.js +10 -1
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
- package/clinkx-workflows/dist/seed-terms.js +176 -0
- package/clinkx-workflows/dist/seed-terms.js.map +1 -0
- package/clinkx-workflows/dist/server.d.ts +5 -2
- package/clinkx-workflows/dist/server.js +82 -52
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +18 -20
- package/clinkx-workflows/dist/shutdown.js +43 -61
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +38 -0
- package/clinkx-workflows/dist/state.d.ts +170 -3
- package/clinkx-workflows/dist/state.js +1275 -28
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/transport.d.ts +15 -0
- package/clinkx-workflows/dist/transport.js +19 -10
- package/clinkx-workflows/dist/transport.js.map +1 -1
- package/clinkx-workflows/dist/types.d.ts +14 -2
- package/clinkx-workflows/dist/validators/council.d.ts +318 -85
- package/clinkx-workflows/dist/validators/council.js +52 -13
- package/clinkx-workflows/dist/validators/council.js.map +1 -1
- package/clinkx-workflows/dist/validators/index.d.ts +1 -0
- package/clinkx-workflows/dist/validators/index.js +3 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
- package/clinkx-workflows/dist/validators/plan-review.js +316 -0
- package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
- package/clinkx-workflows/dist/workflow-receipt.js +188 -4
- package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
- package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
- package/clinkx-workflows/dist/workflow-tools.js +1402 -59
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
- package/clinkx-workflows/dist/workspace-signature.js +388 -0
- package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
- package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
- package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
- package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
- package/clinkx-workflows/templates/council/member.txt +2 -1
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
- package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
- package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
- package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
- package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
- package/clinkx-workflows/workflows/council-answer.yaml +97 -8
- package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
- package/clinkx-workflows/workflows/council-debug.yaml +97 -8
- package/clinkx-workflows/workflows/council-default.yaml +97 -8
- package/clinkx-workflows/workflows/council-discover.yaml +97 -8
- package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
- package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
- package/conf/adapters/agy.json +49 -0
- package/conf/adapters/claude.json +5 -0
- package/conf/adapters/codex-app-server.json +44 -0
- package/conf/adapters/codex.json +7 -2
- package/conf/adapters/glm.json +5 -0
- package/conf/adapters/grok.json +60 -0
- package/conf/adapters/hapi/agy.json +52 -0
- package/conf/adapters/hapi/claude.json +5 -0
- package/conf/adapters/hapi/codex.json +7 -2
- package/conf/adapters/hapi/glm.json +5 -0
- package/dist/activity-journal.d.ts +42 -0
- package/dist/activity-journal.js +381 -0
- package/dist/activity-journal.js.map +1 -0
- package/dist/allowed-roots.d.ts +41 -0
- package/dist/allowed-roots.js +125 -3
- package/dist/allowed-roots.js.map +1 -1
- package/dist/codex/activity-session-log.d.ts +14 -0
- package/dist/codex/activity-session-log.js +206 -0
- package/dist/codex/activity-session-log.js.map +1 -0
- package/dist/codex/agent-tools.d.ts +23 -0
- package/dist/codex/agent-tools.js +1270 -0
- package/dist/codex/agent-tools.js.map +1 -0
- package/dist/codex/app-server-client.d.ts +76 -0
- package/dist/codex/app-server-client.js +582 -0
- package/dist/codex/app-server-client.js.map +1 -0
- package/dist/codex/app-server-registry.d.ts +389 -0
- package/dist/codex/app-server-registry.js +1407 -0
- package/dist/codex/app-server-registry.js.map +1 -0
- package/dist/codex/client-types.d.ts +109 -0
- package/dist/codex/client-types.js +2 -0
- package/dist/codex/client-types.js.map +1 -0
- package/dist/codex/protocol.d.ts +5375 -0
- package/dist/codex/protocol.js +614 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/codex/session.d.ts +263 -0
- package/dist/codex/session.js +466 -0
- package/dist/codex/session.js.map +1 -0
- package/dist/config.d.ts +185 -19
- package/dist/config.js +82 -16
- package/dist/config.js.map +1 -1
- package/dist/context-builder/anchors.d.ts +1 -0
- package/dist/context-builder/anchors.js +3 -0
- package/dist/context-builder/anchors.js.map +1 -1
- package/dist/context-builder/bundle.d.ts +35 -2
- package/dist/context-builder/bundle.js +342 -2417
- package/dist/context-builder/bundle.js.map +1 -1
- package/dist/context-builder/codemap.d.ts +5 -0
- package/dist/context-builder/codemap.js +387 -13
- package/dist/context-builder/codemap.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +12 -0
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/context-run-catalog.d.ts +17 -0
- package/dist/context-builder/context-run-catalog.js +99 -0
- package/dist/context-builder/context-run-catalog.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +37 -0
- package/dist/context-builder/delegate-packet.js +359 -67
- package/dist/context-builder/delegate-packet.js.map +1 -1
- package/dist/context-builder/delegate-validation.d.ts +10 -1
- package/dist/context-builder/delegate-validation.js +153 -41
- package/dist/context-builder/delegate-validation.js.map +1 -1
- package/dist/context-builder/delegate.d.ts +2 -0
- package/dist/context-builder/delegate.js +1 -0
- package/dist/context-builder/delegate.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +11 -2
- package/dist/context-builder/diagnostics.js +39 -6
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery.d.ts +29 -1
- package/dist/context-builder/discovery.js +475 -175
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/globs.js +10 -10
- package/dist/context-builder/globs.js.map +1 -1
- package/dist/context-builder/handler.d.ts +26 -2
- package/dist/context-builder/handler.js +1158 -2416
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/lifecycle-contract.d.ts +424 -0
- package/dist/context-builder/lifecycle-contract.js +523 -0
- package/dist/context-builder/lifecycle-contract.js.map +1 -0
- package/dist/context-builder/lifecycle-journal.d.ts +62 -0
- package/dist/context-builder/lifecycle-journal.js +88 -0
- package/dist/context-builder/lifecycle-journal.js.map +1 -0
- package/dist/context-builder/lifecycle-observer.d.ts +102 -0
- package/dist/context-builder/lifecycle-observer.js +258 -0
- package/dist/context-builder/lifecycle-observer.js.map +1 -0
- package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
- package/dist/context-builder/lifecycle-reconcile.js +191 -0
- package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
- package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
- package/dist/context-builder/lifecycle-recovery.js +14 -0
- package/dist/context-builder/lifecycle-recovery.js.map +1 -0
- package/dist/context-builder/path-signals.js +9 -5
- package/dist/context-builder/path-signals.js.map +1 -1
- package/dist/context-builder/progress.d.ts +3 -1
- package/dist/context-builder/progress.js +23 -0
- package/dist/context-builder/progress.js.map +1 -1
- package/dist/context-builder/receipt.d.ts +6 -4
- package/dist/context-builder/receipt.js +12 -1
- package/dist/context-builder/receipt.js.map +1 -1
- package/dist/context-builder/refine-handler.d.ts +14 -0
- package/dist/context-builder/refine-handler.js +790 -0
- package/dist/context-builder/refine-handler.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +84 -0
- package/dist/context-builder/review-coverage.js +437 -0
- package/dist/context-builder/review-coverage.js.map +1 -0
- package/dist/context-builder/schema.d.ts +239 -1
- package/dist/context-builder/schema.js +112 -6
- package/dist/context-builder/schema.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +28 -0
- package/dist/context-builder/secrets.js +1473 -44
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/selection.d.ts +7 -3
- package/dist/context-builder/selection.js +200 -738
- package/dist/context-builder/selection.js.map +1 -1
- package/dist/context-builder/session-log-identity.d.ts +1 -0
- package/dist/context-builder/session-log-identity.js +66 -0
- package/dist/context-builder/session-log-identity.js.map +1 -0
- package/dist/context-builder/slice-range-math.d.ts +16 -0
- package/dist/context-builder/slice-range-math.js +137 -0
- package/dist/context-builder/slice-range-math.js.map +1 -0
- package/dist/context-builder/status.d.ts +22 -1
- package/dist/context-builder/status.js +59 -4
- package/dist/context-builder/status.js.map +1 -1
- package/dist/context-builder/storage.d.ts +35 -2
- package/dist/context-builder/storage.js +271 -14
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/token-estimate.d.ts +13 -2
- package/dist/context-builder/token-estimate.js +77 -4
- package/dist/context-builder/token-estimate.js.map +1 -1
- package/dist/context-builder/versions.d.ts +29 -0
- package/dist/context-builder/versions.js +30 -0
- package/dist/context-builder/versions.js.map +1 -0
- package/dist/context-builder/workflow-runner.d.ts +69 -0
- package/dist/context-builder/workflow-runner.js +1051 -0
- package/dist/context-builder/workflow-runner.js.map +1 -0
- package/dist/context-builder/write-watchdog.d.ts +14 -0
- package/dist/context-builder/write-watchdog.js +38 -0
- package/dist/context-builder/write-watchdog.js.map +1 -0
- package/dist/dashboard-adapter-catalog.d.ts +9 -0
- package/dist/dashboard-adapter-catalog.js +46 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -0
- package/dist/dashboard-cli.d.ts +79 -0
- package/dist/dashboard-cli.js +585 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-companion/client.d.ts +36 -0
- package/dist/dashboard-companion/client.js +210 -0
- package/dist/dashboard-companion/client.js.map +1 -0
- package/dist/dashboard-companion/config.d.ts +8 -0
- package/dist/dashboard-companion/config.js +52 -0
- package/dist/dashboard-companion/config.js.map +1 -0
- package/dist/dashboard-companion/http-proxy.d.ts +10 -0
- package/dist/dashboard-companion/http-proxy.js +234 -0
- package/dist/dashboard-companion/http-proxy.js.map +1 -0
- package/dist/dashboard-companion/protocol.d.ts +80 -0
- package/dist/dashboard-companion/protocol.js +455 -0
- package/dist/dashboard-companion/protocol.js.map +1 -0
- package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
- package/dist/dashboard-companion/standalone-assets.js +72 -0
- package/dist/dashboard-companion/standalone-assets.js.map +1 -0
- package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
- package/dist/dashboard-companion/standalone-entry.js +17 -0
- package/dist/dashboard-companion/standalone-entry.js.map +1 -0
- package/dist/dashboard-gateway/config.d.ts +9 -0
- package/dist/dashboard-gateway/config.js +44 -0
- package/dist/dashboard-gateway/config.js.map +1 -0
- package/dist/dashboard-gateway/index.d.ts +11 -0
- package/dist/dashboard-gateway/index.js +755 -0
- package/dist/dashboard-gateway/index.js.map +1 -0
- package/dist/dashboard-gateway/main.d.ts +1 -0
- package/dist/dashboard-gateway/main.js +25 -0
- package/dist/dashboard-gateway/main.js.map +1 -0
- package/dist/dashboard-pairing-token.d.ts +1 -0
- package/dist/dashboard-pairing-token.js +5 -0
- package/dist/dashboard-pairing-token.js.map +1 -0
- package/dist/elaris/client.d.ts +73 -0
- package/dist/elaris/client.js +825 -0
- package/dist/elaris/client.js.map +1 -0
- package/dist/elaris/config.d.ts +18 -0
- package/dist/elaris/config.js +75 -0
- package/dist/elaris/config.js.map +1 -0
- package/dist/elaris/errors.d.ts +41 -0
- package/dist/elaris/errors.js +205 -0
- package/dist/elaris/errors.js.map +1 -0
- package/dist/elaris/github-repo.d.ts +18 -0
- package/dist/elaris/github-repo.js +89 -0
- package/dist/elaris/github-repo.js.map +1 -0
- package/dist/elaris/index.d.ts +10 -0
- package/dist/elaris/index.js +11 -0
- package/dist/elaris/index.js.map +1 -0
- package/dist/elaris/redact.d.ts +89 -0
- package/dist/elaris/redact.js +423 -0
- package/dist/elaris/redact.js.map +1 -0
- package/dist/elaris/run-id.d.ts +5 -0
- package/dist/elaris/run-id.js +20 -0
- package/dist/elaris/run-id.js.map +1 -0
- package/dist/elaris/status.d.ts +45 -0
- package/dist/elaris/status.js +122 -0
- package/dist/elaris/status.js.map +1 -0
- package/dist/elaris/tool-schemas.d.ts +314 -0
- package/dist/elaris/tool-schemas.js +208 -0
- package/dist/elaris/tool-schemas.js.map +1 -0
- package/dist/elaris/tools.d.ts +73 -0
- package/dist/elaris/tools.js +1065 -0
- package/dist/elaris/tools.js.map +1 -0
- package/dist/elaris/types.d.ts +1541 -0
- package/dist/elaris/types.js +252 -0
- package/dist/elaris/types.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -1
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +10 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/local-clink-backend.d.ts +3 -0
- package/dist/local-clink-backend.js +4 -0
- package/dist/local-clink-backend.js.map +1 -1
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +18 -0
- package/dist/package-info.js.map +1 -0
- package/dist/parsers/codex-jsonl.js +48 -3
- package/dist/parsers/codex-jsonl.js.map +1 -1
- package/dist/parsers/extract.d.ts +10 -1
- package/dist/parsers/extract.js +11 -3
- package/dist/parsers/extract.js.map +1 -1
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.js +129 -8
- package/dist/pipeline.js.map +1 -1
- package/dist/run-dir.d.ts +24 -0
- package/dist/run-dir.js +201 -2
- package/dist/run-dir.js.map +1 -1
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +184 -15
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +4 -4
- package/dist/safety.js +4 -4
- package/dist/schema.d.ts +7 -7
- package/dist/server.d.ts +1 -0
- package/dist/server.js +359 -17
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +18 -0
- package/dist/session-log-stream.js +319 -0
- package/dist/session-log-stream.js.map +1 -0
- package/package.json +46 -5
- package/conf/adapters/gemini.json +0 -44
- package/conf/adapters/hapi/gemini.json +0 -47
- package/dist/parsers/gemini-json.d.ts +0 -2
- package/dist/parsers/gemini-json.js +0 -72
- package/dist/parsers/gemini-json.js.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Containment-safe, no-follow file reads.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the private sidecar reader in `workflow-tools.ts` so the
|
|
5
|
+
* plan-review snapshot/contract paths reuse ONE audited implementation instead
|
|
6
|
+
* of hand-rolling a second realpath+O_NOFOLLOW dance.
|
|
7
|
+
*
|
|
8
|
+
* The invariant every caller depends on: a single file handle backs both the
|
|
9
|
+
* stat and the read, so there is no check/use window, and `O_NOFOLLOW` rejects
|
|
10
|
+
* a final-component symlink swapped in after the containment check.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Structured discriminator for the subset of policy violations that callers must
|
|
14
|
+
* be able to branch on WITHOUT matching message wording.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a taxonomy over every throw site: only the allowed-roots
|
|
17
|
+
* containment failure carries a reason today, because it is the only one a
|
|
18
|
+
* caller (plan-review's snapshot root hint) must distinguish. The other throw
|
|
19
|
+
* sites leave `reason` undefined rather than grow a speculative enum.
|
|
20
|
+
*/
|
|
21
|
+
export type ContainedFilePolicyReason = "outside_allowed_roots";
|
|
22
|
+
export interface ContainedFileFingerprint {
|
|
23
|
+
readonly dev: bigint;
|
|
24
|
+
readonly ino: bigint;
|
|
25
|
+
readonly mtimeNs: bigint;
|
|
26
|
+
readonly ctimeNs: bigint;
|
|
27
|
+
readonly size: bigint;
|
|
28
|
+
readonly nlink: bigint;
|
|
29
|
+
}
|
|
30
|
+
export interface ContainedReadBeforeCloseContext {
|
|
31
|
+
readonly fingerprint: ContainedFileFingerprint;
|
|
32
|
+
/** Resolve the authoritative path for the still-open descriptor. */
|
|
33
|
+
readonly resolveRealPath: () => Promise<string | undefined>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `expectedPath` is OPTIONAL so an injected one-argument verifier (every test
|
|
37
|
+
* seam in this repo) stays assignable. Callers that know the pathname they
|
|
38
|
+
* opened should always pass it: it selects the cheap darwin verification path
|
|
39
|
+
* described on {@link resolveOpenedDescriptorPath}.
|
|
40
|
+
*/
|
|
41
|
+
export type OpenedDescriptorPathVerifier = (fd: number, expectedPath?: string) => Promise<string | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* A containment/policy violation (absolute where relative is required, escape
|
|
44
|
+
* outside the root, symlink, non-regular file). Distinct from a plain Error so
|
|
45
|
+
* callers can tell "you may not read this" from "it is not there right now".
|
|
46
|
+
*/
|
|
47
|
+
export declare class ContainedFilePolicyError extends Error {
|
|
48
|
+
readonly reason: ContainedFilePolicyReason | undefined;
|
|
49
|
+
constructor(message: string, reason?: ContainedFilePolicyReason);
|
|
50
|
+
}
|
|
51
|
+
export declare class ContainedFileChangedError extends ContainedFilePolicyError {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
export declare class ContainedFileTooLargeError extends ContainedFilePolicyError {
|
|
55
|
+
readonly size: number | undefined;
|
|
56
|
+
readonly fingerprint: ContainedFileFingerprint | undefined;
|
|
57
|
+
constructor(message: string, size?: number, fingerprint?: ContainedFileFingerprint);
|
|
58
|
+
}
|
|
59
|
+
export interface ContainedReadOptions {
|
|
60
|
+
/** Return the file body decoded as UTF-8. */
|
|
61
|
+
readonly wantContent?: boolean | undefined;
|
|
62
|
+
/** Return the raw file body. Mutually usable with `wantContent`. */
|
|
63
|
+
readonly wantBytes?: boolean | undefined;
|
|
64
|
+
/** Reject anything that is not a regular file (fstat on the open handle). */
|
|
65
|
+
readonly requireRegular?: boolean | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Wording for policy-violation messages, e.g. `"run directory"`. Callers with
|
|
68
|
+
* an established message contract pass their own noun so extracting this
|
|
69
|
+
* module did not silently reword their errors.
|
|
70
|
+
*/
|
|
71
|
+
readonly containerLabel?: string | undefined;
|
|
72
|
+
/** Wording for the path itself, e.g. `"outputPath"`. */
|
|
73
|
+
readonly pathLabel?: string | undefined;
|
|
74
|
+
/** Reject larger files before allocating their body. */
|
|
75
|
+
readonly maxBytes?: number | undefined;
|
|
76
|
+
/** Deterministic race-injection seam used by containment regression tests. */
|
|
77
|
+
readonly beforeOpen?: (() => void) | undefined;
|
|
78
|
+
/** Deterministic seam after open but before descriptor-path resolution. */
|
|
79
|
+
readonly afterOpen?: (() => void) | undefined;
|
|
80
|
+
/** Verify caller-owned invariants while the admitted descriptor is still open. */
|
|
81
|
+
readonly beforeClose?: (context: ContainedReadBeforeCloseContext) => void | Promise<void>;
|
|
82
|
+
/** Injectable platform adapter for authoritative descriptor-path resolution. */
|
|
83
|
+
readonly descriptorPathVerifier?: OpenedDescriptorPathVerifier | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Require the opened descriptor to remain at the lexical canonical path
|
|
86
|
+
* requested under `rootDir`, rather than merely somewhere inside that root.
|
|
87
|
+
* Dashboard state/body reads opt in; shared callers retain containment-only
|
|
88
|
+
* behavior.
|
|
89
|
+
*/
|
|
90
|
+
readonly requireExactRequestedPath?: boolean | undefined;
|
|
91
|
+
}
|
|
92
|
+
export interface ContainedReadResult {
|
|
93
|
+
readonly size: number;
|
|
94
|
+
readonly content?: string | undefined;
|
|
95
|
+
readonly bytes?: Buffer | undefined;
|
|
96
|
+
/** The canonical path actually opened. */
|
|
97
|
+
readonly realPath: string;
|
|
98
|
+
/** BigInt identity/content tuple captured from the same handle as the body. */
|
|
99
|
+
readonly fingerprint: ContainedFileFingerprint;
|
|
100
|
+
}
|
|
101
|
+
export declare function containedFileFingerprintsEqual(left: ContainedFileFingerprint, right: ContainedFileFingerprint): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a verified path for an already-open descriptor. Authoritative on
|
|
104
|
+
* Linux; on darwin the check is probabilistic — see below. Unsupported
|
|
105
|
+
* platforms and helper failures fail closed (`undefined`).
|
|
106
|
+
*
|
|
107
|
+
* WITHOUT an `expectedPath`, Linux reads `/proc/self/fd/<fd>`, which the
|
|
108
|
+
* kernel answers from the descriptor itself. Darwin has no equivalent and
|
|
109
|
+
* falls through to the historical `lsof` helper.
|
|
110
|
+
*
|
|
111
|
+
* WITH an `expectedPath` (linux and darwin) this canonicalizes
|
|
112
|
+
* `P = realpath(expectedPath)`, then `lstat`s `P` and compares `(dev, ino)`
|
|
113
|
+
* against an `fstat` of the descriptor. That pair is NOT atomic: it is two
|
|
114
|
+
* independent walks of a mutable pathname, microseconds apart, and `lstat` is
|
|
115
|
+
* no-follow only on the FINAL component — every intermediate component is
|
|
116
|
+
* resolved again. An ancestor swapped between the two walks can steer the
|
|
117
|
+
* `lstat` onto the same out-of-root inode the descriptor holds, so the
|
|
118
|
+
* comparison passes while the returned string still reads as in-root; the
|
|
119
|
+
* caller's containment re-assert then succeeds on a path that no longer
|
|
120
|
+
* describes the opened bytes. `P` is returned for that re-assert, which is
|
|
121
|
+
* deliberately NOT done here (this helper holds no roots).
|
|
122
|
+
*
|
|
123
|
+
* Ancestor ABA restoration defeats the mitigation outright: restore the
|
|
124
|
+
* original ancestor before the caller's post-read re-verify and both the verify
|
|
125
|
+
* and the re-verify agree — the re-verify's string comparison against the first
|
|
126
|
+
* result then matches by construction.
|
|
127
|
+
*
|
|
128
|
+
* So the darwin guarantee RAISES THE COST of a race rather than eliminating it:
|
|
129
|
+
* three adjacent synchronous syscalls with no `await` between them, and callers
|
|
130
|
+
* verify twice, so the window must be won twice. It is meaningful only under a
|
|
131
|
+
* trusted-local-filesystem threat model — NOT against an adversary with
|
|
132
|
+
* concurrent write access to any ancestor directory.
|
|
133
|
+
*
|
|
134
|
+
* No stronger in-process guarantee exists to reach for: Node exposes no
|
|
135
|
+
* `openat`, no `AT_*` constants, and no dirfd-accepting API, so a
|
|
136
|
+
* descriptor-relative no-follow walk cannot be expressed at all, and darwin's
|
|
137
|
+
* `/dev/fd/<fd>` is not traversable. The residual is accepted deliberately, not
|
|
138
|
+
* overlooked; closing it would require a native addon.
|
|
139
|
+
*
|
|
140
|
+
* `lstatSync`, not `statSync`, on purpose: `realpathSync.native` has already
|
|
141
|
+
* resolved every symlink in `P`, so the final component can only be a symlink
|
|
142
|
+
* if one was swapped in AFTER that resolve. `lstat` fails closed on that swap;
|
|
143
|
+
* `stat` would follow it. Same syscall cost, strictly stronger.
|
|
144
|
+
*
|
|
145
|
+
* What it does NOT prove: that the descriptor has no OTHER name. An out-of-root
|
|
146
|
+
* hardlink alias to the same inode is not detected. `fcntl(F_GETPATH)` would not
|
|
147
|
+
* fix that either — it reads the same darwin vnode name cache, which holds one
|
|
148
|
+
* name per inode and is rewritten by the most recent lookup.
|
|
149
|
+
*
|
|
150
|
+
* WITHOUT an `expectedPath` the historical `lsof` implementation is retained
|
|
151
|
+
* verbatim, so an unmigrated caller keeps today's exact behavior instead of
|
|
152
|
+
* silently downgrading to a weaker check it never opted into.
|
|
153
|
+
*/
|
|
154
|
+
export declare function resolveOpenedDescriptorPath(fd: number, expectedPath?: string): Promise<string | undefined>;
|
|
155
|
+
/**
|
|
156
|
+
* Read a run-relative sidecar. `relPath` MUST be relative to `rootDir`.
|
|
157
|
+
*
|
|
158
|
+
* Realpaths BOTH sides: on macOS `/tmp` (and mkdtemp dirs) is itself a symlink
|
|
159
|
+
* to `/private/tmp`, so realpathing only the target would false-reject a
|
|
160
|
+
* legitimate read.
|
|
161
|
+
*/
|
|
162
|
+
export declare function readContainedRelativeFile(rootDir: string, relPath: string, options?: ContainedReadOptions): Promise<ContainedReadResult>;
|
|
163
|
+
/**
|
|
164
|
+
* Read an absolute path that must live inside at least one allowed root.
|
|
165
|
+
*
|
|
166
|
+
* Unlike {@link readContainedRelativeFile} this deliberately does NOT realpath
|
|
167
|
+
* the target before opening: resolving the final component would silently
|
|
168
|
+
* follow a symlink and defeat the "reject symlink" requirement. Only the
|
|
169
|
+
* PARENT is canonicalized for the containment check; `O_NOFOLLOW` then rejects
|
|
170
|
+
* a symlinked final component at open time.
|
|
171
|
+
*/
|
|
172
|
+
export declare function readContainedAbsoluteFile(allowedRoots: readonly string[], targetPath: string, options?: ContainedReadOptions): Promise<ContainedReadResult>;
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Containment-safe, no-follow file reads.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the private sidecar reader in `workflow-tools.ts` so the
|
|
5
|
+
* plan-review snapshot/contract paths reuse ONE audited implementation instead
|
|
6
|
+
* of hand-rolling a second realpath+O_NOFOLLOW dance.
|
|
7
|
+
*
|
|
8
|
+
* The invariant every caller depends on: a single file handle backs both the
|
|
9
|
+
* stat and the read, so there is no check/use window, and `O_NOFOLLOW` rejects
|
|
10
|
+
* a final-component symlink swapped in after the containment check.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { constants as fsConstants, fstatSync, lstatSync, realpathSync, } from "node:fs";
|
|
14
|
+
import { open } from "node:fs/promises";
|
|
15
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
16
|
+
/**
|
|
17
|
+
* A containment/policy violation (absolute where relative is required, escape
|
|
18
|
+
* outside the root, symlink, non-regular file). Distinct from a plain Error so
|
|
19
|
+
* callers can tell "you may not read this" from "it is not there right now".
|
|
20
|
+
*/
|
|
21
|
+
export class ContainedFilePolicyError extends Error {
|
|
22
|
+
// Always assigned (never an optional property): this package enables
|
|
23
|
+
// `exactOptionalPropertyTypes`, under which an `reason?:` declaration would
|
|
24
|
+
// reject an explicit `undefined` assignment.
|
|
25
|
+
reason;
|
|
26
|
+
constructor(message, reason) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ContainedFilePolicyError";
|
|
29
|
+
this.reason = reason;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class ContainedFileChangedError extends ContainedFilePolicyError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ContainedFileChangedError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class ContainedFileTooLargeError extends ContainedFilePolicyError {
|
|
39
|
+
size;
|
|
40
|
+
fingerprint;
|
|
41
|
+
constructor(message, size, fingerprint) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ContainedFileTooLargeError";
|
|
44
|
+
this.size = size;
|
|
45
|
+
this.fingerprint = fingerprint;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function fingerprintFromStats(stats) {
|
|
49
|
+
return {
|
|
50
|
+
dev: stats.dev,
|
|
51
|
+
ino: stats.ino,
|
|
52
|
+
mtimeNs: stats.mtimeNs,
|
|
53
|
+
ctimeNs: stats.ctimeNs,
|
|
54
|
+
size: stats.size,
|
|
55
|
+
nlink: stats.nlink,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function containedFileFingerprintsEqual(left, right) {
|
|
59
|
+
return left.dev === right.dev &&
|
|
60
|
+
left.ino === right.ino &&
|
|
61
|
+
left.mtimeNs === right.mtimeNs &&
|
|
62
|
+
left.ctimeNs === right.ctimeNs &&
|
|
63
|
+
left.size === right.size &&
|
|
64
|
+
left.nlink === right.nlink;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Open `realTarget` once with no-follow semantics and serve every requested
|
|
68
|
+
* projection (size / bytes / text) from that SAME handle.
|
|
69
|
+
*/
|
|
70
|
+
async function readOpenedContained(realTarget, realRoots, options, exactRequestedPath) {
|
|
71
|
+
// O_NOFOLLOW rejects a final-component symlink swapped in between the
|
|
72
|
+
// containment check and this open — closing the check/use TOCTOU window.
|
|
73
|
+
// When the caller requires a regular file, also admit the descriptor
|
|
74
|
+
// non-blockingly: a FIFO (or other special file) opened O_RDONLY alone blocks
|
|
75
|
+
// until a writer connects, which would hang the reader indefinitely BEFORE the
|
|
76
|
+
// requireRegular fstat can reject it. O_NONBLOCK makes that open return at
|
|
77
|
+
// once so the non-regular file is rejected promptly. It is a no-op for regular
|
|
78
|
+
// files (their reads never return EAGAIN), so gating it on requireRegular
|
|
79
|
+
// preserves byte-identical behavior for every shared caller that does not opt
|
|
80
|
+
// into the regular-file requirement.
|
|
81
|
+
const requireRegular = options.requireRegular === true;
|
|
82
|
+
const flags = fsConstants.O_RDONLY |
|
|
83
|
+
(fsConstants.O_NOFOLLOW ?? 0) |
|
|
84
|
+
(requireRegular ? (fsConstants.O_NONBLOCK ?? 0) : 0);
|
|
85
|
+
const verifyDescriptorPath = options.descriptorPathVerifier ??
|
|
86
|
+
resolveOpenedDescriptorPath;
|
|
87
|
+
options.beforeOpen?.();
|
|
88
|
+
const fh = await open(realTarget, flags);
|
|
89
|
+
try {
|
|
90
|
+
const stats = await fh.stat({ bigint: true });
|
|
91
|
+
const fingerprint = fingerprintFromStats(stats);
|
|
92
|
+
// Reject a non-regular file (FIFO, device, ...) right after the same-handle
|
|
93
|
+
// fstat, BEFORE descriptor-path resolution and reads. For a FIFO this is the
|
|
94
|
+
// case O_NONBLOCK admission exists to make reachable: without it the open
|
|
95
|
+
// above would still be blocked here. Moving the check ahead of the expensive
|
|
96
|
+
// descriptor-path lookup also avoids spawning lsof for a file that will be
|
|
97
|
+
// rejected anyway.
|
|
98
|
+
if (requireRegular && !stats.isFile()) {
|
|
99
|
+
throw new ContainedFilePolicyError(`Path is not a regular file: ${realTarget}`);
|
|
100
|
+
}
|
|
101
|
+
options.afterOpen?.();
|
|
102
|
+
const openedRealPath = await verifyDescriptorPath(fh.fd, realTarget);
|
|
103
|
+
if (openedRealPath === undefined) {
|
|
104
|
+
throw new ContainedFilePolicyError(`Opened file descriptor containment could not be verified: ${realTarget}`);
|
|
105
|
+
}
|
|
106
|
+
if (!realRoots.some((root) => containsPath(root, openedRealPath))) {
|
|
107
|
+
throw new ContainedFilePolicyError(`Opened file escaped its containing root: ${realTarget}`);
|
|
108
|
+
}
|
|
109
|
+
if (exactRequestedPath !== undefined &&
|
|
110
|
+
openedRealPath !== exactRequestedPath) {
|
|
111
|
+
throw new ContainedFilePolicyError(`Opened file did not match its requested path: ${realTarget}`);
|
|
112
|
+
}
|
|
113
|
+
const exceedsMaxBytes = options.maxBytes !== undefined &&
|
|
114
|
+
Number(stats.size) > options.maxBytes;
|
|
115
|
+
// Read into a buffer sized from the checked handle stat. Unlike readFile(),
|
|
116
|
+
// this cannot allocate past the admitted size if the file grows while the
|
|
117
|
+
// read is in progress. A one-byte probe detects growth without widening the
|
|
118
|
+
// allocation bound.
|
|
119
|
+
let bytes;
|
|
120
|
+
let extraByteRead = false;
|
|
121
|
+
if (!exceedsMaxBytes && (options.wantBytes === true || options.wantContent === true)) {
|
|
122
|
+
bytes = Buffer.allocUnsafe(Number(stats.size));
|
|
123
|
+
let offset = 0;
|
|
124
|
+
while (offset < bytes.length) {
|
|
125
|
+
const chunk = await fh.read(bytes, offset, bytes.length - offset, null);
|
|
126
|
+
if (chunk.bytesRead === 0)
|
|
127
|
+
break;
|
|
128
|
+
offset += chunk.bytesRead;
|
|
129
|
+
}
|
|
130
|
+
const probe = Buffer.allocUnsafe(1);
|
|
131
|
+
extraByteRead = (await fh.read(probe, 0, 1, null)).bytesRead > 0;
|
|
132
|
+
if (BigInt(offset) !== stats.size) {
|
|
133
|
+
bytes = bytes.subarray(0, offset);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const afterStats = await fh.stat({ bigint: true });
|
|
137
|
+
if (bytes !== undefined &&
|
|
138
|
+
(afterStats.size !== stats.size ||
|
|
139
|
+
BigInt(bytes.byteLength) !== stats.size ||
|
|
140
|
+
extraByteRead)) {
|
|
141
|
+
throw new ContainedFileChangedError(`File changed while it was being read: ${realTarget}`);
|
|
142
|
+
}
|
|
143
|
+
const afterFingerprint = fingerprintFromStats(afterStats);
|
|
144
|
+
if (!containedFileFingerprintsEqual(fingerprint, afterFingerprint) ||
|
|
145
|
+
afterFingerprint.nlink === 0n) {
|
|
146
|
+
throw new ContainedFileChangedError(`File changed while it was being read: ${realTarget}`);
|
|
147
|
+
}
|
|
148
|
+
// `openedRealPath` was resolved from this exact live descriptor after open.
|
|
149
|
+
// The caller's before-close hook may need that authoritative admission path,
|
|
150
|
+
// but resolving it a second time is redundant: the unconditional final
|
|
151
|
+
// descriptor lookup below still detects any rename/unlink that occurs before
|
|
152
|
+
// commit. On Darwin this avoids one `lsof` subprocess per admitted file
|
|
153
|
+
// without weakening the final post-hook path check.
|
|
154
|
+
await options.beforeClose?.({
|
|
155
|
+
fingerprint: afterFingerprint,
|
|
156
|
+
resolveRealPath: async () => openedRealPath,
|
|
157
|
+
});
|
|
158
|
+
const finalStats = await fh.stat({ bigint: true });
|
|
159
|
+
const finalFingerprint = fingerprintFromStats(finalStats);
|
|
160
|
+
const finalRealPath = await verifyDescriptorPath(fh.fd, realTarget);
|
|
161
|
+
if (!containedFileFingerprintsEqual(fingerprint, finalFingerprint) ||
|
|
162
|
+
finalFingerprint.nlink === 0n ||
|
|
163
|
+
finalRealPath === undefined ||
|
|
164
|
+
finalRealPath !== openedRealPath ||
|
|
165
|
+
(exactRequestedPath !== undefined &&
|
|
166
|
+
finalRealPath !== exactRequestedPath) ||
|
|
167
|
+
!realRoots.some((root) => containsPath(root, finalRealPath))) {
|
|
168
|
+
throw new ContainedFileChangedError(`File changed while it was being read: ${realTarget}`);
|
|
169
|
+
}
|
|
170
|
+
if (exceedsMaxBytes) {
|
|
171
|
+
throw new ContainedFileTooLargeError(`File exceeds the ${String(options.maxBytes)} byte limit: ${realTarget}`, Number(finalStats.size), finalFingerprint);
|
|
172
|
+
}
|
|
173
|
+
const result = {
|
|
174
|
+
size: Number(finalStats.size),
|
|
175
|
+
realPath: finalRealPath,
|
|
176
|
+
...(options.wantContent === true
|
|
177
|
+
? { content: bytes.toString("utf-8") }
|
|
178
|
+
: {}),
|
|
179
|
+
...(options.wantBytes === true ? { bytes } : {}),
|
|
180
|
+
};
|
|
181
|
+
// Preserve the historical whole-result JSON shape. BigInt cannot be encoded
|
|
182
|
+
// by JSON.stringify, while callers that need cache identity access the
|
|
183
|
+
// fingerprint explicitly through the typed interface.
|
|
184
|
+
Object.defineProperty(result, "fingerprint", {
|
|
185
|
+
configurable: false,
|
|
186
|
+
enumerable: false,
|
|
187
|
+
value: finalFingerprint,
|
|
188
|
+
writable: false,
|
|
189
|
+
});
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
await fh.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Proper-descendant containment: `realTarget` must sit strictly INSIDE
|
|
198
|
+
* `realRoot`, never be the root itself.
|
|
199
|
+
*
|
|
200
|
+
* Segment-aware on purpose. A bare `rel.startsWith("..")` test conflates the
|
|
201
|
+
* escape segment `..` with any legal in-root component whose NAME merely begins
|
|
202
|
+
* with two dots — `<root>/..plans` is an ordinary child directory, not an
|
|
203
|
+
* escape. Only `..` alone, or `..` followed by a separator, is an ascent; both
|
|
204
|
+
* `relative()` outputs are matched exactly here.
|
|
205
|
+
*/
|
|
206
|
+
function containsPathStrictly(realRoot, realTarget) {
|
|
207
|
+
const rel = relative(realRoot, realTarget);
|
|
208
|
+
return (rel !== "" &&
|
|
209
|
+
rel !== ".." &&
|
|
210
|
+
!rel.startsWith(`..${sep}`) &&
|
|
211
|
+
!isAbsolute(rel));
|
|
212
|
+
}
|
|
213
|
+
/** Containment that additionally accepts the root directory itself. */
|
|
214
|
+
function containsPath(realRoot, realTarget) {
|
|
215
|
+
return relative(realRoot, realTarget) === "" ||
|
|
216
|
+
containsPathStrictly(realRoot, realTarget);
|
|
217
|
+
}
|
|
218
|
+
const DESCRIPTOR_PATH_TIMEOUT_MS = 2_000;
|
|
219
|
+
const DESCRIPTOR_PATH_OUTPUT_LIMIT = 8_192;
|
|
220
|
+
/**
|
|
221
|
+
* Resolve a verified path for an already-open descriptor. Authoritative on
|
|
222
|
+
* Linux; on darwin the check is probabilistic — see below. Unsupported
|
|
223
|
+
* platforms and helper failures fail closed (`undefined`).
|
|
224
|
+
*
|
|
225
|
+
* WITHOUT an `expectedPath`, Linux reads `/proc/self/fd/<fd>`, which the
|
|
226
|
+
* kernel answers from the descriptor itself. Darwin has no equivalent and
|
|
227
|
+
* falls through to the historical `lsof` helper.
|
|
228
|
+
*
|
|
229
|
+
* WITH an `expectedPath` (linux and darwin) this canonicalizes
|
|
230
|
+
* `P = realpath(expectedPath)`, then `lstat`s `P` and compares `(dev, ino)`
|
|
231
|
+
* against an `fstat` of the descriptor. That pair is NOT atomic: it is two
|
|
232
|
+
* independent walks of a mutable pathname, microseconds apart, and `lstat` is
|
|
233
|
+
* no-follow only on the FINAL component — every intermediate component is
|
|
234
|
+
* resolved again. An ancestor swapped between the two walks can steer the
|
|
235
|
+
* `lstat` onto the same out-of-root inode the descriptor holds, so the
|
|
236
|
+
* comparison passes while the returned string still reads as in-root; the
|
|
237
|
+
* caller's containment re-assert then succeeds on a path that no longer
|
|
238
|
+
* describes the opened bytes. `P` is returned for that re-assert, which is
|
|
239
|
+
* deliberately NOT done here (this helper holds no roots).
|
|
240
|
+
*
|
|
241
|
+
* Ancestor ABA restoration defeats the mitigation outright: restore the
|
|
242
|
+
* original ancestor before the caller's post-read re-verify and both the verify
|
|
243
|
+
* and the re-verify agree — the re-verify's string comparison against the first
|
|
244
|
+
* result then matches by construction.
|
|
245
|
+
*
|
|
246
|
+
* So the darwin guarantee RAISES THE COST of a race rather than eliminating it:
|
|
247
|
+
* three adjacent synchronous syscalls with no `await` between them, and callers
|
|
248
|
+
* verify twice, so the window must be won twice. It is meaningful only under a
|
|
249
|
+
* trusted-local-filesystem threat model — NOT against an adversary with
|
|
250
|
+
* concurrent write access to any ancestor directory.
|
|
251
|
+
*
|
|
252
|
+
* No stronger in-process guarantee exists to reach for: Node exposes no
|
|
253
|
+
* `openat`, no `AT_*` constants, and no dirfd-accepting API, so a
|
|
254
|
+
* descriptor-relative no-follow walk cannot be expressed at all, and darwin's
|
|
255
|
+
* `/dev/fd/<fd>` is not traversable. The residual is accepted deliberately, not
|
|
256
|
+
* overlooked; closing it would require a native addon.
|
|
257
|
+
*
|
|
258
|
+
* `lstatSync`, not `statSync`, on purpose: `realpathSync.native` has already
|
|
259
|
+
* resolved every symlink in `P`, so the final component can only be a symlink
|
|
260
|
+
* if one was swapped in AFTER that resolve. `lstat` fails closed on that swap;
|
|
261
|
+
* `stat` would follow it. Same syscall cost, strictly stronger.
|
|
262
|
+
*
|
|
263
|
+
* What it does NOT prove: that the descriptor has no OTHER name. An out-of-root
|
|
264
|
+
* hardlink alias to the same inode is not detected. `fcntl(F_GETPATH)` would not
|
|
265
|
+
* fix that either — it reads the same darwin vnode name cache, which holds one
|
|
266
|
+
* name per inode and is rewritten by the most recent lookup.
|
|
267
|
+
*
|
|
268
|
+
* WITHOUT an `expectedPath` the historical `lsof` implementation is retained
|
|
269
|
+
* verbatim, so an unmigrated caller keeps today's exact behavior instead of
|
|
270
|
+
* silently downgrading to a weaker check it never opted into.
|
|
271
|
+
*/
|
|
272
|
+
export async function resolveOpenedDescriptorPath(fd, expectedPath) {
|
|
273
|
+
if (expectedPath !== undefined) {
|
|
274
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const canonical = realpathSync.native(expectedPath);
|
|
279
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
280
|
+
const named = lstatSync(canonical, { bigint: true });
|
|
281
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino)
|
|
282
|
+
return undefined;
|
|
283
|
+
return canonical;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (process.platform === "linux") {
|
|
290
|
+
try {
|
|
291
|
+
const path = realpathSync(`/proc/self/fd/${String(fd)}`);
|
|
292
|
+
return path.includes(" (deleted)") ? undefined : path;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (process.platform !== "darwin")
|
|
299
|
+
return undefined;
|
|
300
|
+
return await new Promise((resolvePath) => {
|
|
301
|
+
let child;
|
|
302
|
+
try {
|
|
303
|
+
child = spawn("/usr/sbin/lsof", ["-b", "-w", "-a", "-p", String(process.pid), "-d", String(fd), "-F0n"], {
|
|
304
|
+
shell: false,
|
|
305
|
+
detached: true,
|
|
306
|
+
env: {
|
|
307
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
308
|
+
LANG: "C",
|
|
309
|
+
},
|
|
310
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
resolvePath(undefined);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let stdout = "";
|
|
318
|
+
let stderrBytes = 0;
|
|
319
|
+
let outputExceeded = false;
|
|
320
|
+
let forcedFailure = false;
|
|
321
|
+
let settled = false;
|
|
322
|
+
const terminate = () => {
|
|
323
|
+
try {
|
|
324
|
+
if (child.pid != null)
|
|
325
|
+
process.kill(-child.pid, "SIGKILL");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
child.kill("SIGKILL");
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const timer = setTimeout(() => {
|
|
332
|
+
forcedFailure = true;
|
|
333
|
+
terminate();
|
|
334
|
+
}, DESCRIPTOR_PATH_TIMEOUT_MS);
|
|
335
|
+
timer.unref();
|
|
336
|
+
const settle = (path) => {
|
|
337
|
+
if (settled)
|
|
338
|
+
return;
|
|
339
|
+
settled = true;
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
resolvePath(path);
|
|
342
|
+
};
|
|
343
|
+
child.stdout?.on("data", (chunk) => {
|
|
344
|
+
if (outputExceeded)
|
|
345
|
+
return;
|
|
346
|
+
if (Buffer.byteLength(stdout) + chunk.byteLength > DESCRIPTOR_PATH_OUTPUT_LIMIT) {
|
|
347
|
+
outputExceeded = true;
|
|
348
|
+
terminate();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
stdout += chunk.toString("utf-8");
|
|
352
|
+
});
|
|
353
|
+
child.stderr?.on("data", (chunk) => {
|
|
354
|
+
stderrBytes += chunk.byteLength;
|
|
355
|
+
if (stderrBytes > DESCRIPTOR_PATH_OUTPUT_LIMIT) {
|
|
356
|
+
forcedFailure = true;
|
|
357
|
+
terminate();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
child.once("error", () => settle(undefined));
|
|
361
|
+
child.once("close", (status) => {
|
|
362
|
+
if (status !== 0 || outputExceeded || forcedFailure) {
|
|
363
|
+
settle(undefined);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const names = stdout
|
|
367
|
+
.split("\0")
|
|
368
|
+
.map((field) => field.replace(/^\n+/, ""))
|
|
369
|
+
.filter((field) => field.startsWith("n"))
|
|
370
|
+
.map((field) => field.slice(1))
|
|
371
|
+
.filter((path) => path !== "" && !path.includes(" (deleted)"));
|
|
372
|
+
settle(names.length === 1 ? names[0] : undefined);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Read a run-relative sidecar. `relPath` MUST be relative to `rootDir`.
|
|
378
|
+
*
|
|
379
|
+
* Realpaths BOTH sides: on macOS `/tmp` (and mkdtemp dirs) is itself a symlink
|
|
380
|
+
* to `/private/tmp`, so realpathing only the target would false-reject a
|
|
381
|
+
* legitimate read.
|
|
382
|
+
*/
|
|
383
|
+
export async function readContainedRelativeFile(rootDir, relPath, options = {}) {
|
|
384
|
+
const container = options.containerLabel ?? "containing directory";
|
|
385
|
+
const pathLabel = options.pathLabel ?? "path";
|
|
386
|
+
if (isAbsolute(relPath)) {
|
|
387
|
+
throw new ContainedFilePolicyError(`${pathLabel} must be relative to the ${container}`);
|
|
388
|
+
}
|
|
389
|
+
const realRoot = realpathSync(resolve(rootDir));
|
|
390
|
+
const normalized = resolve(realRoot, relPath);
|
|
391
|
+
if (!containsPath(realRoot, normalized)) {
|
|
392
|
+
throw new ContainedFilePolicyError(`${pathLabel} escapes the ${container}`);
|
|
393
|
+
}
|
|
394
|
+
const base = basename(normalized);
|
|
395
|
+
if (base === "") {
|
|
396
|
+
throw new ContainedFilePolicyError(`${pathLabel} has no file component`);
|
|
397
|
+
}
|
|
398
|
+
// Canonicalize only the parent. Realpathing the final component would follow
|
|
399
|
+
// an in-root symlink before O_NOFOLLOW gets a chance to reject it.
|
|
400
|
+
const realParent = realpathSync(resolve(normalized, ".."));
|
|
401
|
+
const realTarget = resolve(realParent, base);
|
|
402
|
+
if (!containsPath(realRoot, realTarget)) {
|
|
403
|
+
throw new ContainedFilePolicyError(`${pathLabel} escapes the ${container}`);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
return await readOpenedContained(realTarget, [realRoot], options, options.requireExactRequestedPath === true ? normalized : undefined);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
if (err?.code === "ELOOP") {
|
|
410
|
+
throw new ContainedFilePolicyError(`${pathLabel} is a symlink and cannot be read`);
|
|
411
|
+
}
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Read an absolute path that must live inside at least one allowed root.
|
|
417
|
+
*
|
|
418
|
+
* Unlike {@link readContainedRelativeFile} this deliberately does NOT realpath
|
|
419
|
+
* the target before opening: resolving the final component would silently
|
|
420
|
+
* follow a symlink and defeat the "reject symlink" requirement. Only the
|
|
421
|
+
* PARENT is canonicalized for the containment check; `O_NOFOLLOW` then rejects
|
|
422
|
+
* a symlinked final component at open time.
|
|
423
|
+
*/
|
|
424
|
+
export async function readContainedAbsoluteFile(allowedRoots, targetPath, options = {}) {
|
|
425
|
+
if (!isAbsolute(targetPath)) {
|
|
426
|
+
throw new ContainedFilePolicyError(`path must be absolute: ${targetPath}`);
|
|
427
|
+
}
|
|
428
|
+
if (allowedRoots.length === 0) {
|
|
429
|
+
throw new ContainedFilePolicyError("no allowed roots are configured for path containment");
|
|
430
|
+
}
|
|
431
|
+
const normalized = resolve(targetPath);
|
|
432
|
+
const parent = resolve(normalized, "..");
|
|
433
|
+
const base = basename(normalized);
|
|
434
|
+
if (base === "") {
|
|
435
|
+
throw new ContainedFilePolicyError(`path has no file component: ${targetPath}`);
|
|
436
|
+
}
|
|
437
|
+
let realParent;
|
|
438
|
+
try {
|
|
439
|
+
realParent = realpathSync(parent);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
throw new ContainedFilePolicyError(`path parent directory does not exist: ${parent}`);
|
|
443
|
+
}
|
|
444
|
+
// basename(), NOT slice arithmetic against `parent.length`: for a file
|
|
445
|
+
// directly under "/" the parent is "/" (length 1) and the offset math would
|
|
446
|
+
// silently eat the filename's first character.
|
|
447
|
+
const realTarget = resolve(realParent, base);
|
|
448
|
+
const realRoots = allowedRoots.map((root) => {
|
|
449
|
+
let realRoot;
|
|
450
|
+
try {
|
|
451
|
+
realRoot = realpathSync(resolve(root));
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
realRoot = resolve(root);
|
|
455
|
+
}
|
|
456
|
+
return realRoot;
|
|
457
|
+
});
|
|
458
|
+
const allowed = realRoots.some((realRoot) => containsPathStrictly(realRoot, realTarget));
|
|
459
|
+
if (!allowed) {
|
|
460
|
+
// `rel === ""` means the path IS an allowed root directory, not an escape —
|
|
461
|
+
// a materially different mistake that the shared "outside all allowed roots"
|
|
462
|
+
// wording actively misdescribes. Safe to re-scan: `some()` short-circuits
|
|
463
|
+
// only on true, so `allowed === false` means every root was visited and none
|
|
464
|
+
// matched. Both sub-cases carry the SAME reason: each is a rejection by the
|
|
465
|
+
// allowed-roots check, which is exactly what the reason discriminates.
|
|
466
|
+
const isExactRoot = realRoots.some((realRoot) => relative(realRoot, realTarget) === "");
|
|
467
|
+
if (isExactRoot) {
|
|
468
|
+
throw new ContainedFilePolicyError(`path "${targetPath}" resolves to an allowed root directory; expected a file within an allowed root`, "outside_allowed_roots");
|
|
469
|
+
}
|
|
470
|
+
throw new ContainedFilePolicyError(`path "${targetPath}" is outside all allowed roots [${allowedRoots.join(", ")}]`, "outside_allowed_roots");
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
return await readOpenedContained(realTarget, realRoots, options);
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
// ELOOP is how O_NOFOLLOW reports "the final component is a symlink".
|
|
477
|
+
if (err?.code === "ELOOP") {
|
|
478
|
+
throw new ContainedFilePolicyError(`path is a symlink and cannot be read: ${targetPath}`);
|
|
479
|
+
}
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
//# sourceMappingURL=contained-file.js.map
|