@wardby/cli 0.6.0 → 0.7.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/deploy/local/docker-compose.quickstart-coding.yml +13 -0
- package/dist/claude-coding-worker/driver.d.ts +21 -3
- package/dist/claude-coding-worker/driver.js +125 -17
- package/dist/claude-coding-worker/main.js +34 -6
- package/dist/claude-coding-worker/sdk.js +12 -1
- package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
- package/dist/claude-coding-worker/tool-socket.js +111 -0
- package/dist/claude-coding-worker/transcript.d.ts +18 -0
- package/dist/claude-coding-worker/transcript.js +77 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +24 -10
- package/dist/coding/continuation-wording.d.ts +35 -0
- package/dist/coding/continuation-wording.js +73 -0
- package/dist/coding/linked-issue-note.d.ts +23 -0
- package/dist/coding/linked-issue-note.js +47 -0
- package/dist/coding/merge-wording.d.ts +28 -0
- package/dist/coding/merge-wording.js +78 -0
- package/dist/coding/pr-description-note.d.ts +9 -0
- package/dist/coding/pr-description-note.js +32 -0
- package/dist/coding/profile.d.ts +46 -126
- package/dist/coding/profile.js +45 -4
- package/dist/coding/protected-path-wording.d.ts +8 -0
- package/dist/coding/protected-path-wording.js +1 -1
- package/dist/coding/protocol.d.ts +264 -346
- package/dist/coding/protocol.js +235 -12
- package/dist/coding/redact.d.ts +3 -0
- package/dist/coding/redact.js +12 -0
- package/dist/coding/registry/allowlist.d.ts +12 -0
- package/dist/coding/registry/allowlist.js +14 -1
- package/dist/coding/registry/audit-mode.d.ts +153 -0
- package/dist/coding/registry/audit-mode.js +304 -0
- package/dist/coding/registry/lockfile-shared.d.ts +36 -0
- package/dist/coding/registry/lockfile-shared.js +128 -0
- package/dist/coding/registry/lockfiles.js +1 -1
- package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
- package/dist/coding/registry/npm-lockfile-formats.js +41 -0
- package/dist/coding/registry/npm-lockfile.d.ts +8 -4
- package/dist/coding/registry/npm-lockfile.js +4 -6
- package/dist/coding/registry/npm.js +78 -8
- package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
- package/dist/coding/registry/pnpm-lockfile.js +189 -0
- package/dist/coding/registry/pypi-plan.d.ts +50 -0
- package/dist/coding/registry/pypi-plan.js +406 -0
- package/dist/coding/registry/pypi-requirements.d.ts +58 -0
- package/dist/coding/registry/pypi-requirements.js +155 -0
- package/dist/coding/registry/pypi.d.ts +3 -27
- package/dist/coding/registry/pypi.js +40 -154
- package/dist/coding/registry/refusal-hints.d.ts +20 -0
- package/dist/coding/registry/refusal-hints.js +61 -0
- package/dist/coding/registry/refusal-wording.d.ts +11 -0
- package/dist/coding/registry/refusal-wording.js +56 -0
- package/dist/coding/registry/report.d.ts +70 -8
- package/dist/coding/registry/report.js +112 -5
- package/dist/coding/registry/types.d.ts +58 -8
- package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
- package/dist/coding/registry/yarn-lockfile.js +367 -0
- package/dist/coding/services/catalog.d.ts +37 -351
- package/dist/coding/transcript/claude-code.d.ts +13 -0
- package/dist/coding/transcript/claude-code.js +273 -0
- package/dist/coding/transcript/download-protocol.d.ts +30 -0
- package/dist/coding/transcript/download-protocol.js +18 -0
- package/dist/coding/transcript/format.d.ts +20 -0
- package/dist/coding/transcript/format.js +32 -0
- package/dist/coding/transcript/recorder.d.ts +51 -0
- package/dist/coding/transcript/recorder.js +225 -0
- package/dist/coding/transcript/resume/checkout.d.ts +31 -0
- package/dist/coding/transcript/resume/checkout.js +203 -0
- package/dist/coding/transcript/resume/command.d.ts +17 -0
- package/dist/coding/transcript/resume/command.js +176 -0
- package/dist/coding/transcript/resume/download.d.ts +20 -0
- package/dist/coding/transcript/resume/download.js +153 -0
- package/dist/coding/transcript/resume/launch.d.ts +38 -0
- package/dist/coding/transcript/resume/launch.js +128 -0
- package/dist/coding/transcript/resume/session-file.d.ts +13 -0
- package/dist/coding/transcript/resume/session-file.js +79 -0
- package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
- package/dist/coding/transcript/resume/stand-in.js +49 -0
- package/dist/coding-worker/artifact.d.ts +8 -3
- package/dist/coding-worker/artifact.js +3 -1
- package/dist/coding-worker/debug-trace.d.ts +6 -5
- package/dist/coding-worker/debug-trace.js +13 -18
- package/dist/coding-worker/dependency-cache.d.mts +229 -0
- package/dist/coding-worker/dependency-cache.mjs +2123 -0
- package/dist/coding-worker/driver.d.ts +4 -3
- package/dist/coding-worker/driver.js +116 -38
- package/dist/coding-worker/errors.d.ts +1 -0
- package/dist/coding-worker/errors.js +32 -0
- package/dist/coding-worker/task-note.d.ts +11 -0
- package/dist/coding-worker/task-note.js +13 -0
- package/dist/coding-worker/types.d.ts +11 -0
- package/dist/config/dependency-cache.d.ts +23 -0
- package/dist/config/dependency-cache.js +32 -0
- package/dist/config/providers.d.ts +12 -3
- package/dist/config/providers.js +55 -5
- package/dist/config/test-simulators.d.ts +82 -0
- package/dist/config/test-simulators.js +305 -0
- package/dist/core/async-context.d.ts +2 -0
- package/dist/core/async-context.js +12 -0
- package/dist/core/budget-events.d.ts +108 -0
- package/dist/core/budget-events.js +346 -0
- package/dist/core/budget-groups.d.ts +50 -1
- package/dist/core/budget-groups.js +236 -27
- package/dist/core/budget-stopped-head.d.ts +46 -0
- package/dist/core/budget-stopped-head.js +81 -0
- package/dist/core/budget-topup.d.ts +51 -0
- package/dist/core/budget-topup.js +156 -0
- package/dist/core/budget-wording.d.ts +16 -2
- package/dist/core/budget-wording.js +40 -2
- package/dist/core/coding-queue.d.ts +2 -1
- package/dist/core/coding-queue.js +7 -0
- package/dist/core/cron.d.ts +19 -1
- package/dist/core/cron.js +17 -0
- package/dist/core/datastores.js +1 -1
- package/dist/core/deferred-coding.d.ts +14 -0
- package/dist/core/deferred-coding.js +62 -0
- package/dist/core/delegation-child.d.ts +25 -0
- package/dist/core/delegation-child.js +37 -0
- package/dist/core/dispatch.d.ts +42 -0
- package/dist/core/dispatch.js +131 -56
- package/dist/core/engine-native.js +1 -1
- package/dist/core/error-code.d.ts +38 -0
- package/dist/core/error-code.js +168 -0
- package/dist/core/grants.d.ts +19 -6
- package/dist/core/grants.js +55 -6
- package/dist/core/host-events.d.ts +18 -2
- package/dist/core/host-events.js +284 -24
- package/dist/core/host-status.d.ts +85 -3
- package/dist/core/host-status.js +298 -20
- package/dist/core/issue-bridge.d.ts +50 -6
- package/dist/core/issue-bridge.js +124 -33
- package/dist/core/issue-events.d.ts +1 -1
- package/dist/core/issue-events.js +4 -10
- package/dist/core/issue-status.d.ts +1 -1
- package/dist/core/issue-status.js +54 -26
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +14 -1
- package/dist/core/lost-run-children.d.ts +6 -0
- package/dist/core/lost-run-children.js +32 -0
- package/dist/core/mark-ready.d.ts +34 -0
- package/dist/core/mark-ready.js +152 -0
- package/dist/core/markdown-code.d.ts +8 -0
- package/dist/core/markdown-code.js +107 -0
- package/dist/core/memory-tools.js +7 -0
- package/dist/core/merge-order-check.d.ts +2 -1
- package/dist/core/merge-order-check.js +8 -1
- package/dist/core/notification-templates.d.ts +1 -7
- package/dist/core/notification-templates.js +36 -2
- package/dist/core/pull-request-state-sync.js +2 -2
- package/dist/core/reconciler.d.ts +4 -3
- package/dist/core/reconciler.js +8 -1
- package/dist/core/related-pull-requests.d.ts +11 -3
- package/dist/core/related-pull-requests.js +27 -14
- package/dist/core/reply-parts.d.ts +21 -0
- package/dist/core/reply-parts.js +73 -0
- package/dist/core/review-fix.d.ts +1 -1
- package/dist/core/review-fix.js +2 -1
- package/dist/core/review-host-checks.d.ts +1 -1
- package/dist/core/review-host-tools.d.ts +3 -0
- package/dist/core/review-host-tools.js +99 -13
- package/dist/core/review-reads.d.ts +55 -0
- package/dist/core/review-reads.js +233 -0
- package/dist/core/run-status.d.ts +12 -0
- package/dist/core/run-status.js +12 -0
- package/dist/core/runner.d.ts +80 -34
- package/dist/core/runner.js +439 -144
- package/dist/core/schedule-start.d.ts +12 -0
- package/dist/core/schedule-start.js +14 -0
- package/dist/core/secret-broker-config.d.ts +10 -74
- package/dist/core/secret-guard.d.ts +46 -0
- package/dist/core/secret-guard.js +104 -0
- package/dist/core/secret-guarded-stores.d.ts +12 -0
- package/dist/core/secret-guarded-stores.js +41 -0
- package/dist/core/secret-spans.d.ts +50 -0
- package/dist/core/secret-spans.js +94 -0
- package/dist/core/stop-run.d.ts +46 -0
- package/dist/core/stop-run.js +61 -0
- package/dist/core/workflow-events.d.ts +14 -2
- package/dist/core/workflow-events.js +4 -0
- package/dist/core/workflow-run-events.d.ts +5 -3
- package/dist/core/workflow-run-events.js +43 -8
- package/dist/core/zod-json-schema.d.ts +4 -0
- package/dist/core/zod-json-schema.js +39 -0
- package/dist/core/zod3-window.d.ts +35 -0
- package/dist/core/zod3-window.js +124 -0
- package/dist/generated/prisma/browser.d.ts +34 -3
- package/dist/generated/prisma/client.d.ts +34 -3
- package/dist/generated/prisma/commonInputTypes.d.ts +220 -60
- package/dist/generated/prisma/enums.d.ts +10 -0
- package/dist/generated/prisma/enums.js +8 -0
- package/dist/generated/prisma/internal/class.d.ts +44 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +428 -9
- package/dist/generated/prisma/internal/prismaNamespace.js +92 -1
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +95 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +92 -1
- package/dist/generated/prisma/models/Agent.d.ts +106 -8
- package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +229 -1
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
- package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +1637 -1
- package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
- package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
- package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +35 -1
- package/dist/generated/prisma/models/RegistryAllowance.d.ts +41 -1
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
- package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
- package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +738 -2
- package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
- package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
- package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
- package/dist/generated/prisma/models/RunTranscript.js +1 -0
- package/dist/generated/prisma/models.d.ts +4 -0
- package/dist/help-index.json +1320 -69
- package/dist/import/create.js +32 -3
- package/dist/import/neutral-schema.d.ts +50 -281
- package/dist/import/neutral-schema.js +5 -4
- package/dist/knowledge/check.d.ts +1 -1
- package/dist/knowledge/cli.d.ts +11 -1
- package/dist/knowledge/cli.js +37 -4
- package/dist/knowledge/concept.d.ts +3 -19
- package/dist/knowledge/location.d.ts +31 -0
- package/dist/knowledge/location.js +78 -0
- package/dist/mcp/auth/access.d.ts +10 -2
- package/dist/mcp/auth/access.js +15 -1
- package/dist/mcp/auth/ownership.d.ts +7 -7
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
- package/dist/mcp/host-events/github-ingress.d.ts +2 -0
- package/dist/mcp/host-events/github-ingress.js +2 -1
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +58 -10
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +33 -14
- package/dist/mcp/tools/agents.d.ts +4 -0
- package/dist/mcp/tools/agents.js +198 -16
- package/dist/mcp/tools/datastore.js +17 -8
- package/dist/mcp/tools/grants.js +9 -3
- package/dist/mcp/tools/memory.js +11 -1
- package/dist/mcp/tools/notification-channels.js +2 -2
- package/dist/mcp/tools/packages.d.ts +2 -0
- package/dist/mcp/tools/packages.js +195 -0
- package/dist/mcp/tools/repositories.js +18 -5
- package/dist/mcp/tools/run-access.d.ts +16 -0
- package/dist/mcp/tools/run-access.js +25 -0
- package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
- package/dist/mcp/tools/run-transcript-download.js +129 -0
- package/dist/mcp/tools/run-transcript.d.ts +19 -0
- package/dist/mcp/tools/run-transcript.js +70 -0
- package/dist/mcp/tools/runs.js +34 -12
- package/dist/mcp/tools/scheduling.js +3 -1
- package/dist/mcp/tools/secret-elicitation-form.js +5 -0
- package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
- package/dist/mcp/tools/secret-elicitation-server.js +10 -61
- package/dist/mcp/tools/secrets.js +2 -2
- package/dist/mcp/tools/stop-run.d.ts +2 -0
- package/dist/mcp/tools/stop-run.js +82 -0
- package/dist/mcp/tools/subagents.js +2 -2
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +25 -9
- package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
- package/dist/mcp/transport/stdio-link-host.js +67 -0
- package/dist/mcp/transport/streamable-http.d.ts +13 -0
- package/dist/mcp/transport/streamable-http.js +41 -1
- package/dist/native-worker/docker-launcher.d.ts +38 -2
- package/dist/native-worker/docker-launcher.js +65 -5
- package/dist/native-worker/gateway-server.d.ts +41 -3
- package/dist/native-worker/gateway-server.js +124 -27
- package/dist/native-worker/gateway.d.ts +27 -1
- package/dist/native-worker/gateway.js +70 -4
- package/dist/native-worker/http-server.d.ts +3 -1
- package/dist/native-worker/http-server.js +11 -7
- package/dist/native-worker/http-transport.js +1 -1
- package/dist/native-worker/kubernetes-launcher.d.ts +7 -3
- package/dist/native-worker/kubernetes-launcher.js +43 -3
- package/dist/native-worker/launch.d.ts +18 -1
- package/dist/native-worker/launch.js +6 -1
- package/dist/native-worker/ledger.d.ts +18 -2
- package/dist/native-worker/ledger.js +57 -4
- package/dist/native-worker/loopback.js +3 -8
- package/dist/native-worker/main.d.ts +2 -1
- package/dist/native-worker/main.js +14 -3
- package/dist/native-worker/protocol.d.ts +129 -577
- package/dist/native-worker/protocol.js +47 -1
- package/dist/native-worker/sandbox-executor.d.ts +17 -2
- package/dist/native-worker/sandbox-executor.js +78 -4
- package/dist/native-worker/stderr-redaction.d.ts +77 -0
- package/dist/native-worker/stderr-redaction.js +237 -0
- package/dist/native-worker/stdio.d.ts +1 -1
- package/dist/native-worker/stdio.js +6 -5
- package/dist/native-worker/warm-pool.d.ts +2 -2
- package/dist/native-worker/warm-pool.js +4 -4
- package/dist/native-worker/worker.js +28 -2
- package/dist/observability/metrics.js +6 -1
- package/dist/providers/auth/self-hosted.d.ts +1 -1
- package/dist/providers/coding-proxy/dependency-cache/eviction.d.ts +29 -0
- package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
- package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
- package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
- package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
- package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
- package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
- package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
- package/dist/providers/coding-proxy/hold.d.ts +32 -0
- package/dist/providers/coding-proxy/hold.js +86 -0
- package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
- package/dist/providers/coding-proxy/interrupted.js +104 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/memory-ledger.js +84 -8
- package/dist/providers/coding-proxy/metering.d.ts +16 -0
- package/dist/providers/coding-proxy/metering.js +27 -4
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
- package/dist/providers/coding-proxy/proxy.d.ts +14 -1
- package/dist/providers/coding-proxy/proxy.js +166 -14
- package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
- package/dist/providers/coding-proxy/registry/audit.js +94 -17
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
- package/dist/providers/coding-proxy/registry/plan.js +201 -48
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
- package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
- package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
- package/dist/providers/coding-proxy/registry/service.js +579 -112
- package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
- package/dist/providers/coding-proxy/registry/store.js +54 -1
- package/dist/providers/coding-proxy/runtime.d.ts +4 -0
- package/dist/providers/coding-proxy/runtime.js +78 -16
- package/dist/providers/coding-proxy/server.d.ts +6 -1
- package/dist/providers/coding-proxy/server.js +45 -1
- package/dist/providers/coding-proxy/types.d.ts +65 -1
- package/dist/providers/datastore/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +17 -2
- package/dist/providers/engine/types.js +17 -0
- package/dist/providers/executor/composition.d.ts +10 -3
- package/dist/providers/executor/composition.js +47 -7
- package/dist/providers/executor/container.d.ts +361 -8
- package/dist/providers/executor/container.js +1136 -91
- package/dist/providers/executor/dbos-status.d.ts +31 -0
- package/dist/providers/executor/dbos-status.js +64 -0
- package/dist/providers/executor/dbos.d.ts +50 -1
- package/dist/providers/executor/dbos.js +207 -20
- package/dist/providers/executor/deferred.d.ts +12 -24
- package/dist/providers/executor/deferred.js +10 -29
- package/dist/providers/executor/in-process.d.ts +9 -1
- package/dist/providers/executor/in-process.js +14 -4
- package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
- package/dist/providers/executor/linked-issue-reader.js +30 -0
- package/dist/providers/executor/routing.d.ts +2 -0
- package/dist/providers/executor/routing.js +4 -0
- package/dist/providers/executor/types.d.ts +13 -0
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/issue-tracker/types.d.ts +17 -0
- package/dist/providers/issue-tracker/types.js +39 -1
- package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
- package/dist/providers/jobs/claude-tool-setup.js +5 -2
- package/dist/providers/jobs/docker-isolation.d.ts +8 -0
- package/dist/providers/jobs/docker-isolation.js +17 -0
- package/dist/providers/jobs/docker.d.ts +1 -0
- package/dist/providers/jobs/docker.js +27 -2
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +2 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
- package/dist/providers/jobs/kubernetes-api.d.ts +8 -0
- package/dist/providers/jobs/kubernetes-api.js +10 -0
- package/dist/providers/jobs/kubernetes-client.js +10 -1
- package/dist/providers/jobs/kubernetes-isolation.d.ts +2 -0
- package/dist/providers/jobs/kubernetes-isolation.js +17 -1
- package/dist/providers/jobs/kubernetes.d.ts +6 -1
- package/dist/providers/jobs/kubernetes.js +105 -16
- package/dist/providers/jobs/types.d.ts +33 -0
- package/dist/providers/jobs/types.js +13 -1
- package/dist/providers/llm/anthropic.js +5 -1
- package/dist/providers/llm/bedrock.d.ts +0 -12
- package/dist/providers/llm/bedrock.js +4 -0
- package/dist/providers/llm/claude-messages.d.ts +3 -1
- package/dist/providers/llm/claude-messages.js +4 -1
- package/dist/providers/llm/claude-provider.d.ts +2 -2
- package/dist/providers/llm/claude-provider.js +2 -2
- package/dist/providers/llm/openai.d.ts +2 -2
- package/dist/providers/llm/openai.js +6 -2
- package/dist/providers/llm/routing.d.ts +2 -2
- package/dist/providers/llm/routing.js +2 -2
- package/dist/providers/llm/types.d.ts +12 -2
- package/dist/providers/review-host/github-events.d.ts +9 -1
- package/dist/providers/review-host/github-events.js +34 -12
- package/dist/providers/review-host/github.d.ts +29 -2
- package/dist/providers/review-host/github.js +224 -29
- package/dist/providers/review-host/local.d.ts +8 -4
- package/dist/providers/review-host/local.js +49 -31
- package/dist/providers/review-host/patch-pages.d.ts +41 -0
- package/dist/providers/review-host/patch-pages.js +176 -0
- package/dist/providers/review-host/review-format.d.ts +19 -1
- package/dist/providers/review-host/review-format.js +38 -7
- package/dist/providers/review-host/types.d.ts +134 -2
- package/dist/providers/review-host/types.js +8 -0
- package/dist/providers/storage/gcs.d.ts +63 -0
- package/dist/providers/storage/gcs.js +69 -0
- package/dist/providers/storage/index.d.ts +15 -0
- package/dist/providers/storage/index.js +14 -0
- package/dist/providers/storage/local.d.ts +24 -0
- package/dist/providers/storage/local.js +92 -0
- package/dist/providers/storage/meter.d.ts +14 -0
- package/dist/providers/storage/meter.js +34 -0
- package/dist/providers/storage/s3.d.ts +41 -0
- package/dist/providers/storage/s3.js +107 -0
- package/dist/providers/storage/types.d.ts +37 -3
- package/dist/providers/storage/types.js +21 -4
- package/dist/providers/vcs/git.d.ts +33 -2
- package/dist/providers/vcs/git.js +324 -22
- package/dist/providers/vcs/github-remote.d.ts +26 -3
- package/dist/providers/vcs/github-remote.js +124 -14
- package/dist/providers/vcs/github.d.ts +214 -6
- package/dist/providers/vcs/github.js +447 -45
- package/dist/providers/vcs/index.js +8 -1
- package/dist/providers/vcs/pr-description.d.ts +127 -0
- package/dist/providers/vcs/pr-description.js +270 -0
- package/dist/providers/vcs/remote.d.ts +33 -1
- package/dist/providers/vcs/routing.d.ts +4 -2
- package/dist/providers/vcs/routing.js +6 -0
- package/dist/providers/vcs/types.d.ts +80 -3
- package/dist/quickstart/coding-seed.d.ts +14 -3
- package/dist/quickstart/coding-seed.js +213 -4
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +6 -6
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/generated/zod.bundle.js +62 -1
- package/dist/sandbox/generated/zod3.bundle.js +5 -0
- package/dist/sandbox/host-functions.d.ts +1 -0
- package/dist/sandbox/host-functions.js +6 -11
- package/dist/sandbox/secret-broker.js +3 -4
- package/dist/sandbox/tool-capabilities.d.ts +10 -30
- package/dist/sandbox/zod-params.d.ts +37 -5
- package/dist/sandbox/zod-params.js +181 -26
- package/dist/serve.js +8 -1
- package/dist/viewer/api-schema.d.ts +349 -2627
- package/dist/viewer/api-schema.js +7 -0
- package/dist/viewer/build-schemas.js +6 -2
- package/dist/viewer/openapi.js +2 -4
- package/dist/wardby-bin.js +10 -0
- package/docs/agent-recipes.md +58 -20
- package/docs/code-review-agents.md +551 -41
- package/docs/coding-agent-setup.md +489 -7
- package/docs/coding-packages.md +1033 -90
- package/docs/coding-services.md +27 -12
- package/docs/coding-worker-byo-images.md +20 -0
- package/docs/coding-worker-isolation.md +105 -30
- package/docs/getting-started-gke.md +124 -137
- package/docs/getting-started-identity-provider.md +10 -1
- package/docs/getting-started.md +9 -0
- package/docs/jira-agents.md +17 -8
- package/docs/knowledge/blob-not-found-semantics.md +41 -0
- package/docs/knowledge/bounded-file-descriptor.md +52 -0
- package/docs/knowledge/delegation-admission-slots.md +52 -0
- package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
- package/docs/knowledge/import-rename-target.md +52 -0
- package/docs/knowledge/index.md +57 -0
- package/docs/knowledge/log.md +9 -0
- package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
- package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
- package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
- package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
- package/docs/knowledge/overview-cli.md +74 -0
- package/docs/knowledge/overview-coding-proxy.md +72 -0
- package/docs/knowledge/overview-coding.md +72 -0
- package/docs/knowledge/overview-config.md +60 -0
- package/docs/knowledge/overview-core.md +72 -0
- package/docs/knowledge/overview-deploy.md +83 -0
- package/docs/knowledge/overview-mcp.md +61 -0
- package/docs/knowledge/overview-prisma.md +72 -0
- package/docs/knowledge/overview-providers.md +89 -0
- package/docs/knowledge/overview-sandbox.md +83 -0
- package/docs/knowledge/overview-surfaces.md +83 -0
- package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
- package/docs/knowledge/proxy-settlement-projections.md +41 -0
- package/docs/knowledge/run-pricing-first-writer.md +43 -0
- package/docs/knowledge/schedule-claim-transaction.md +41 -0
- package/docs/knowledge/secret-entry-replica-contract.md +52 -0
- package/docs/knowledge/terminal-run-first-writer.md +41 -0
- package/docs/knowledge/transactional-agent-authorization.md +52 -0
- package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
- package/docs/knowledge/warm-input-publication.md +30 -0
- package/docs/knowledge/workspace-staging-failure.md +54 -0
- package/docs/knowledge.md +54 -14
- package/docs/native-sandbox.md +119 -23
- package/docs/observability.md +4 -0
- package/docs/security-deployment.md +33 -2
- package/docs/slack-notifications.md +35 -19
- package/docs/tool-secrets.md +42 -10
- package/docs/viewer-api.openapi.yml +1 -1
- package/help/admin-viewer.md +24 -2
- package/help/architecture-agent.md +11 -6
- package/help/build-worker-image.md +3 -1
- package/help/builder-agent.md +19 -6
- package/help/code-review-agents.md +177 -6
- package/help/coding-packages.md +156 -6
- package/help/cost-attribution.md +9 -5
- package/help/creating-agents.md +14 -1
- package/help/dependency-cache.md +143 -0
- package/help/deploy-gke.md +15 -8
- package/help/errors/budget-exhausted.md +187 -0
- package/help/errors/budget-group-exhausted.md +5 -1
- package/help/errors/coding-stream-quota-exceeded.md +33 -0
- package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
- package/help/errors/continuation-closed.md +9 -12
- package/help/errors/delegation-conflict.md +24 -0
- package/help/errors/file-list-truncated.md +45 -0
- package/help/errors/full-read-required.md +49 -0
- package/help/errors/github-account-not-allowed.md +4 -0
- package/help/errors/graph-incomplete.md +43 -0
- package/help/errors/graph-limit.md +43 -0
- package/help/errors/merge-conflict.md +49 -0
- package/help/errors/native-sandbox-gateway-error.md +20 -0
- package/help/errors/native-sandbox-launch-abandoned.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +3 -2
- package/help/errors/package-not-allowed-pending.md +52 -0
- package/help/errors/package-not-allowed.md +51 -0
- package/help/errors/package-refusals.md +43 -0
- package/help/errors/patch-cursor-malformed.md +31 -0
- package/help/errors/run-lost.md +39 -0
- package/help/errors/run-transcript-download.md +44 -0
- package/help/errors/secret-in-value.md +69 -0
- package/help/errors/target-merge.md +40 -0
- package/help/fix-merge-conflicts.md +73 -0
- package/help/github-app-setup.md +132 -0
- package/help/github.md +22 -0
- package/help/identity-and-access.md +2 -0
- package/help/jira.md +23 -6
- package/help/knowledge.md +16 -7
- package/help/local-repositories.md +1 -1
- package/help/low-power-mode.md +7 -1
- package/help/merge-order-check.md +5 -0
- package/help/native-capabilities.md +29 -1
- package/help/native-sandbox.md +15 -1
- package/help/operating-agents.md +5 -0
- package/help/package-audit-mode.md +91 -0
- package/help/related-pull-requests.md +10 -7
- package/help/resume-runs-locally.md +131 -0
- package/help/review-fix-rounds.md +33 -1
- package/help/sharing-agents.md +59 -0
- package/help/slack-notifications.md +19 -5
- package/help/status-comments.md +31 -0
- package/help/stop-run.md +58 -0
- package/help/troubleshooting/budgets.md +28 -0
- package/help/troubleshooting/coding-workers.md +78 -7
- package/package.json +17 -10
- package/prisma/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
- package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
- package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
- package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
- package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
- package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
- package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
- package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
- package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
- package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
- package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
- package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
- package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
- package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
- package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
- package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
- package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
- package/prisma/schema.prisma +311 -97
- package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { logger } from "../../../core/logger.js";
|
|
13
13
|
import { matchRoot, parseAllowlist, resolvePolicy } from "../../../coding/registry/allowlist.js";
|
|
14
|
-
import { PLAN_REFUSAL_FILENAME } from "../../../coding/registry/report.js";
|
|
14
|
+
import { acceptedAdvisoriesReason, LISTING_REFUSAL_FILENAME, PLAN_REFUSAL_FILENAME, } from "../../../coding/registry/report.js";
|
|
15
|
+
import { GRAPH_INCOMPLETE, GRAPH_LIMIT, graphIncompleteMessage, graphLimitMessage, notAllowedMessage, notAllowedPendingMessage, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_ALLOWED_PENDING, refusalHint, } from "../../../coding/registry/refusal-hints.js";
|
|
15
16
|
import { RegistryError, } from "../../../coding/registry/types.js";
|
|
16
17
|
import { capabilityHash } from "../proxy.js";
|
|
18
|
+
import { DEPENDENCY_CACHE_ECOSYSTEM, MAX_DEPENDENCY_CACHE_LOCK_HASHES, sha256Hex } from "../dependency-cache/key.js";
|
|
17
19
|
import { boundedMetadataFetch, DEFAULT_MAX_METADATA_BYTES, DEFAULT_METADATA_TIMEOUT_MS } from "./bounded-fetch.js";
|
|
20
|
+
import { parseLockfileOffThread } from "./lockfile-parse.js";
|
|
18
21
|
import { DEFAULT_PLAN_MAX_ENTRIES, DEFAULT_PLAN_TIMEOUT_MS, verifyLockfilePlan } from "./plan.js";
|
|
22
|
+
import { AUDITED_VIA_UNKNOWN } from "./store.js";
|
|
19
23
|
const registryLog = logger.child({ module: "coding-registry" });
|
|
20
24
|
/** Server-side record of a registry failure. The worker only ever sees the
|
|
21
25
|
* RegistryError's code and message; the operator needs the cause. Without
|
|
@@ -57,9 +61,26 @@ const GRAPH_WALK_CONCURRENCY = 2;
|
|
|
57
61
|
* walk cannot starve the others' upstream fetches -- or the proxy's event
|
|
58
62
|
* loop: parsing packuments is CPU work on the same loop that carries model
|
|
59
63
|
* traffic (per-walk 2 and global 3 since a live install at 4/6 on 500m
|
|
60
|
-
* expired a ledger transaction). The walk is only the fallback now
|
|
61
|
-
*
|
|
64
|
+
* expired a ledger transaction). The walk is only the fallback now: a
|
|
65
|
+
* download of a version the run has not resolved starts one (and a
|
|
66
|
+
* metadata miss, only after a failed or partial lockfile plan), and
|
|
67
|
+
* lockfile verification answers most of those without it. */
|
|
62
68
|
export const DEFAULT_MAX_GRAPH_CONCURRENCY = 3;
|
|
69
|
+
/** Byte budget of the graph walk's memo of expanded packages (kept
|
|
70
|
+
* versions and their declared dependencies), across every run. */
|
|
71
|
+
export const DEFAULT_GRAPH_MEMO_BYTES = 32 * 1024 * 1024;
|
|
72
|
+
/** How long a memoized expansion is reused. Safe for that long: a kept
|
|
73
|
+
* version depends on the release-age cutoff, which only moves forward (a
|
|
74
|
+
* stale entry can only omit a version that has since become old enough,
|
|
75
|
+
* which fails closed), and on the OSV audit, which is itself cached for an
|
|
76
|
+
* hour (audit.ts). The walk's view of advisories can therefore lag by the
|
|
77
|
+
* audit cache TTL plus 15 minutes; that only affects which names the walk
|
|
78
|
+
* allows, because every served version is re-audited through keptVersions
|
|
79
|
+
* and malware is never allowed. A version published upstream within the TTL is
|
|
80
|
+
* too new to be kept under any non-zero minReleaseAgeDays; with
|
|
81
|
+
* minReleaseAgeDays 0 it is omitted until the entry expires (fails
|
|
82
|
+
* closed). */
|
|
83
|
+
export const GRAPH_MEMO_TTL_MS = 15 * 60_000;
|
|
63
84
|
/** A node whose metadata or audit fails transiently is retried once more
|
|
64
85
|
* in the same walk, then parked; a later miss retries it again, up to
|
|
65
86
|
* this many attempts per node per run. */
|
|
@@ -82,6 +103,17 @@ export const DEFAULT_PLAN_MAX_PER_RUN = 20;
|
|
|
82
103
|
export const PLAN_FETCH_FILENAME = PLAN_REFUSAL_FILENAME;
|
|
83
104
|
/** Largest lockfile a plan reads (the HTTP layer enforces it: 413). */
|
|
84
105
|
export const MAX_PLAN_BODY_BYTES = 20 * 1024 * 1024;
|
|
106
|
+
/** Approximate retained size of a memoized expansion, for the memo's byte
|
|
107
|
+
* budget. Deliberately generous. */
|
|
108
|
+
function keptBytes(key, kept) {
|
|
109
|
+
let bytes = 128 + 2 * key.length;
|
|
110
|
+
for (const [version, dependencies] of kept) {
|
|
111
|
+
bytes += 64 + 2 * version.length;
|
|
112
|
+
for (const spec of dependencies)
|
|
113
|
+
bytes += 48 + 2 * (spec.name.length + spec.range.length);
|
|
114
|
+
}
|
|
115
|
+
return bytes;
|
|
116
|
+
}
|
|
85
117
|
/** A small counting semaphore. */
|
|
86
118
|
class Slots {
|
|
87
119
|
size;
|
|
@@ -108,10 +140,92 @@ class Slots {
|
|
|
108
140
|
}
|
|
109
141
|
}
|
|
110
142
|
const edgeKey = (edge) => `${edge.name}\0${edge.range}`;
|
|
143
|
+
/** Whether a parked failure may be retried. */
|
|
144
|
+
function retriable(walk, name) {
|
|
145
|
+
return (walk.attempts.get(name) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS;
|
|
146
|
+
}
|
|
147
|
+
/** Names with an edge the walk has not expanded yet: pending, or parked in
|
|
148
|
+
* `failed` with attempts left. Read only between batches (a running
|
|
149
|
+
* batch is off the queue). */
|
|
150
|
+
function unexpandedNames(walk) {
|
|
151
|
+
const names = new Set(walk.pending.keys());
|
|
152
|
+
for (const name of walk.failed.keys())
|
|
153
|
+
if (retriable(walk, name))
|
|
154
|
+
names.add(name);
|
|
155
|
+
return names;
|
|
156
|
+
}
|
|
157
|
+
/** Packages walked so far, and the distinct names with an unexpanded edge
|
|
158
|
+
* that are not yet expanded themselves (a further edge to an expanded
|
|
159
|
+
* package adds no new package). */
|
|
160
|
+
function walkProgress(walk) {
|
|
161
|
+
const queued = unexpandedNames(walk);
|
|
162
|
+
for (const name of walk.expandedVersions.keys())
|
|
163
|
+
queued.delete(name);
|
|
164
|
+
return { walked: walk.expandedVersions.size, queued: queued.size };
|
|
165
|
+
}
|
|
166
|
+
/** Unexpanded edges, for the progress log. */
|
|
167
|
+
function unexpandedEdgeCount(walk) {
|
|
168
|
+
let count = 0;
|
|
169
|
+
for (const edges of walk.pending.values())
|
|
170
|
+
count += edges.length;
|
|
171
|
+
for (const [name, entry] of walk.failed)
|
|
172
|
+
if (retriable(walk, name))
|
|
173
|
+
count += entry.edges.length;
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
/** Adds edges to `name`'s pending group (at the end of the order, or at
|
|
177
|
+
* the front with `first`). */
|
|
178
|
+
function addPending(walk, name, edges, first = false) {
|
|
179
|
+
const group = walk.pending.get(name);
|
|
180
|
+
if (group) {
|
|
181
|
+
group.push(...edges);
|
|
182
|
+
if (first)
|
|
183
|
+
moveToFront(walk, name);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
walk.pending.set(name, [...edges]);
|
|
187
|
+
if (first)
|
|
188
|
+
walk.order.unshift(name);
|
|
189
|
+
else
|
|
190
|
+
walk.order.push(name);
|
|
191
|
+
}
|
|
192
|
+
function moveToFront(walk, name) {
|
|
193
|
+
const index = walk.order.indexOf(name);
|
|
194
|
+
if (index <= 0)
|
|
195
|
+
return;
|
|
196
|
+
walk.order.splice(index, 1);
|
|
197
|
+
walk.order.unshift(name);
|
|
198
|
+
}
|
|
111
199
|
export const DEFAULT_MAX_REFUSAL_RECORDS = 500;
|
|
200
|
+
/** The most refusals `GET /registry/-/refusals` returns (distinct, newest first). */
|
|
201
|
+
export const MAX_LISTED_REFUSALS = 20;
|
|
202
|
+
const DEFAULT_REFUSALS_WINDOW_MS = 15 * 60_000;
|
|
112
203
|
function contentTypeOf(route) {
|
|
113
204
|
return route.kind === "file-metadata" ? "text/plain" : "application/octet-stream";
|
|
114
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Why kept versions leave nothing to download, or null when some file is
|
|
208
|
+
* servable: every allowed file newer than the release-age limit
|
|
209
|
+
* (`wardby_version_filtered`), or no allowed file at all, such as a Python
|
|
210
|
+
* package with only source distributions (`wardby_file_not_allowed`).
|
|
211
|
+
*/
|
|
212
|
+
export function unservableReason(meta, keep, keptFiles) {
|
|
213
|
+
let anyFile = false;
|
|
214
|
+
let anyAllowed = false;
|
|
215
|
+
for (const version of keep) {
|
|
216
|
+
for (const file of meta.versions.get(version)?.files ?? []) {
|
|
217
|
+
anyFile = true;
|
|
218
|
+
if (!file.allowed)
|
|
219
|
+
continue;
|
|
220
|
+
if (keptFiles.has(file.filename))
|
|
221
|
+
return null;
|
|
222
|
+
anyAllowed = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (anyAllowed)
|
|
226
|
+
return "wardby_version_filtered";
|
|
227
|
+
return anyFile ? "wardby_file_not_allowed" : null;
|
|
228
|
+
}
|
|
115
229
|
export function errorResponse(error) {
|
|
116
230
|
return {
|
|
117
231
|
status: error.status,
|
|
@@ -217,6 +331,17 @@ export class RegistryService {
|
|
|
217
331
|
* across replicas. */
|
|
218
332
|
inFlight = new Map();
|
|
219
333
|
tallies = new Map();
|
|
334
|
+
/** Expanded packages of the graph walks (`runId\0ecosystem\0name`),
|
|
335
|
+
* least recently used first, so a package reached again under a new
|
|
336
|
+
* range, in a later request of the same run, is not read again.
|
|
337
|
+
* Run-scoped by key: a package's kept versions depend on the run's
|
|
338
|
+
* policy and allowlist range. Bounded to `graphMemoBytes`; entries
|
|
339
|
+
* expire GRAPH_MEMO_TTL_MS after they are computed, and a run's entries
|
|
340
|
+
* are dropped when the run is released. */
|
|
341
|
+
graphMemo = new Map();
|
|
342
|
+
/** Sum of the memo entries' `bytes`. */
|
|
343
|
+
graphMemoTotal = 0;
|
|
344
|
+
graphMemoBytes = DEFAULT_GRAPH_MEMO_BYTES;
|
|
220
345
|
walkSlots;
|
|
221
346
|
planSlots;
|
|
222
347
|
versionUpstream;
|
|
@@ -237,6 +362,49 @@ export class RegistryService {
|
|
|
237
362
|
maxBytes: MAX_VERSION_DOCUMENT_BYTES,
|
|
238
363
|
});
|
|
239
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* `GET /registry/-/refusals`: the run's recent refusals (package, code and the
|
|
367
|
+
* shared hint), for a client that does not print the registry's 403 body
|
|
368
|
+
* (pip, uv). Authenticated with the run's registry token. Names come from the
|
|
369
|
+
* run's own requests; nothing else (no URL, no token) is returned.
|
|
370
|
+
*/
|
|
371
|
+
async refusals(request) {
|
|
372
|
+
try {
|
|
373
|
+
const context = await this.options.store.findRunByRegistryTokenHash(capabilityHash(request.token), this.now());
|
|
374
|
+
if (!context)
|
|
375
|
+
throw new RegistryError(401, "invalid_capability", "the registry token is not valid for a live run");
|
|
376
|
+
const since = request.since && !Number.isNaN(request.since.getTime())
|
|
377
|
+
? request.since
|
|
378
|
+
: new Date(this.now().getTime() - DEFAULT_REFUSALS_WINDOW_MS);
|
|
379
|
+
const rows = await this.options.store.listRecentRefusals(context.runId, since, MAX_LISTED_REFUSALS * 5);
|
|
380
|
+
const seen = new Set();
|
|
381
|
+
const refusals = [];
|
|
382
|
+
for (const row of rows) {
|
|
383
|
+
const code = row.reason ?? "wardby_refused";
|
|
384
|
+
const key = `${row.ecosystem}\0${row.name}\0${row.version ?? ""}\0${code}`;
|
|
385
|
+
if (seen.has(key))
|
|
386
|
+
continue;
|
|
387
|
+
seen.add(key);
|
|
388
|
+
const hint = refusalHint(code);
|
|
389
|
+
refusals.push({
|
|
390
|
+
ecosystem: row.ecosystem,
|
|
391
|
+
name: row.name,
|
|
392
|
+
...(row.version ? { version: row.version } : {}),
|
|
393
|
+
code,
|
|
394
|
+
...(hint ? { hint } : {}),
|
|
395
|
+
});
|
|
396
|
+
if (refusals.length >= MAX_LISTED_REFUSALS)
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
return { status: 200, contentType: "application/json", body: JSON.stringify({ refusals }) };
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
logRegistryFailure("refusals", "-", error);
|
|
403
|
+
if (error instanceof RegistryError)
|
|
404
|
+
return errorResponse(error);
|
|
405
|
+
return errorResponse(new RegistryError(502, "wardby_upstream_error", "the package registry request failed"));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
240
408
|
/** `POST /registry/<ecosystem>/-/plan`: verifies a lockfile against the
|
|
241
409
|
* registry and approves exactly its verified, reachable name@version
|
|
242
410
|
* pairs for the run (plan.ts). Answers `{ approved, refused }`. */
|
|
@@ -263,6 +431,15 @@ export class RegistryService {
|
|
|
263
431
|
if (!support || !auditVersions)
|
|
264
432
|
throw new RegistryError(404, "wardby_plan_unsupported", `the ${adapter.id} registry does not verify lockfiles`);
|
|
265
433
|
const tally = this.tally(context);
|
|
434
|
+
try {
|
|
435
|
+
return await this.runPlan(request, adapter, support, auditVersions, context, tally);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
(tally.planFallback ??= new Set()).add(adapter.id);
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async runPlan(request, adapter, support, auditVersions, context, tally) {
|
|
266
443
|
if (tally.planning)
|
|
267
444
|
throw new RegistryError(429, "wardby_plan_in_progress", "a lockfile plan is already running for this run; wait for it to finish");
|
|
268
445
|
const maxPlans = this.options.planMaxPerRun ?? DEFAULT_PLAN_MAX_PER_RUN;
|
|
@@ -274,20 +451,32 @@ export class RegistryService {
|
|
|
274
451
|
// The body is read only while a slot is held: a queued plan holds no
|
|
275
452
|
// lockfile text, and at most `planConcurrency` bodies exist at once.
|
|
276
453
|
return await this.planSlots.run(async () => {
|
|
277
|
-
const
|
|
454
|
+
const body = await request.readBody();
|
|
455
|
+
// The dependency cache's lockHash: computed here, from the bytes the
|
|
456
|
+
// proxy itself verified, never from anything a client claims.
|
|
457
|
+
const lockHash = context.dependencyCache && adapter.id === DEPENDENCY_CACHE_ECOSYSTEM ? sha256Hex(body) : null;
|
|
458
|
+
const parseOptions = {
|
|
278
459
|
maxEntries: this.options.planMaxEntries ?? DEFAULT_PLAN_MAX_ENTRIES,
|
|
279
460
|
proxyRegistryUrl: `${this.options.proxyBase}${adapter.id}/`,
|
|
280
|
-
}
|
|
281
|
-
|
|
461
|
+
};
|
|
462
|
+
// A YAML or yarn lockfile is read in a worker thread with a time
|
|
463
|
+
// bound (lockfile-parse.ts); npm's JSON stays on the event loop.
|
|
464
|
+
const lockfile = support.parsesOffThread?.(body)
|
|
465
|
+
? await parseLockfileOffThread(adapter.id, body, parseOptions, {
|
|
466
|
+
timeoutMs: this.options.planParseTimeoutMs,
|
|
467
|
+
signal: request.signal,
|
|
468
|
+
})
|
|
469
|
+
: support.parse(body, parseOptions);
|
|
470
|
+
return this.verifyPlan(request, adapter, support, lockfile, auditVersions, context, lockHash);
|
|
282
471
|
});
|
|
283
472
|
}
|
|
284
473
|
finally {
|
|
285
474
|
tally.planning = false;
|
|
286
475
|
}
|
|
287
476
|
}
|
|
288
|
-
async verifyPlan(request, adapter, support, lockfile, auditVersions, context) {
|
|
477
|
+
async verifyPlan(request, adapter, support, lockfile, auditVersions, context, lockHash) {
|
|
289
478
|
const entries = parseAllowlist(context.allowlist, this.options.adapters).get(adapter.id) ?? [];
|
|
290
|
-
const { minReleaseAgeDays } = resolvePolicy(context.policy);
|
|
479
|
+
const { minReleaseAgeDays, acceptedAdvisories } = resolvePolicy(context.policy);
|
|
291
480
|
const timeoutMs = this.options.planTimeoutMs ?? DEFAULT_PLAN_TIMEOUT_MS;
|
|
292
481
|
const signal = AbortSignal.any([AbortSignal.timeout(timeoutMs), request.signal]);
|
|
293
482
|
const store = this.options.store;
|
|
@@ -306,6 +495,8 @@ export class RegistryService {
|
|
|
306
495
|
upstream: this.versionUpstream,
|
|
307
496
|
streamUpstream: this.options.upstream,
|
|
308
497
|
audit: (versions, auditSignal) => auditVersions(adapter, versions, auditSignal),
|
|
498
|
+
accepted: new Set(acceptedAdvisories),
|
|
499
|
+
auditMode: context.packageAudit,
|
|
309
500
|
hostAllowed: (url) => this.hostAllowed(adapter, url),
|
|
310
501
|
signal,
|
|
311
502
|
});
|
|
@@ -318,7 +509,33 @@ export class RegistryService {
|
|
|
318
509
|
? "the client went away before the lockfile was verified"
|
|
319
510
|
: `the lockfile was not verified within ${Math.round(timeoutMs / 1000)} s (REGISTRY_PLAN_TIMEOUT_MS); what was verified is kept, so try again`);
|
|
320
511
|
}
|
|
321
|
-
|
|
512
|
+
// An approved name is proven part of the approved graph, so it is an
|
|
513
|
+
// allowance too: its metadata (which npm re-reads for packages already
|
|
514
|
+
// in the lockfile) is served without a graph walk. Written before the
|
|
515
|
+
// approvals: a name allowance left behind by a failed approval write is
|
|
516
|
+
// only what a walk would conclude.
|
|
517
|
+
// In package audit mode an approval outside the allowlist is an audited
|
|
518
|
+
// allowance, tagged with the top-level audited package that brought it
|
|
519
|
+
// (one of unknown relation with AUDITED_VIA_UNKNOWN, never itself).
|
|
520
|
+
const plain = result.approved.filter((v) => !v.audit || v.audit.relation === "allowed");
|
|
521
|
+
await store.addAllowances(context.runId, adapter.id, [...new Set(plain.map((v) => v.name))]);
|
|
522
|
+
const byVia = new Map();
|
|
523
|
+
for (const v of result.approved) {
|
|
524
|
+
if (!v.audit || v.audit.relation === "allowed")
|
|
525
|
+
continue;
|
|
526
|
+
const via = v.audit.relation === "unknown" ? AUDITED_VIA_UNKNOWN : (v.audit.via ?? v.name);
|
|
527
|
+
byVia.set(via, (byVia.get(via) ?? new Set()).add(v.name));
|
|
528
|
+
}
|
|
529
|
+
for (const [via, names] of byVia)
|
|
530
|
+
await store.addAuditedAllowances(context.runId, adapter.id, [...names], via);
|
|
531
|
+
// A registry whose approvals are names (PyPI) keeps every download on the
|
|
532
|
+
// usual per-file path: no exact version is served from an approval.
|
|
533
|
+
if (support.approves !== "names")
|
|
534
|
+
await store.approveVersions(context.runId, adapter.id, result.approved);
|
|
535
|
+
// A plan that refused anything leaves some lockfile packages without an
|
|
536
|
+
// allowance: metadata misses walk for the rest of the run.
|
|
537
|
+
if (result.refused.length > 0)
|
|
538
|
+
(this.tally(context).planFallback ??= new Set()).add(adapter.id);
|
|
322
539
|
// Only verdicts about the registry's name@version itself answer later
|
|
323
540
|
// downloads; a lockfile-dependent one (unreachable, an integrity or
|
|
324
541
|
// source the lockfile claims) must not block the same version reached
|
|
@@ -327,12 +544,19 @@ export class RegistryService {
|
|
|
327
544
|
.filter((refusal) => refusal.intrinsic)
|
|
328
545
|
.map(({ name, version, code, reason, publishedAt }) => ({ name, version, code, reason, publishedAt })));
|
|
329
546
|
await this.recordPlanRefusals(context, adapter, result.refused);
|
|
547
|
+
// Only a lockfile approved whole may be restored from the dependency
|
|
548
|
+
// cache; the store ignores it once the session is sealed or full. A plan
|
|
549
|
+
// that audited anything is never cached: a restore downloads nothing,
|
|
550
|
+
// so nothing it installs would be recorded.
|
|
551
|
+
if (lockHash && result.refused.length === 0 && result.audited === 0)
|
|
552
|
+
await store.recordDependencyCacheLockHash(context.runId, lockHash, MAX_DEPENDENCY_CACHE_LOCK_HASHES);
|
|
330
553
|
return {
|
|
331
554
|
status: 200,
|
|
332
555
|
contentType: "application/json",
|
|
333
556
|
body: JSON.stringify({
|
|
334
557
|
approved: result.approved.length,
|
|
335
558
|
refused: result.refused.map(({ name, version, code, reason }) => ({ name, version, code, reason })),
|
|
559
|
+
...(context.packageAudit ? { audited: result.audited } : {}),
|
|
336
560
|
}),
|
|
337
561
|
};
|
|
338
562
|
}
|
|
@@ -407,7 +631,9 @@ export class RegistryService {
|
|
|
407
631
|
}
|
|
408
632
|
if (request.method === "HEAD")
|
|
409
633
|
return { status: 200, contentType: contentTypeOf(route), body: "" };
|
|
410
|
-
|
|
634
|
+
// The plan's waiver (if any) is recorded on the served row, and its
|
|
635
|
+
// audit verdict (audit-mode plans only) as the run's audit record.
|
|
636
|
+
return this.download(adapter, context, name, route, file, request.signal, acceptedAdvisoriesReason(approved.acceptedAdvisories), approved.audit);
|
|
411
637
|
}
|
|
412
638
|
// A version the plan definitively refused is answered from the plan,
|
|
413
639
|
// with its code and reason: no metadata, no graph walk.
|
|
@@ -428,7 +654,7 @@ export class RegistryService {
|
|
|
428
654
|
throw new RegistryError(403, refused.code, `"${name}" ${route.version} was refused by this run's lockfile verification: ${refused.reason}`);
|
|
429
655
|
}
|
|
430
656
|
}
|
|
431
|
-
const root = await this.authorize(adapter, context, name);
|
|
657
|
+
const { root, audit } = await this.authorize(adapter, context, name, route.kind);
|
|
432
658
|
let meta;
|
|
433
659
|
try {
|
|
434
660
|
// Only a metadata request renders a document; a download, the version
|
|
@@ -445,8 +671,10 @@ export class RegistryService {
|
|
|
445
671
|
}
|
|
446
672
|
let keep;
|
|
447
673
|
let keptFiles;
|
|
674
|
+
let waivers;
|
|
675
|
+
let auditAdvisories;
|
|
448
676
|
try {
|
|
449
|
-
({ keep, keptFiles } = await this.keptVersions(adapter, context, meta, root));
|
|
677
|
+
({ keep, keptFiles, waivers, auditAdvisories } = await this.keptVersions(adapter, context, meta, root));
|
|
450
678
|
}
|
|
451
679
|
catch (error) {
|
|
452
680
|
// Every refusal gets a row, including one the run never chose: the
|
|
@@ -456,12 +684,37 @@ export class RegistryService {
|
|
|
456
684
|
}
|
|
457
685
|
throw error;
|
|
458
686
|
}
|
|
687
|
+
if (route.kind === "metadata" && route.version !== undefined) {
|
|
688
|
+
// One version's document: the same checks as the whole package's,
|
|
689
|
+
// narrowed to that version.
|
|
690
|
+
const document = keep.has(route.version)
|
|
691
|
+
? (adapter.renderVersion?.(meta, route.version, keptFiles, `${this.options.proxyBase}${adapter.id}/`) ?? null)
|
|
692
|
+
: null;
|
|
693
|
+
if (!document) {
|
|
694
|
+
await this.refuse(context, adapter, name, "wardby_version_filtered", {
|
|
695
|
+
version: route.version,
|
|
696
|
+
filename: `${route.version}.tgz`,
|
|
697
|
+
});
|
|
698
|
+
throw new RegistryError(404, "wardby_version_filtered", `"${name}" ${route.version} is outside the allowlisted range, newer than the release-age limit, has a high-severity advisory, or is not published`);
|
|
699
|
+
}
|
|
700
|
+
await this.allowServed(context, adapter, name, keptDependencies(adapter, meta, new Set([route.version])), audit);
|
|
701
|
+
return { status: 200, contentType: document.contentType, body: document.body };
|
|
702
|
+
}
|
|
459
703
|
if (route.kind === "metadata") {
|
|
460
704
|
if (keep.size === 0) {
|
|
461
705
|
await this.refuse(context, adapter, name, "wardby_version_filtered");
|
|
462
706
|
throw new RegistryError(404, "wardby_version_filtered", `every matching version of "${name}" is outside the allowlisted range, newer than the release-age limit, or has a high-severity advisory`);
|
|
463
707
|
}
|
|
464
|
-
await this.
|
|
708
|
+
await this.allowServed(context, adapter, name, keptDependencies(adapter, meta, keep), audit);
|
|
709
|
+
// Versions kept but no file the client may download: a Python package
|
|
710
|
+
// published only as an sdist, or whose every wheel is newer than the
|
|
711
|
+
// release-age limit. The (empty) index is still served, and pip and uv
|
|
712
|
+
// only say "no versions", so the refusal is recorded to say why, marked
|
|
713
|
+
// as a listing's: the installer may not have needed it (see
|
|
714
|
+
// LISTING_REFUSAL_FILENAME for how it counts).
|
|
715
|
+
const unservable = unservableReason(meta, keep, keptFiles);
|
|
716
|
+
if (unservable)
|
|
717
|
+
await this.refuse(context, adapter, name, unservable, { filename: LISTING_REFUSAL_FILENAME });
|
|
465
718
|
const document = adapter.renderMetadata(meta, keep, keptFiles, `${this.options.proxyBase}${adapter.id}/`);
|
|
466
719
|
return { status: 200, contentType: document.contentType, body: document.body };
|
|
467
720
|
}
|
|
@@ -474,7 +727,9 @@ export class RegistryService {
|
|
|
474
727
|
// (route.filename), since keptFiles holds wheel filenames.
|
|
475
728
|
const agedFile = route.kind === "file-metadata" ? route.filename : file?.filename;
|
|
476
729
|
if (!file || !keep.has(file.version) || !agedFile || !keptFiles.has(agedFile)) {
|
|
477
|
-
|
|
730
|
+
// A download that does not resolve is still a refusal of the version asked for, not of the whole
|
|
731
|
+
// name (which any version served later would clear; see summarizeRegistryFetches).
|
|
732
|
+
await this.refuse(context, adapter, name, "wardby_version_filtered", file ?? (route.kind === "download" ? { version: route.version, filename: route.filename } : undefined));
|
|
478
733
|
const versionSuffix = route.kind === "download" ? ` ${route.version}` : "";
|
|
479
734
|
throw new RegistryError(404, "wardby_version_filtered", `"${name}"${versionSuffix} is not available to this run`);
|
|
480
735
|
}
|
|
@@ -492,7 +747,30 @@ export class RegistryService {
|
|
|
492
747
|
// reservation, nothing recorded as served.
|
|
493
748
|
if (request.method === "HEAD")
|
|
494
749
|
return { status: 200, contentType: contentTypeOf(route), body: "" };
|
|
495
|
-
|
|
750
|
+
// A version kept only by waiving accepted advisories records them on
|
|
751
|
+
// its served row. In package audit mode, a package outside the
|
|
752
|
+
// allowlist, or a version kept only past advisories (full), is recorded
|
|
753
|
+
// as the run's audit record.
|
|
754
|
+
const advisories = auditAdvisories.get(file.version) ?? [];
|
|
755
|
+
const verdict = audit
|
|
756
|
+
? { ...audit, ...(advisories.length > 0 ? { advisories } : {}) }
|
|
757
|
+
: advisories.length > 0
|
|
758
|
+
? { relation: "allowed", advisories }
|
|
759
|
+
: undefined;
|
|
760
|
+
return this.download(adapter, context, name, route, file, request.signal, acceptedAdvisoriesReason(waivers.get(file.version) ?? []), verdict);
|
|
761
|
+
}
|
|
762
|
+
/** The allowances serving a package's metadata grows: its kept versions'
|
|
763
|
+
* dependencies, and, for an audited package, each tagged with the
|
|
764
|
+
* top-level audited package that declared it (its own name for a
|
|
765
|
+
* top-level one, which is also recorded for itself so its later
|
|
766
|
+
* downloads read as top-level). */
|
|
767
|
+
async allowServed(context, adapter, name, dependencies, audit) {
|
|
768
|
+
const store = this.options.store;
|
|
769
|
+
if (!audit)
|
|
770
|
+
return store.addAllowances(context.runId, adapter.id, dependencies);
|
|
771
|
+
if (audit.relation === "top-level")
|
|
772
|
+
await store.addAuditedAllowances(context.runId, adapter.id, [name], name);
|
|
773
|
+
await store.addAuditedAllowances(context.runId, adapter.id, dependencies, audit.via ?? name);
|
|
496
774
|
}
|
|
497
775
|
hostAllowed(adapter, url) {
|
|
498
776
|
try {
|
|
@@ -513,32 +791,71 @@ export class RegistryService {
|
|
|
513
791
|
}
|
|
514
792
|
return tally.served;
|
|
515
793
|
}
|
|
516
|
-
async authorize(adapter, context, name) {
|
|
794
|
+
async authorize(adapter, context, name, kind) {
|
|
517
795
|
const entries = parseAllowlist(context.allowlist, this.options.adapters).get(adapter.id) ?? [];
|
|
796
|
+
// A root keeps its own range in every mode: audit mode never serves a
|
|
797
|
+
// version an entry's range excludes (keptVersions applies root.range).
|
|
518
798
|
const root = matchRoot(entries, name);
|
|
519
|
-
if (root
|
|
520
|
-
return root;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
799
|
+
if (root)
|
|
800
|
+
return { root };
|
|
801
|
+
const allowance = await this.options.store.findAllowance(context.runId, adapter.id, name);
|
|
802
|
+
if (allowance) {
|
|
803
|
+
if (allowance.auditedVia === null)
|
|
804
|
+
return { root: undefined };
|
|
805
|
+
if (allowance.auditedVia === AUDITED_VIA_UNKNOWN)
|
|
806
|
+
return { root: undefined, audit: { relation: "unknown" } };
|
|
807
|
+
return allowance.auditedVia === name
|
|
808
|
+
? { root: undefined, audit: { relation: "top-level" } }
|
|
809
|
+
: { root: undefined, audit: { relation: "transitive", via: allowance.auditedVia } };
|
|
810
|
+
}
|
|
811
|
+
if (context.packageAudit) {
|
|
812
|
+
// Package audit mode: a miss is never refused and never walks; it is
|
|
813
|
+
// served (through every other check) and recorded. A download with
|
|
814
|
+
// no metadata first, or a metadata miss once a lockfile plan of the
|
|
815
|
+
// run fell back, cannot say how the package reached the run.
|
|
816
|
+
const fellBack = this.tally(context).planFallback?.has(adapter.id) ?? false;
|
|
817
|
+
return { root: undefined, audit: { relation: kind === "download" || fellBack ? "unknown" : "top-level" } };
|
|
818
|
+
}
|
|
819
|
+
// A package manager resolves top-down: it reads a parent's metadata,
|
|
820
|
+
// which records the parent's declared dependencies as allowances,
|
|
821
|
+
// before it asks for a child. A metadata request for a name that is
|
|
822
|
+
// neither a root nor an allowance is therefore refused at once, unless
|
|
823
|
+
// a lockfile plan of this run failed or refused an entry (then the
|
|
824
|
+
// lockfile's packages may lack allowances, and the miss walks).
|
|
825
|
+
if (kind !== "download" && !this.tally(context).planFallback?.has(adapter.id)) {
|
|
826
|
+
await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED);
|
|
827
|
+
throw new RegistryError(403, PACKAGE_NOT_ALLOWED, notAllowedMessage(name, adapter.id, entries.length > 0));
|
|
828
|
+
}
|
|
829
|
+
// A download (or, after a failed or partial plan, a metadata request)
|
|
830
|
+
// that is not yet an allowance: the name may still be in the approved
|
|
831
|
+
// graph, unrecorded only because the client never asked for its
|
|
832
|
+
// parent's metadata (a lockfile install requests tarballs directly).
|
|
833
|
+
// Resolve the graph before refusing.
|
|
525
834
|
const graph = await this.resolveInGraph(adapter, context, entries, name);
|
|
526
835
|
if (graph === "found")
|
|
527
|
-
return undefined;
|
|
836
|
+
return { root: undefined };
|
|
528
837
|
if (typeof graph === "object" && "cutShort" in graph) {
|
|
529
838
|
if (graph.cutShort === "limit") {
|
|
530
839
|
// The bound is permanent for the rest of the run, so a retry can
|
|
531
840
|
// never succeed: answer with a definitive 4xx (npm does not retry
|
|
532
841
|
// it) rather than a 503 that a client or agent would retry forever.
|
|
533
842
|
// Still not "not allowed": the name was not proven absent.
|
|
534
|
-
await this.refuse(context, adapter, name,
|
|
535
|
-
throw new RegistryError(403,
|
|
843
|
+
await this.refuse(context, adapter, name, GRAPH_LIMIT);
|
|
844
|
+
throw new RegistryError(403, GRAPH_LIMIT, graphLimitMessage(name, adapter.id, graph.limit));
|
|
536
845
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
846
|
+
if (graph.frontier) {
|
|
847
|
+
// An expanded version declares the name and only its own versions
|
|
848
|
+
// are left to check: the walk resumes there on the next miss, so a
|
|
849
|
+
// retry (npm retries a 5xx on its own) can find it.
|
|
850
|
+
await this.refuse(context, adapter, name, GRAPH_INCOMPLETE);
|
|
851
|
+
throw new RegistryError(503, GRAPH_INCOMPLETE, graphIncompleteMessage(name, adapter.id, graph.progress));
|
|
852
|
+
}
|
|
853
|
+
// The walk never reached the name. Not proven absent, so not the
|
|
854
|
+
// final "not allowed", but definite (a 4xx npm does not retry): a
|
|
855
|
+
// later miss may still resume the walk and find it, which is no
|
|
856
|
+
// reason to keep the agent waiting on retries now.
|
|
857
|
+
await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED_PENDING);
|
|
858
|
+
throw new RegistryError(403, PACKAGE_NOT_ALLOWED_PENDING, notAllowedPendingMessage(name, adapter.id, graph.progress));
|
|
542
859
|
}
|
|
543
860
|
if (typeof graph === "object") {
|
|
544
861
|
// Some part of the graph could not be read, so whether the name is in
|
|
@@ -549,19 +866,18 @@ export class RegistryService {
|
|
|
549
866
|
await this.refuse(context, adapter, name, code);
|
|
550
867
|
throw new RegistryError(status, code, `"${name}" could not be checked against this agent's approved ${adapter.id} dependency graph because ${source} was unavailable while resolving it; try again`);
|
|
551
868
|
}
|
|
552
|
-
await this.refuse(context, adapter, name,
|
|
553
|
-
|
|
554
|
-
? `this agent has no ${adapter.id} package allowlist`
|
|
555
|
-
: `"${name}" is not on this agent's ${adapter.id} package allowlist or in its dependency graph`;
|
|
556
|
-
throw new RegistryError(403, "wardby_package_not_allowed", hint);
|
|
869
|
+
await this.refuse(context, adapter, name, PACKAGE_NOT_ALLOWED);
|
|
870
|
+
throw new RegistryError(403, PACKAGE_NOT_ALLOWED, notAllowedMessage(name, adapter.id, entries.length > 0));
|
|
557
871
|
}
|
|
558
872
|
/** Whether `name` is in the run's approved graph for this ecosystem:
|
|
559
873
|
* the allowlist's exact roots plus the dependencies of every kept
|
|
560
874
|
* version (the same filter the metadata path applies: root range,
|
|
561
875
|
* release age, advisories), transitively. Each newly found name is
|
|
562
876
|
* recorded as an allowance as the walk goes, exactly as if its
|
|
563
|
-
* parent's metadata had been served.
|
|
564
|
-
*
|
|
877
|
+
* parent's metadata had been served. Only a download asks, or a
|
|
878
|
+
* metadata miss after a failed or partial lockfile plan (see
|
|
879
|
+
* `authorize`). A scoped wildcard root cannot be enumerated, so it is
|
|
880
|
+
* never a starting point. */
|
|
565
881
|
async resolveInGraph(adapter, context, entries, name) {
|
|
566
882
|
if (!adapter.dependenciesInMetadata)
|
|
567
883
|
return "absent";
|
|
@@ -573,13 +889,16 @@ export class RegistryService {
|
|
|
573
889
|
const edges = roots.map((root) => ({ name: root, range: "*" }));
|
|
574
890
|
walk = {
|
|
575
891
|
found: new Set(roots),
|
|
576
|
-
|
|
892
|
+
order: [],
|
|
893
|
+
pending: new Map(),
|
|
577
894
|
seen: new Set(edges.map(edgeKey)),
|
|
578
895
|
expandedVersions: new Map(),
|
|
579
896
|
exhausted: false,
|
|
580
897
|
attempts: new Map(),
|
|
581
898
|
failed: new Map(),
|
|
582
899
|
};
|
|
900
|
+
for (const edge of edges)
|
|
901
|
+
addPending(walk, edge.name, [edge]);
|
|
583
902
|
tally.graphs.set(adapter.id, walk);
|
|
584
903
|
}
|
|
585
904
|
const current = walk;
|
|
@@ -588,7 +907,11 @@ export class RegistryService {
|
|
|
588
907
|
if (current.found.has(name))
|
|
589
908
|
return "found";
|
|
590
909
|
if (current.exhausted)
|
|
591
|
-
return {
|
|
910
|
+
return {
|
|
911
|
+
cutShort: "limit",
|
|
912
|
+
progress: walkProgress(current),
|
|
913
|
+
limit: this.options.maxGraphPackages ?? DEFAULT_MAX_GRAPH_PACKAGES,
|
|
914
|
+
};
|
|
592
915
|
// Single-flight: a miss while a walk is running waits for it rather
|
|
593
916
|
// than starting a second one, then re-checks (that walk may have
|
|
594
917
|
// stopped on finding a different name). Checked before the queue: a
|
|
@@ -597,17 +920,19 @@ export class RegistryService {
|
|
|
597
920
|
// starts at most one walk of its own, so it spends at most one
|
|
598
921
|
// round of retries on failed nodes.
|
|
599
922
|
if (!current.running) {
|
|
600
|
-
const
|
|
601
|
-
if (started || (current.
|
|
923
|
+
const anyRetriable = [...current.failed.keys()].some((key) => retriable(current, key));
|
|
924
|
+
if (started || (current.pending.size === 0 && !anyRetriable))
|
|
602
925
|
return this.graphMiss(current);
|
|
603
926
|
started = true;
|
|
604
927
|
current.running = this.walkGraph(adapter, context, entries, current, name).finally(() => {
|
|
605
928
|
current.running = undefined;
|
|
606
929
|
});
|
|
607
930
|
}
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
931
|
+
const result = await current.running;
|
|
932
|
+
// Read from the walk's own snapshot, never from the live walk: a
|
|
933
|
+
// miss in the meantime may have started the next walk already.
|
|
934
|
+
if (result.end === "timeout" && !current.found.has(name))
|
|
935
|
+
return { cutShort: "timeout", progress: result.progress, frontier: result.frontier?.has(name) ?? false };
|
|
611
936
|
}
|
|
612
937
|
}
|
|
613
938
|
/** The answer for a name the walk did not find: unavailable while any
|
|
@@ -618,21 +943,42 @@ export class RegistryService {
|
|
|
618
943
|
return "absent";
|
|
619
944
|
return { unavailable: [...walk.failed.values()].some((entry) => entry.cause === "audit") ? "audit" : "upstream" };
|
|
620
945
|
}
|
|
946
|
+
/** One walk, logged once when it stops, with its progress (and, on a
|
|
947
|
+
* timeout, its frontier) taken before `running` is cleared. */
|
|
621
948
|
async walkGraph(adapter, context, entries, walk, target) {
|
|
949
|
+
const startedAt = this.now().getTime();
|
|
950
|
+
const loadsBefore = walk.loads ?? 0;
|
|
951
|
+
const end = await this.expandGraph(adapter, context, entries, walk, target);
|
|
952
|
+
const progress = walkProgress(walk);
|
|
953
|
+
registryLog.info({
|
|
954
|
+
event: "registry.graph_walk",
|
|
955
|
+
runId: context.runId,
|
|
956
|
+
ecosystem: adapter.id,
|
|
957
|
+
end,
|
|
958
|
+
walked: progress.walked,
|
|
959
|
+
queued: progress.queued,
|
|
960
|
+
queuedEdges: unexpandedEdgeCount(walk),
|
|
961
|
+
loads: (walk.loads ?? 0) - loadsBefore,
|
|
962
|
+
durationMs: this.now().getTime() - startedAt,
|
|
963
|
+
}, "coding registry graph walk stopped");
|
|
964
|
+
return end === "timeout" ? { end, progress, frontier: unexpandedNames(walk) } : { end, progress };
|
|
965
|
+
}
|
|
966
|
+
async expandGraph(adapter, context, entries, walk, target) {
|
|
622
967
|
const maxPackages = this.options.maxGraphPackages ?? DEFAULT_MAX_GRAPH_PACKAGES;
|
|
623
968
|
const deadline = this.now().getTime() + (this.options.graphTimeoutMs ?? DEFAULT_GRAPH_TIMEOUT_MS);
|
|
624
|
-
// Parked failures with attempts left go first
|
|
625
|
-
// walk's retry of them
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
969
|
+
// Parked failures with attempts left go first (a later miss is this
|
|
970
|
+
// walk's retry of them), and the requested name before everything:
|
|
971
|
+
// order changes only how soon an answer comes, never which edges are
|
|
972
|
+
// followed or what they allow.
|
|
973
|
+
const retry = [...walk.failed.entries()].filter(([name]) => retriable(walk, name));
|
|
974
|
+
for (const [name] of retry)
|
|
975
|
+
walk.failed.delete(name);
|
|
976
|
+
for (const [name, entry] of retry.reverse())
|
|
977
|
+
addPending(walk, name, entry.edges, true);
|
|
978
|
+
if (walk.pending.has(target))
|
|
979
|
+
moveToFront(walk, target);
|
|
630
980
|
const attemptsThisWalk = new Map();
|
|
631
|
-
|
|
632
|
-
// the metadata and audit cache lifetimes, so this changes no decision.
|
|
633
|
-
// A failed computation is not memoized, so its retry really retries.
|
|
634
|
-
const kept = new Map();
|
|
635
|
-
while (walk.queue.length > 0) {
|
|
981
|
+
while (walk.pending.size > 0) {
|
|
636
982
|
if (walk.found.has(target))
|
|
637
983
|
return "found";
|
|
638
984
|
if (walk.released)
|
|
@@ -645,65 +991,68 @@ export class RegistryService {
|
|
|
645
991
|
walk.exhausted = true;
|
|
646
992
|
return "limit";
|
|
647
993
|
}
|
|
648
|
-
// Each
|
|
649
|
-
// the room left can never overshoot the bound.
|
|
650
|
-
|
|
651
|
-
const
|
|
994
|
+
// Each name adds at most one new package, so a batch no larger than
|
|
995
|
+
// the room left can never overshoot the bound. A name's node takes
|
|
996
|
+
// every edge pending for it.
|
|
997
|
+
const batch = walk.order.splice(0, Math.min(GRAPH_WALK_CONCURRENCY, room)).map((name) => {
|
|
998
|
+
const edges = walk.pending.get(name) ?? [];
|
|
999
|
+
walk.pending.delete(name);
|
|
1000
|
+
return { name, edges };
|
|
1001
|
+
});
|
|
1002
|
+
const expansions = Promise.all(batch.map(({ name, edges }) => this.graphNode(adapter, context, entries, name, edges, walk)));
|
|
652
1003
|
let timer;
|
|
653
1004
|
const timedOut = new Promise((resolve) => {
|
|
654
1005
|
timer = setTimeout(() => resolve("timeout"), remaining);
|
|
655
1006
|
});
|
|
656
1007
|
const results = await Promise.race([expansions, timedOut]).finally(() => clearTimeout(timer));
|
|
657
1008
|
if (results === "timeout") {
|
|
658
|
-
// Not expanded: put the batch back so a later
|
|
659
|
-
//
|
|
660
|
-
|
|
1009
|
+
// Not expanded: put the whole batch back, in order, so a later
|
|
1010
|
+
// miss retries it (its node is likely memoized by then).
|
|
1011
|
+
for (const { name, edges } of [...batch].reverse())
|
|
1012
|
+
addPending(walk, name, edges, true);
|
|
661
1013
|
return "timeout";
|
|
662
1014
|
}
|
|
663
1015
|
const expanded = [];
|
|
664
|
-
batch.forEach((
|
|
1016
|
+
batch.forEach(({ name, edges }, index) => {
|
|
665
1017
|
const result = results[index];
|
|
666
1018
|
if (result.ok) {
|
|
667
|
-
expanded.push({
|
|
1019
|
+
expanded.push({ name, edges, selected: result.selected });
|
|
668
1020
|
return;
|
|
669
1021
|
}
|
|
670
|
-
// A transient failure is never counted as expanded: the
|
|
1022
|
+
// A transient failure is never counted as expanded: the node is
|
|
671
1023
|
// retried (once more in this walk, then by later misses) instead
|
|
672
1024
|
// of silently closing its whole subtree for the run.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
(walk.attempts.get(key) ?? 0) < MAX_GRAPH_NODE_ATTEMPTS)
|
|
678
|
-
walk.queue.push(edge);
|
|
1025
|
+
walk.attempts.set(name, (walk.attempts.get(name) ?? 0) + 1);
|
|
1026
|
+
attemptsThisWalk.set(name, (attemptsThisWalk.get(name) ?? 0) + 1);
|
|
1027
|
+
if ((attemptsThisWalk.get(name) ?? 0) < GRAPH_NODE_ATTEMPTS_PER_WALK && retriable(walk, name))
|
|
1028
|
+
addPending(walk, name, edges);
|
|
679
1029
|
else
|
|
680
|
-
walk.failed.set(
|
|
1030
|
+
walk.failed.set(name, { edges, cause: result.cause });
|
|
681
1031
|
});
|
|
682
1032
|
// A package joins the graph (and gets its allowance) only when some
|
|
683
1033
|
// kept version of it satisfies a range that reached it. Recorded
|
|
684
1034
|
// before it counts as found, so `found` never holds a name the store
|
|
685
1035
|
// would refuse.
|
|
686
|
-
const discovered =
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
.map(({ edge }) => edge.name)),
|
|
690
|
-
];
|
|
1036
|
+
const discovered = expanded
|
|
1037
|
+
.filter(({ name, selected }) => selected.length > 0 && !walk.found.has(name))
|
|
1038
|
+
.map(({ name }) => name);
|
|
691
1039
|
try {
|
|
692
1040
|
if (discovered.length > 0)
|
|
693
1041
|
await this.options.store.addAllowances(context.runId, adapter.id, discovered);
|
|
694
1042
|
}
|
|
695
1043
|
catch (error) {
|
|
696
|
-
// The store write failed: put the expanded
|
|
1044
|
+
// The store write failed: put the expanded groups back so a later
|
|
697
1045
|
// miss re-expands them, rather than leaving the graph silently short.
|
|
698
|
-
|
|
1046
|
+
for (const { name, edges } of [...expanded].reverse())
|
|
1047
|
+
addPending(walk, name, edges, true);
|
|
699
1048
|
throw error;
|
|
700
1049
|
}
|
|
701
1050
|
for (const name of discovered)
|
|
702
1051
|
walk.found.add(name);
|
|
703
|
-
for (const {
|
|
704
|
-
let versions = walk.expandedVersions.get(
|
|
1052
|
+
for (const { name, selected } of expanded) {
|
|
1053
|
+
let versions = walk.expandedVersions.get(name);
|
|
705
1054
|
if (!versions)
|
|
706
|
-
walk.expandedVersions.set(
|
|
1055
|
+
walk.expandedVersions.set(name, (versions = new Set()));
|
|
707
1056
|
for (const { version, dependencies } of selected) {
|
|
708
1057
|
if (versions.has(version))
|
|
709
1058
|
continue;
|
|
@@ -713,45 +1062,43 @@ export class RegistryService {
|
|
|
713
1062
|
if (walk.seen.has(key))
|
|
714
1063
|
continue;
|
|
715
1064
|
walk.seen.add(key);
|
|
716
|
-
|
|
1065
|
+
// A package parked after failing waits with its edges and is
|
|
1066
|
+
// retried with them.
|
|
1067
|
+
const parked = walk.failed.get(dependency.name);
|
|
1068
|
+
if (parked)
|
|
1069
|
+
parked.edges.push(dependency);
|
|
1070
|
+
else
|
|
1071
|
+
addPending(walk, dependency.name, [dependency], dependency.name === target);
|
|
717
1072
|
}
|
|
718
1073
|
}
|
|
719
1074
|
}
|
|
720
1075
|
}
|
|
721
1076
|
return walk.found.has(target) ? "found" : "complete";
|
|
722
1077
|
}
|
|
723
|
-
/** The kept versions of one package inside the
|
|
724
|
-
* one's declared dependencies, through the same metadata
|
|
725
|
-
* `keptVersions` filter the metadata path uses (a root's
|
|
726
|
-
* applies there), within the process-wide walk
|
|
727
|
-
* selects every kept version; a range no
|
|
728
|
-
* none. A package npm doesn't have, or
|
|
729
|
-
* definitively selects nothing; any other
|
|
730
|
-
* timeout, audit unavailable) is transient
|
|
731
|
-
|
|
1078
|
+
/** The kept versions of one package inside any of the ranges that reached
|
|
1079
|
+
* it, and each one's declared dependencies, through the same metadata
|
|
1080
|
+
* cache and `keptVersions` filter the metadata path uses (a root's
|
|
1081
|
+
* allowlist range applies there), within the process-wide walk
|
|
1082
|
+
* concurrency cap. A `"*"` edge selects every kept version; a range no
|
|
1083
|
+
* kept version satisfies selects none. A package npm doesn't have, or
|
|
1084
|
+
* whose metadata is too large, definitively selects nothing; any other
|
|
1085
|
+
* failure (upstream error or timeout, audit unavailable) is transient
|
|
1086
|
+
* and reported for retry. */
|
|
1087
|
+
graphNode(adapter, context, entries, name, edges, walk) {
|
|
732
1088
|
return this.walkSlots.run(async () => {
|
|
733
1089
|
try {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const { keep } = await this.keptVersions(adapter, context, meta, matchRoot(entries, edge.name));
|
|
739
|
-
return new Map([...keep].map((version) => [version, versionDependencies(adapter, meta, version)]));
|
|
740
|
-
})();
|
|
741
|
-
memo.set(edge.name, load);
|
|
742
|
-
load.catch(() => memo.delete(edge.name));
|
|
743
|
-
}
|
|
744
|
-
const keep = await load;
|
|
745
|
-
const inRange = (version) => {
|
|
746
|
-
if (edge.range === "*")
|
|
1090
|
+
const keep = await this.keptForGraph(adapter, context, entries, name, walk);
|
|
1091
|
+
const ranges = [...new Set(edges.map((edge) => edge.range))];
|
|
1092
|
+
const inRange = (version) => ranges.some((range) => {
|
|
1093
|
+
if (range === "*")
|
|
747
1094
|
return true;
|
|
748
1095
|
try {
|
|
749
|
-
return adapter.satisfies(version,
|
|
1096
|
+
return adapter.satisfies(version, range);
|
|
750
1097
|
}
|
|
751
1098
|
catch {
|
|
752
1099
|
return false;
|
|
753
1100
|
}
|
|
754
|
-
};
|
|
1101
|
+
});
|
|
755
1102
|
const selected = [];
|
|
756
1103
|
for (const [version, dependencies] of keep)
|
|
757
1104
|
if (inRange(version))
|
|
@@ -766,6 +1113,56 @@ export class RegistryService {
|
|
|
766
1113
|
}
|
|
767
1114
|
});
|
|
768
1115
|
}
|
|
1116
|
+
/** A package's kept versions for the run's walk, from the memo or
|
|
1117
|
+
* computed (and memoized). A failed computation is not memoized, so its
|
|
1118
|
+
* retry really retries. */
|
|
1119
|
+
async keptForGraph(adapter, context, entries, name, walk) {
|
|
1120
|
+
const key = `${context.runId}\0${adapter.id}\0${name}`;
|
|
1121
|
+
const now = this.now().getTime();
|
|
1122
|
+
const memoized = this.graphMemo.get(key);
|
|
1123
|
+
if (memoized) {
|
|
1124
|
+
this.dropMemo(key);
|
|
1125
|
+
if (memoized.expires > now) {
|
|
1126
|
+
this.putMemo(key, memoized); // most recently used
|
|
1127
|
+
return memoized.kept;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (walk)
|
|
1131
|
+
walk.loads = (walk.loads ?? 0) + 1;
|
|
1132
|
+
const meta = await this.metadata(adapter, name, false);
|
|
1133
|
+
const { keep } = await this.keptVersions(adapter, context, meta, matchRoot(entries, name));
|
|
1134
|
+
const kept = new Map([...keep].map((version) => [version, versionDependencies(adapter, meta, version)]));
|
|
1135
|
+
// A run released meanwhile (its walk stopped) keeps nothing here.
|
|
1136
|
+
if (this.tallies.has(context.runId))
|
|
1137
|
+
this.memoize(key, kept);
|
|
1138
|
+
return kept;
|
|
1139
|
+
}
|
|
1140
|
+
dropMemo(key) {
|
|
1141
|
+
const entry = this.graphMemo.get(key);
|
|
1142
|
+
if (!entry)
|
|
1143
|
+
return;
|
|
1144
|
+
this.graphMemo.delete(key);
|
|
1145
|
+
this.graphMemoTotal -= entry.bytes;
|
|
1146
|
+
}
|
|
1147
|
+
putMemo(key, entry) {
|
|
1148
|
+
this.graphMemo.set(key, entry);
|
|
1149
|
+
this.graphMemoTotal += entry.bytes;
|
|
1150
|
+
}
|
|
1151
|
+
/** Memoizes an expansion, least recently used evicted first, within the
|
|
1152
|
+
* byte budget. An entry larger than the whole budget is not kept. */
|
|
1153
|
+
memoize(key, kept) {
|
|
1154
|
+
const now = this.now().getTime();
|
|
1155
|
+
for (const [memoKey, entry] of this.graphMemo)
|
|
1156
|
+
if (entry.expires <= now)
|
|
1157
|
+
this.dropMemo(memoKey);
|
|
1158
|
+
this.dropMemo(key);
|
|
1159
|
+
const bytes = keptBytes(key, kept);
|
|
1160
|
+
if (bytes > this.graphMemoBytes)
|
|
1161
|
+
return;
|
|
1162
|
+
while (this.graphMemo.size > 0 && this.graphMemoTotal + bytes > this.graphMemoBytes)
|
|
1163
|
+
this.dropMemo(this.graphMemo.keys().next().value);
|
|
1164
|
+
this.putMemo(key, { expires: now + GRAPH_MEMO_TTL_MS, bytes, kept });
|
|
1165
|
+
}
|
|
769
1166
|
/** A package's metadata, cached. `render` asks for the form
|
|
770
1167
|
* `renderMetadata` can serve; a cached entry fetched without it (by the
|
|
771
1168
|
* graph walk or a download) is fetched again with it, and replaces it. */
|
|
@@ -836,11 +1233,20 @@ export class RegistryService {
|
|
|
836
1233
|
this.putCached(key, { expires: now + this.metadataTtlMs, bytes, meta });
|
|
837
1234
|
}
|
|
838
1235
|
async keptVersions(adapter, context, meta, root) {
|
|
839
|
-
const { minReleaseAgeDays } = resolvePolicy(context.policy);
|
|
1236
|
+
const { minReleaseAgeDays, acceptedAdvisories } = resolvePolicy(context.policy);
|
|
1237
|
+
// Package audit mode `full` lets non-malware blocking advisories through;
|
|
1238
|
+
// malware is withheld whatever the mode (audit.ts splitWaived).
|
|
1239
|
+
const auditAll = context.packageAudit === "full";
|
|
840
1240
|
const cutoff = this.now().getTime() - minReleaseAgeDays * DAY_MS;
|
|
1241
|
+
// The run's accepted advisories waive the blocking ones they name
|
|
1242
|
+
// (never malware); fixed for the run (snapshotted at dispatch).
|
|
1243
|
+
const accepted = new Set(acceptedAdvisories);
|
|
841
1244
|
const advisories = await this.options.audit.audit(adapter, meta.name);
|
|
842
1245
|
const keep = new Set();
|
|
843
1246
|
const keptFiles = new Set();
|
|
1247
|
+
/** Kept version -> the advisories waived to keep it (only non-empty). */
|
|
1248
|
+
const waivers = new Map();
|
|
1249
|
+
const auditAdvisories = new Map();
|
|
844
1250
|
for (const info of meta.versions.values()) {
|
|
845
1251
|
if (root?.range && !adapter.satisfies(info.version, root.range))
|
|
846
1252
|
continue;
|
|
@@ -850,13 +1256,23 @@ export class RegistryService {
|
|
|
850
1256
|
const oldEnough = info.files.filter((file) => file.publishedAt && file.publishedAt.getTime() <= cutoff);
|
|
851
1257
|
if (oldEnough.length === 0)
|
|
852
1258
|
continue;
|
|
853
|
-
if (advisories.withheld(info.version).length > 0)
|
|
1259
|
+
if (advisories.withheld(info.version, accepted, auditAll).length > 0)
|
|
854
1260
|
continue;
|
|
855
1261
|
keep.add(info.version);
|
|
1262
|
+
if (auditAll) {
|
|
1263
|
+
const audited = advisories.audited?.(info.version, accepted) ?? [];
|
|
1264
|
+
if (audited.length > 0)
|
|
1265
|
+
auditAdvisories.set(info.version, [...audited]);
|
|
1266
|
+
}
|
|
856
1267
|
for (const file of oldEnough)
|
|
857
1268
|
keptFiles.add(file.filename);
|
|
1269
|
+
if (accepted.size > 0) {
|
|
1270
|
+
const waived = advisories.waived(info.version, accepted);
|
|
1271
|
+
if (waived.length > 0)
|
|
1272
|
+
waivers.set(info.version, waived);
|
|
1273
|
+
}
|
|
858
1274
|
}
|
|
859
|
-
return { keep, keptFiles };
|
|
1275
|
+
return { keep, keptFiles, waivers, auditAdvisories };
|
|
860
1276
|
}
|
|
861
1277
|
tally(context) {
|
|
862
1278
|
const now = this.now().getTime();
|
|
@@ -896,6 +1312,10 @@ export class RegistryService {
|
|
|
896
1312
|
for (const walk of tally.graphs?.values() ?? [])
|
|
897
1313
|
walk.released = true;
|
|
898
1314
|
tally.graphs?.clear();
|
|
1315
|
+
const prefix = `${runId}\0`;
|
|
1316
|
+
for (const key of [...this.graphMemo.keys()])
|
|
1317
|
+
if (key.startsWith(prefix))
|
|
1318
|
+
this.dropMemo(key);
|
|
899
1319
|
}
|
|
900
1320
|
/** Releases every run's in-process state (timers included), e.g. when the
|
|
901
1321
|
* server shuts down. */
|
|
@@ -972,7 +1392,40 @@ export class RegistryService {
|
|
|
972
1392
|
extras.add(extra);
|
|
973
1393
|
return [...extras].sort();
|
|
974
1394
|
}
|
|
975
|
-
|
|
1395
|
+
/** One RegistryAuditRecord (the first served file of a version wins) and
|
|
1396
|
+
* its `registry.audited` log line. */
|
|
1397
|
+
async recordAudit(context, adapter, name, version, sizeBytes, audit) {
|
|
1398
|
+
const advisories = audit.advisories ?? [];
|
|
1399
|
+
await this.options.store.recordAudit({
|
|
1400
|
+
runId: context.runId,
|
|
1401
|
+
ecosystem: adapter.id,
|
|
1402
|
+
name,
|
|
1403
|
+
version,
|
|
1404
|
+
relation: audit.relation,
|
|
1405
|
+
...(audit.via ? { via: audit.via } : {}),
|
|
1406
|
+
sizeBytes,
|
|
1407
|
+
advisories,
|
|
1408
|
+
});
|
|
1409
|
+
registryLog.info({
|
|
1410
|
+
event: "registry.audited",
|
|
1411
|
+
runId: context.runId,
|
|
1412
|
+
ecosystem: adapter.id,
|
|
1413
|
+
name,
|
|
1414
|
+
version,
|
|
1415
|
+
sizeBytes,
|
|
1416
|
+
relation: audit.relation,
|
|
1417
|
+
...(audit.via ? { via: audit.via } : {}),
|
|
1418
|
+
mode: context.packageAudit,
|
|
1419
|
+
advisories: advisories.map((advisory) => advisory.id),
|
|
1420
|
+
}, "coding registry served a package under package audit mode");
|
|
1421
|
+
}
|
|
1422
|
+
async download(adapter, context, name, route, file, signal,
|
|
1423
|
+
/** The served row's reason: set only when serving it waived accepted
|
|
1424
|
+
* advisories (`accepted advisories: <ids>`). */
|
|
1425
|
+
servedReason,
|
|
1426
|
+
/** Set when package audit mode is why it is served: recorded once the
|
|
1427
|
+
* file completes, beside its served row. */
|
|
1428
|
+
audit) {
|
|
976
1429
|
const { limits, store } = this.options;
|
|
977
1430
|
const runId = context.runId;
|
|
978
1431
|
const served = await this.servedUsage(context);
|
|
@@ -1100,6 +1553,7 @@ export class RegistryService {
|
|
|
1100
1553
|
integrity: file.integrity ? `${file.integrity.algorithm}:${file.integrity.hex}` : undefined,
|
|
1101
1554
|
sizeBytes: bytes,
|
|
1102
1555
|
outcome: "served",
|
|
1556
|
+
...(servedReason ? { reason: servedReason } : {}),
|
|
1103
1557
|
});
|
|
1104
1558
|
// Counted before release() drops the reservation, so no window
|
|
1105
1559
|
// exists where neither the tally nor inFlight holds this file.
|
|
@@ -1109,7 +1563,16 @@ export class RegistryService {
|
|
|
1109
1563
|
const body = Buffer.concat(buffered);
|
|
1110
1564
|
const extras = await this.allowedExtras(adapter, context, name);
|
|
1111
1565
|
const dependencies = (await adapter.dependenciesFromFile(route, body, extras).catch(() => [])).map((dependency) => ({ name: adapter.normalizeName(dependency.name), extras: dependency.extras }));
|
|
1112
|
-
|
|
1566
|
+
const names = dependencies.map((dependency) => dependency.name);
|
|
1567
|
+
// A file's dependencies are audited too when the package is
|
|
1568
|
+
// outside the allowlist (never for one kept only past advisories).
|
|
1569
|
+
if (audit && audit.relation !== "allowed") {
|
|
1570
|
+
if (audit.relation === "top-level")
|
|
1571
|
+
await store.addAuditedAllowances(context.runId, adapter.id, [name], name);
|
|
1572
|
+
await store.addAuditedAllowances(context.runId, adapter.id, names, audit.via ?? name);
|
|
1573
|
+
}
|
|
1574
|
+
else
|
|
1575
|
+
await store.addAllowances(context.runId, adapter.id, names);
|
|
1113
1576
|
// Dependency extras are followed only below a package the run
|
|
1114
1577
|
// allows with extras (an entry naming extras, or a dependency
|
|
1115
1578
|
// such a package asked for with extras): a plain entry follows
|
|
@@ -1117,6 +1580,10 @@ export class RegistryService {
|
|
|
1117
1580
|
if (extras.length > 0)
|
|
1118
1581
|
await store.addExtraAllowances(context.runId, adapter.id, dependencies);
|
|
1119
1582
|
}
|
|
1583
|
+
// Recorded before the stream closes: a failed write errors it
|
|
1584
|
+
// like a failed served row, so nothing is installed unrecorded.
|
|
1585
|
+
if (audit)
|
|
1586
|
+
await this.recordAudit(context, adapter, name, file.version, bytes, audit);
|
|
1120
1587
|
controller.close();
|
|
1121
1588
|
}
|
|
1122
1589
|
catch (error) {
|