firedrill-run 2.2.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +506 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1627 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +96 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native process supervisor (Plan20 W09).
|
|
3
|
+
*
|
|
4
|
+
* Starts the services of an approved local plan as native trusted processes,
|
|
5
|
+
* in descriptor startup order, with argv only (`shell: false`), a minimal
|
|
6
|
+
* constructed environment, loopback readiness checks and bounded redacted
|
|
7
|
+
* output. This is process supervision, not an OS sandbox: approved processes
|
|
8
|
+
* run with the user's permissions.
|
|
9
|
+
*
|
|
10
|
+
* Ownership rules:
|
|
11
|
+
* - Only processes this supervisor spawned are ever signalled, and only while
|
|
12
|
+
* their leader is still a live child of this process.
|
|
13
|
+
* - On POSIX each launched service is its own process group (detached), and
|
|
14
|
+
* stop signals the group: SIGTERM, wait, then SIGKILL.
|
|
15
|
+
* - On Windows there are no process groups here: only the started pid is
|
|
16
|
+
* tracked and stopped; its descendants may keep running (reported by
|
|
17
|
+
* `limitations()`).
|
|
18
|
+
* - Attached services are never signalled, and nothing is ever stopped by
|
|
19
|
+
* name or by port.
|
|
20
|
+
* - The service a jsonl-process.v1 driver speaks to is started and owned by
|
|
21
|
+
* that driver (it needs the process's stdin/stdout), so it is skipped here.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
25
|
+
import { request } from 'node:http';
|
|
26
|
+
import { connect } from 'node:net';
|
|
27
|
+
import { constants as osConstants } from 'node:os';
|
|
28
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
29
|
+
import { CliError } from '../http.js';
|
|
30
|
+
import { NATIVE_ID, nativeError } from './contracts.js';
|
|
31
|
+
import { ENV_NAME, assertPlanApproved, loopbackUrl, processControlLimitations } from './plan.js';
|
|
32
|
+
import { allocateLoopbackPort } from './ports.js';
|
|
33
|
+
import { RedactingCapture } from './redaction.js';
|
|
34
|
+
import { insideRoot } from './roots.js';
|
|
35
|
+
import { ownGroup, sweepGroup } from './owned-groups.js';
|
|
36
|
+
const POSIX_GROUPS = process.platform !== 'win32';
|
|
37
|
+
const KILL_WAIT_MS = 5000;
|
|
38
|
+
const PROBE_BODY_MAX_BYTES = 65_536;
|
|
39
|
+
/** An exit code, or minus the signal number for a process ended by a signal; null when it has not ended (or ended unknowably). */
|
|
40
|
+
export function exitCode(exit) {
|
|
41
|
+
if (!exit)
|
|
42
|
+
return null;
|
|
43
|
+
if (typeof exit.code === 'number' && Number.isSafeInteger(exit.code) && exit.code >= -255 && exit.code <= 255)
|
|
44
|
+
return exit.code;
|
|
45
|
+
const signals = osConstants.signals;
|
|
46
|
+
const number = exit.signal ? signals[exit.signal] : undefined;
|
|
47
|
+
return typeof number === 'number' && number > 0 && number <= 255 ? -number : null;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Bounded output ring.
|
|
51
|
+
/** Keep the last `limit` UTF-8 bytes of appended text. */
|
|
52
|
+
export class OutputRing {
|
|
53
|
+
#limit;
|
|
54
|
+
#chunks = [];
|
|
55
|
+
#bytes = 0;
|
|
56
|
+
constructor(limit) { this.#limit = Math.max(0, Math.floor(limit)); }
|
|
57
|
+
/** Append already-redacted text, dropping the oldest bytes beyond the limit. */
|
|
58
|
+
append(text) {
|
|
59
|
+
if (!text || !this.#limit)
|
|
60
|
+
return;
|
|
61
|
+
const size = Buffer.byteLength(text, 'utf8');
|
|
62
|
+
const kept = size > this.#limit ? tailBytes(text, this.#limit) : text;
|
|
63
|
+
this.#chunks.push(kept);
|
|
64
|
+
this.#bytes += Buffer.byteLength(kept, 'utf8');
|
|
65
|
+
while (this.#bytes > this.#limit && this.#chunks.length) {
|
|
66
|
+
const first = this.#chunks[0];
|
|
67
|
+
const firstBytes = Buffer.byteLength(first, 'utf8');
|
|
68
|
+
const excess = this.#bytes - this.#limit;
|
|
69
|
+
if (excess >= firstBytes) {
|
|
70
|
+
this.#chunks.shift();
|
|
71
|
+
this.#bytes -= firstBytes;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const trimmed = tailBytes(first, firstBytes - excess);
|
|
75
|
+
this.#chunks[0] = trimmed;
|
|
76
|
+
this.#bytes += Buffer.byteLength(trimmed, 'utf8') - firstBytes;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The retained tail. */
|
|
80
|
+
text() { return this.#chunks.join(''); }
|
|
81
|
+
}
|
|
82
|
+
function tailBytes(text, bytes) {
|
|
83
|
+
const buffer = Buffer.from(text, 'utf8');
|
|
84
|
+
let start = Math.max(0, buffer.length - bytes);
|
|
85
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80)
|
|
86
|
+
start++;
|
|
87
|
+
return buffer.subarray(start).toString('utf8');
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Environment construction.
|
|
91
|
+
const BASE_POSIX = ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TMPDIR', 'TZ'];
|
|
92
|
+
const BASE_WINDOWS = ['PATH', 'PATHEXT', 'SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'TEMP', 'TMP', 'USERPROFILE',
|
|
93
|
+
'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'USERNAME'];
|
|
94
|
+
/**
|
|
95
|
+
* Build a child environment from empty: platform base names (PATH, HOME, LANG,
|
|
96
|
+
* TMPDIR and equivalents), then `env_passthrough` names present in the user
|
|
97
|
+
* environment, `env_static`, dependency ports, binding/identity env and the
|
|
98
|
+
* allocated port. Descriptor secret names not supplied by a binding are
|
|
99
|
+
* stripped, so live credentials are never inherited silently. Names are
|
|
100
|
+
* case-insensitive on Windows.
|
|
101
|
+
*/
|
|
102
|
+
export function buildServiceEnv(input) {
|
|
103
|
+
const platform = input.platform ?? process.platform;
|
|
104
|
+
const userEnv = input.userEnv ?? process.env;
|
|
105
|
+
const windows = platform === 'win32';
|
|
106
|
+
const keyOf = (name) => windows ? name.toUpperCase() : name;
|
|
107
|
+
const env = new Map();
|
|
108
|
+
const set = (name, value) => {
|
|
109
|
+
if (typeof value !== 'string' || value.includes('\0') || !ENV_NAME.test(name))
|
|
110
|
+
return;
|
|
111
|
+
env.set(keyOf(name), [name, value]);
|
|
112
|
+
};
|
|
113
|
+
const lookup = (name) => {
|
|
114
|
+
if (!windows)
|
|
115
|
+
return userEnv[name];
|
|
116
|
+
const key = Object.keys(userEnv).find(candidate => candidate.toUpperCase() === name.toUpperCase());
|
|
117
|
+
return key === undefined ? undefined : userEnv[key];
|
|
118
|
+
};
|
|
119
|
+
for (const name of windows ? BASE_WINDOWS : BASE_POSIX)
|
|
120
|
+
set(name, lookup(name));
|
|
121
|
+
for (const name of input.service.env_passthrough)
|
|
122
|
+
set(name, lookup(name));
|
|
123
|
+
for (const [name, value] of Object.entries(input.service.env_static))
|
|
124
|
+
set(name, value);
|
|
125
|
+
for (const [name, port] of input.dependencyPorts ?? [])
|
|
126
|
+
set(name, String(port));
|
|
127
|
+
const supplied = new Set();
|
|
128
|
+
for (const [name, value] of Object.entries(input.bindingEnv)) {
|
|
129
|
+
set(name, value);
|
|
130
|
+
supplied.add(keyOf(name));
|
|
131
|
+
}
|
|
132
|
+
if (input.service.port_env && input.port !== null)
|
|
133
|
+
set(input.service.port_env, String(input.port));
|
|
134
|
+
for (const name of input.secretNames)
|
|
135
|
+
if (!supplied.has(keyOf(name)))
|
|
136
|
+
env.delete(keyOf(name));
|
|
137
|
+
return Object.fromEntries([...env.values()]);
|
|
138
|
+
}
|
|
139
|
+
const startFailed = (label) => new CliError('native_process_start_failed', `The approved process for ${label} could not be started. Check that its executable exists and is runnable.`);
|
|
140
|
+
/** A process this supervisor spawned and therefore may signal. */
|
|
141
|
+
class NoProcessStarted extends CliError {
|
|
142
|
+
constructor() { super('native_process_start_failed', 'The operating system did not start the approved process.'); }
|
|
143
|
+
}
|
|
144
|
+
class OwnedProcess {
|
|
145
|
+
child;
|
|
146
|
+
pid;
|
|
147
|
+
group;
|
|
148
|
+
ring;
|
|
149
|
+
exited;
|
|
150
|
+
#exit = null;
|
|
151
|
+
#captures = [];
|
|
152
|
+
constructor(child, pid, ring, exited, captures) {
|
|
153
|
+
this.child = child;
|
|
154
|
+
this.pid = pid;
|
|
155
|
+
this.group = POSIX_GROUPS;
|
|
156
|
+
this.ring = ring;
|
|
157
|
+
this.exited = exited;
|
|
158
|
+
this.#captures.push(...captures);
|
|
159
|
+
void exited.then(exit => { this.#exit = exit; });
|
|
160
|
+
}
|
|
161
|
+
/** Spawn with argv only and wait for the OS to confirm the start. */
|
|
162
|
+
static async spawn(label, spec) {
|
|
163
|
+
let child;
|
|
164
|
+
try {
|
|
165
|
+
child = spawn(spec.executable, spec.args, {
|
|
166
|
+
cwd: spec.cwd, env: spec.env, shell: false, detached: POSIX_GROUPS, windowsHide: true,
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new NoProcessStarted();
|
|
172
|
+
}
|
|
173
|
+
let spawnObserved = false;
|
|
174
|
+
child.once('spawn', () => { spawnObserved = true; });
|
|
175
|
+
// Errors after start (for example a failed signal) must not crash the companion.
|
|
176
|
+
child.on('error', () => undefined);
|
|
177
|
+
const ring = new OutputRing(spec.outputBytes);
|
|
178
|
+
const captures = [];
|
|
179
|
+
let settleExit = () => undefined;
|
|
180
|
+
const exited = new Promise(resolve => { settleExit = resolve; });
|
|
181
|
+
if (POSIX_GROUPS)
|
|
182
|
+
child.once('spawn', () => ownGroup(child.pid));
|
|
183
|
+
child.once('exit', (code, signal) => {
|
|
184
|
+
// The service ended when its leader did, whether or not we asked it to.
|
|
185
|
+
// Members it left in its group are ours and would otherwise outlive the
|
|
186
|
+
// drill unseen, so they are swept now, while the group id is still ours.
|
|
187
|
+
if (POSIX_GROUPS)
|
|
188
|
+
sweepGroup(child.pid);
|
|
189
|
+
settleExit({ code, signal });
|
|
190
|
+
});
|
|
191
|
+
const capture = (stream, observe) => {
|
|
192
|
+
if (!stream)
|
|
193
|
+
return;
|
|
194
|
+
const decoder = new StringDecoder('utf8');
|
|
195
|
+
const redactor = new RedactingCapture(spec.secrets, text => ring.append(text));
|
|
196
|
+
captures.push(redactor);
|
|
197
|
+
stream.on('data', (chunk) => {
|
|
198
|
+
const text = decoder.write(chunk);
|
|
199
|
+
if (observe) {
|
|
200
|
+
try {
|
|
201
|
+
observe(text);
|
|
202
|
+
}
|
|
203
|
+
catch { /* observer errors never affect capture */ }
|
|
204
|
+
}
|
|
205
|
+
redactor.push(text);
|
|
206
|
+
});
|
|
207
|
+
stream.once('end', () => { redactor.push(decoder.end()); redactor.flush(); });
|
|
208
|
+
stream.on('error', () => undefined);
|
|
209
|
+
};
|
|
210
|
+
capture(child.stdout, spec.onStdout);
|
|
211
|
+
capture(child.stderr);
|
|
212
|
+
try {
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
child.once('spawn', () => resolve());
|
|
215
|
+
child.once('error', reject);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
settleExit({ code: null, signal: null });
|
|
220
|
+
if (!spawnObserved && typeof child.pid !== 'number')
|
|
221
|
+
throw new NoProcessStarted();
|
|
222
|
+
throw startFailed(label);
|
|
223
|
+
}
|
|
224
|
+
if (typeof child.pid !== 'number') {
|
|
225
|
+
settleExit({ code: null, signal: null });
|
|
226
|
+
throw startFailed(label);
|
|
227
|
+
}
|
|
228
|
+
return new OwnedProcess(child, child.pid, ring, exited, captures);
|
|
229
|
+
}
|
|
230
|
+
/** True while the leader has not exited (its pid is still ours). */
|
|
231
|
+
get alive() { return this.#exit === null && this.child.exitCode === null && this.child.signalCode === null; }
|
|
232
|
+
get exit() { return this.#exit; }
|
|
233
|
+
/** Signal the owned group (POSIX) or the owned pid (Windows), only while the leader is alive. */
|
|
234
|
+
signal(signal) {
|
|
235
|
+
if (!this.alive || this.child.pid !== this.pid)
|
|
236
|
+
return;
|
|
237
|
+
if (this.group) {
|
|
238
|
+
try {
|
|
239
|
+
process.kill(-this.pid, signal);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
catch { /* fall back to the leader itself */ }
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
this.child.kill(signal);
|
|
246
|
+
}
|
|
247
|
+
catch { /* already gone */ }
|
|
248
|
+
}
|
|
249
|
+
/** Resolve true if the leader exits within `ms`. */
|
|
250
|
+
async waitExit(ms) {
|
|
251
|
+
if (!this.alive)
|
|
252
|
+
return true;
|
|
253
|
+
let timer;
|
|
254
|
+
const timeout = new Promise(resolve => { timer = setTimeout(() => resolve(false), Math.max(0, ms)); });
|
|
255
|
+
try {
|
|
256
|
+
return await Promise.race([this.exited.then(() => true), timeout]);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** SIGTERM, wait `graceMs`, then SIGKILL while still owned. Releases pipes afterwards. */
|
|
263
|
+
async terminate(graceMs) {
|
|
264
|
+
if (this.alive) {
|
|
265
|
+
this.signal('SIGTERM');
|
|
266
|
+
if (!(await this.waitExit(graceMs))) {
|
|
267
|
+
this.signal('SIGKILL');
|
|
268
|
+
await this.waitExit(KILL_WAIT_MS);
|
|
269
|
+
}
|
|
270
|
+
// The exit handler normally swept this group already. Never signal its
|
|
271
|
+
// numeric id again after ownership has been removed from the registry.
|
|
272
|
+
if (this.group && !this.alive)
|
|
273
|
+
sweepGroup(this.pid);
|
|
274
|
+
}
|
|
275
|
+
for (const capture of this.#captures)
|
|
276
|
+
capture.flush();
|
|
277
|
+
// Descendants may hold pipes open; never let them keep the companion alive.
|
|
278
|
+
for (const stream of [this.child.stdout, this.child.stderr]) {
|
|
279
|
+
try {
|
|
280
|
+
stream?.destroy();
|
|
281
|
+
}
|
|
282
|
+
catch { /* ignore */ }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Readiness.
|
|
288
|
+
function sleep(ms, signal, exited) {
|
|
289
|
+
return new Promise(resolve => {
|
|
290
|
+
const timer = setTimeout(done, ms);
|
|
291
|
+
function done() { clearTimeout(timer); signal.removeEventListener('abort', done); resolve(); }
|
|
292
|
+
signal.addEventListener('abort', done, { once: true });
|
|
293
|
+
void exited?.then(done);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/** The accepted status code and body digest, or null when the check did not pass. */
|
|
297
|
+
function probeHttp(url, expect, timeoutMs) {
|
|
298
|
+
return new Promise(resolve => {
|
|
299
|
+
let settled = false;
|
|
300
|
+
const finish = (result) => { if (!settled) {
|
|
301
|
+
settled = true;
|
|
302
|
+
resolve(result);
|
|
303
|
+
} };
|
|
304
|
+
try {
|
|
305
|
+
const req = request({
|
|
306
|
+
host: url.hostname.replace(/^\[|\]$/g, ''), port: Number(url.port), path: `${url.pathname}${url.search}`,
|
|
307
|
+
method: 'GET', agent: false, timeout: timeoutMs, headers: { accept: '*/*' },
|
|
308
|
+
}, response => {
|
|
309
|
+
const status = response.statusCode ?? 0;
|
|
310
|
+
if (!expect.includes(status)) {
|
|
311
|
+
finish(null);
|
|
312
|
+
response.resume();
|
|
313
|
+
response.destroy();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const hash = createHash('sha256');
|
|
317
|
+
let size = 0;
|
|
318
|
+
const done = () => { if (!settled)
|
|
319
|
+
finish({ status, body_sha256: hash.digest('hex') }); };
|
|
320
|
+
response.on('data', (chunk) => {
|
|
321
|
+
if (settled)
|
|
322
|
+
return;
|
|
323
|
+
size += chunk.length;
|
|
324
|
+
// Too large to attest exactly: the check still passed, with no body digest.
|
|
325
|
+
if (size > PROBE_BODY_MAX_BYTES) {
|
|
326
|
+
finish({ status, body_sha256: null });
|
|
327
|
+
response.destroy();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
hash.update(chunk);
|
|
331
|
+
});
|
|
332
|
+
response.once('end', done);
|
|
333
|
+
response.once('error', () => finish(null));
|
|
334
|
+
});
|
|
335
|
+
req.once('timeout', () => { req.destroy(); finish(null); });
|
|
336
|
+
req.once('error', () => finish(null));
|
|
337
|
+
req.end();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
finish(null);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function probeTcp(port, timeoutMs) {
|
|
345
|
+
return new Promise(resolve => {
|
|
346
|
+
let settled = false;
|
|
347
|
+
const socket = connect({ host: '127.0.0.1', port });
|
|
348
|
+
const finish = (ready) => { if (!settled) {
|
|
349
|
+
settled = true;
|
|
350
|
+
socket.destroy();
|
|
351
|
+
resolve(ready);
|
|
352
|
+
} };
|
|
353
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
354
|
+
socket.once('connect', () => finish(true));
|
|
355
|
+
socket.once('error', () => finish(false));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** Wait for readiness within the service bound and return what passed; early exit fails with `native_process_exited`. */
|
|
359
|
+
async function waitReady(target, signal) {
|
|
360
|
+
const { readiness } = target.service;
|
|
361
|
+
const deadline = Date.now() + target.service.readiness_timeout_ms;
|
|
362
|
+
let delay = 100;
|
|
363
|
+
while (true) {
|
|
364
|
+
signal.throwIfAborted();
|
|
365
|
+
if (target.process && !target.process.alive) {
|
|
366
|
+
throw nativeError('process_exited', `Service ${target.alias} exited before it became ready.`);
|
|
367
|
+
}
|
|
368
|
+
const remaining = deadline - Date.now();
|
|
369
|
+
if (remaining <= 0)
|
|
370
|
+
break;
|
|
371
|
+
const attempt = Math.max(100, Math.min(2000, remaining));
|
|
372
|
+
let ready = null;
|
|
373
|
+
if (readiness.kind === 'none')
|
|
374
|
+
ready = { kind: 'none' };
|
|
375
|
+
else if (readiness.kind === 'stdout')
|
|
376
|
+
ready = target.stdoutSeen() ? { kind: 'stdout' } : null;
|
|
377
|
+
else if (readiness.kind === 'http' && target.url) {
|
|
378
|
+
const probed = await probeHttp(target.url, readiness.expect_status, attempt);
|
|
379
|
+
ready = probed === null ? null : { kind: 'http', status_code: probed.status, body_sha256: probed.body_sha256 };
|
|
380
|
+
}
|
|
381
|
+
else if (readiness.kind === 'tcp' && target.port)
|
|
382
|
+
ready = await probeTcp(target.port, attempt) ? { kind: 'tcp' } : null;
|
|
383
|
+
if (ready) {
|
|
384
|
+
if (target.process && !target.process.alive) {
|
|
385
|
+
throw nativeError('process_exited', `Service ${target.alias} exited before it became ready.`);
|
|
386
|
+
}
|
|
387
|
+
return ready;
|
|
388
|
+
}
|
|
389
|
+
await sleep(Math.min(delay, Math.max(0, deadline - Date.now())), signal, target.process?.exited ?? null);
|
|
390
|
+
delay = Math.min(1000, delay * 2);
|
|
391
|
+
}
|
|
392
|
+
throw nativeError('readiness_timeout', `Service ${target.alias} did not become ready within its readiness timeout.`);
|
|
393
|
+
}
|
|
394
|
+
const canceled = () => new CliError('canceled', 'Service startup was canceled. Services started for it were stopped.', 130);
|
|
395
|
+
const closedError = () => new CliError('native_supervisor_closed', 'Local services are shutting down. No new service was started.');
|
|
396
|
+
/**
|
|
397
|
+
* Starts, tracks and stops the services of one approved plan for a companion
|
|
398
|
+
* session. Shared services start once and are reused; per-drill services get
|
|
399
|
+
* one copy (and port) per drill key.
|
|
400
|
+
*/
|
|
401
|
+
export class Supervisor {
|
|
402
|
+
#plan;
|
|
403
|
+
#normalized;
|
|
404
|
+
#approval;
|
|
405
|
+
#roots;
|
|
406
|
+
#maxProcesses;
|
|
407
|
+
#outputBytes;
|
|
408
|
+
#services;
|
|
409
|
+
#secretNames;
|
|
410
|
+
#dependencies;
|
|
411
|
+
#driverOwnedAlias;
|
|
412
|
+
#entries = new Map();
|
|
413
|
+
#pending = new Map();
|
|
414
|
+
#byHandle = new WeakMap();
|
|
415
|
+
#order = [];
|
|
416
|
+
/** Per-drill processes launched for each drill key (ready or not), until the drill's close reads them. */
|
|
417
|
+
#drillLaunches = new Map();
|
|
418
|
+
#closing = new AbortController();
|
|
419
|
+
#runOwned;
|
|
420
|
+
#runLaunchId;
|
|
421
|
+
#beforeRunLaunch;
|
|
422
|
+
#captureRunObserved;
|
|
423
|
+
#runNotStarted;
|
|
424
|
+
/** The one launch of each run-owned service, kept after it stops so its end can be reported. */
|
|
425
|
+
#runLaunched = new Map();
|
|
426
|
+
#reserved = 0;
|
|
427
|
+
#stopAll = null;
|
|
428
|
+
#stopAllDone = false;
|
|
429
|
+
constructor(options) {
|
|
430
|
+
if (!Number.isSafeInteger(options.maxProcesses) || options.maxProcesses < 1 || options.maxProcesses > 1024
|
|
431
|
+
|| !Number.isSafeInteger(options.outputBytes) || options.outputBytes < 0 || options.outputBytes > 64 * 1024 * 1024) {
|
|
432
|
+
throw new CliError('native_supervisor_invalid', 'The local process limits are not valid. Nothing was started.', 2);
|
|
433
|
+
}
|
|
434
|
+
assertPlanApproved(options.approval, options.plan, options.approval.plan_sha256);
|
|
435
|
+
this.#plan = options.plan;
|
|
436
|
+
this.#normalized = options.normalized;
|
|
437
|
+
this.#approval = options.approval;
|
|
438
|
+
this.#roots = options.roots;
|
|
439
|
+
this.#maxProcesses = options.maxProcesses;
|
|
440
|
+
this.#outputBytes = options.outputBytes;
|
|
441
|
+
this.#services = new Map(options.plan.services.map(service => [service.alias, service]));
|
|
442
|
+
this.#secretNames = new Map(options.normalized.descriptor.services.map(service => [service.alias, new Set(service.configuration.filter(item => item.secret).map(item => item.name))]));
|
|
443
|
+
this.#dependencies = new Map(options.normalized.descriptor.services.map(service => [service.alias, service.depends_on]));
|
|
444
|
+
this.#driverOwnedAlias = options.plan.driver.profile === 'jsonl-process.v1' ? options.plan.driver.service_alias : null;
|
|
445
|
+
this.#runOwned = options.runOwned ?? new Set();
|
|
446
|
+
this.#runLaunchId = options.runLaunchId ?? (() => null);
|
|
447
|
+
this.#beforeRunLaunch = options.beforeRunLaunch;
|
|
448
|
+
this.#captureRunObserved = options.captureRunObserved;
|
|
449
|
+
this.#runNotStarted = options.runNotStarted;
|
|
450
|
+
for (const alias of this.#runOwned) {
|
|
451
|
+
const service = this.#services.get(alias);
|
|
452
|
+
if (!service || service.mode !== 'launch' || service.per_drill || alias === this.#driverOwnedAlias) {
|
|
453
|
+
throw new CliError('native_supervisor_invalid', `Service ${alias} cannot belong to a campaign's local run. Nothing was started.`, 2);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Each run-owned service as observed now: never launched (`planned`), its one
|
|
459
|
+
* launch still running, or ended with a known exit (`reaped`). A launch whose
|
|
460
|
+
* end cannot be attested (still alive after every service was stopped, or an
|
|
461
|
+
* exit with neither code nor known signal) is `unknown`, never `reaped`.
|
|
462
|
+
*/
|
|
463
|
+
runStates() {
|
|
464
|
+
const states = new Map();
|
|
465
|
+
for (const alias of this.#runOwned) {
|
|
466
|
+
const handle = this.#runLaunched.get(alias);
|
|
467
|
+
if (!handle) {
|
|
468
|
+
states.set(alias, { state: 'planned' });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const exit = handle.exit();
|
|
472
|
+
const code = exitCode(exit);
|
|
473
|
+
if (exit === null)
|
|
474
|
+
states.set(alias, this.#stopAllDone ? { state: 'unknown' } : { state: 'running' });
|
|
475
|
+
else
|
|
476
|
+
states.set(alias, code === null ? { state: 'unknown' } : { state: 'reaped', exit_code: code });
|
|
477
|
+
}
|
|
478
|
+
return states;
|
|
479
|
+
}
|
|
480
|
+
/** Known platform limitations of process control, for status output. */
|
|
481
|
+
limitations() {
|
|
482
|
+
return processControlLimitations();
|
|
483
|
+
}
|
|
484
|
+
/** Current services, without environment, argv or output. */
|
|
485
|
+
status() {
|
|
486
|
+
return this.#order.map(entry => ({
|
|
487
|
+
alias: entry.alias, mode: entry.handle.mode, drillKey: entry.drillKey, pid: entry.handle.pid, port: entry.handle.port,
|
|
488
|
+
running: entry.process ? entry.process.alive : entry.stopReason === null, stop_reason: entry.stopReason,
|
|
489
|
+
exit: entry.process?.exit ?? null,
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Start (or reuse) the named services and their dependencies in descriptor
|
|
494
|
+
* startup order, waiting for each to be ready. Returns handles in that order.
|
|
495
|
+
* The jsonl-process.v1 driver's own service is skipped (the driver starts it).
|
|
496
|
+
* On failure or cancellation, per-drill services started by this call are
|
|
497
|
+
* stopped before the error is rethrown.
|
|
498
|
+
*/
|
|
499
|
+
async start(services, ctx) {
|
|
500
|
+
if (this.#stopAll)
|
|
501
|
+
throw closedError();
|
|
502
|
+
assertPlanApproved(this.#approval, this.#plan, this.#approval.plan_sha256);
|
|
503
|
+
if (typeof ctx.drillKey !== 'string' || !NATIVE_ID.test(ctx.drillKey)
|
|
504
|
+
|| Object.entries(ctx.env).some(([name, value]) => !ENV_NAME.test(name) || typeof value !== 'string' || value.includes('\0'))) {
|
|
505
|
+
throw new CliError('native_start_invalid', 'The service start request is not valid. Nothing was started.', 2);
|
|
506
|
+
}
|
|
507
|
+
const wanted = new Set();
|
|
508
|
+
const visit = (alias) => {
|
|
509
|
+
if (wanted.has(alias))
|
|
510
|
+
return;
|
|
511
|
+
if (!this.#services.has(alias))
|
|
512
|
+
throw new CliError('native_start_invalid', 'A requested service is not part of the approved plan. Nothing was started.', 2);
|
|
513
|
+
wanted.add(alias);
|
|
514
|
+
for (const dependency of this.#dependencies.get(alias) ?? [])
|
|
515
|
+
visit(dependency);
|
|
516
|
+
};
|
|
517
|
+
for (const alias of services)
|
|
518
|
+
visit(alias);
|
|
519
|
+
const order = this.#normalized.startup_order.filter(alias => wanted.has(alias) && alias !== this.#driverOwnedAlias);
|
|
520
|
+
const signal = AbortSignal.any([ctx.signal, this.#closing.signal]);
|
|
521
|
+
const handles = new Map();
|
|
522
|
+
const createdHere = [];
|
|
523
|
+
try {
|
|
524
|
+
for (const alias of order) {
|
|
525
|
+
if (signal.aborted)
|
|
526
|
+
throw this.#stopAll ? closedError() : canceled();
|
|
527
|
+
const entry = await this.#obtain(alias, ctx, signal, handles, createdHere);
|
|
528
|
+
handles.set(alias, entry.handle);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
for (const entry of createdHere.reverse()) {
|
|
533
|
+
if (entry.drillKey !== null)
|
|
534
|
+
await this.#stopEntry(entry, 'start_failed').catch(() => undefined);
|
|
535
|
+
}
|
|
536
|
+
if (signal.aborted && !(error instanceof CliError && error.code !== 'canceled'))
|
|
537
|
+
throw this.#stopAll ? closedError() : canceled();
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
return order.map(alias => handles.get(alias));
|
|
541
|
+
}
|
|
542
|
+
async #obtain(alias, ctx, signal, started, createdHere) {
|
|
543
|
+
const service = this.#services.get(alias);
|
|
544
|
+
const key = service.per_drill ? `${alias}\0${ctx.drillKey}` : alias;
|
|
545
|
+
const existing = this.#entries.get(key);
|
|
546
|
+
if (existing && existing.stopReason === null && (!existing.process || existing.process.alive))
|
|
547
|
+
return existing;
|
|
548
|
+
// A shared service that exited is released (pipes closed) and started afresh as a new handle.
|
|
549
|
+
if (existing)
|
|
550
|
+
await this.#stopEntry(existing, existing.stopReason ?? 'exited');
|
|
551
|
+
// A start already in flight is shared (for a run-owned service, that is its one launch).
|
|
552
|
+
const pending = this.#pending.get(key);
|
|
553
|
+
if (pending)
|
|
554
|
+
return pending;
|
|
555
|
+
// A run-owned service has one registered launch identity for the whole run: it is never started twice.
|
|
556
|
+
if (this.#runOwned.has(alias) && this.#runLaunched.has(alias)) {
|
|
557
|
+
throw new CliError('native_local_run_service_ended', `Shared service ${alias} ended, and this campaign's local run cannot start it again under its registered identity. No drill step was taken.`);
|
|
558
|
+
}
|
|
559
|
+
const promise = (service.mode === 'attach'
|
|
560
|
+
? this.#attach(key, service, signal)
|
|
561
|
+
// A shared service outlives any one drill, so it never receives a drill's
|
|
562
|
+
// binding env or secrets, even when a drill's start (re)launches it.
|
|
563
|
+
: this.#launch(key, service, service.per_drill ? ctx : { ...ctx, env: {}, secrets: [] }, signal, started))
|
|
564
|
+
.then(entry => { createdHere.push(entry); return entry; });
|
|
565
|
+
this.#pending.set(key, promise);
|
|
566
|
+
try {
|
|
567
|
+
return await promise;
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
this.#pending.delete(key);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
#fixedPort(service) {
|
|
574
|
+
const readiness = service.readiness;
|
|
575
|
+
if (readiness.kind === 'tcp' && readiness.port > 0)
|
|
576
|
+
return readiness.port;
|
|
577
|
+
if (readiness.kind === 'http') {
|
|
578
|
+
const url = loopbackUrl(readiness.url);
|
|
579
|
+
if (url && url.port && url.port !== '0')
|
|
580
|
+
return Number(url.port);
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
#readinessUrl(service, port) {
|
|
585
|
+
if (service.readiness.kind !== 'http')
|
|
586
|
+
return null;
|
|
587
|
+
const url = loopbackUrl(service.readiness.url);
|
|
588
|
+
if (!url)
|
|
589
|
+
return null;
|
|
590
|
+
if (url.port === '0' && port !== null)
|
|
591
|
+
url.port = String(port);
|
|
592
|
+
return url;
|
|
593
|
+
}
|
|
594
|
+
#register(entry) {
|
|
595
|
+
this.#entries.set(entry.key, entry);
|
|
596
|
+
this.#byHandle.set(entry.handle, entry);
|
|
597
|
+
this.#order.push(entry);
|
|
598
|
+
}
|
|
599
|
+
#forget(entry) {
|
|
600
|
+
if (this.#entries.get(entry.key) === entry)
|
|
601
|
+
this.#entries.delete(entry.key);
|
|
602
|
+
const index = this.#order.indexOf(entry);
|
|
603
|
+
if (index >= 0)
|
|
604
|
+
this.#order.splice(index, 1);
|
|
605
|
+
}
|
|
606
|
+
async #attach(key, service, signal) {
|
|
607
|
+
const port = this.#fixedPort(service);
|
|
608
|
+
const url = this.#readinessUrl(service, port);
|
|
609
|
+
let release = () => undefined;
|
|
610
|
+
const exited = new Promise(resolve => { release = resolve; });
|
|
611
|
+
let observed = null;
|
|
612
|
+
const handle = Object.freeze({
|
|
613
|
+
alias: service.alias, mode: 'attach', launchId: randomUUID(), drillKey: null, pid: null, port,
|
|
614
|
+
url: url ? `${url.protocol}//${url.host}` : port !== null ? `http://127.0.0.1:${port}` : null,
|
|
615
|
+
output: () => '', exited, exit: () => null, ready: () => observed,
|
|
616
|
+
});
|
|
617
|
+
// Readiness only: an attached service is never started, signalled or stopped here.
|
|
618
|
+
observed = await waitReady({ alias: service.alias, service, url, port, process: null, stdoutSeen: () => false }, signal);
|
|
619
|
+
const entry = { key, alias: service.alias, service, drillKey: null, handle, process: null, release, stopReason: null, stopping: null };
|
|
620
|
+
this.#register(entry);
|
|
621
|
+
return entry;
|
|
622
|
+
}
|
|
623
|
+
async #launch(key, service, ctx, signal, started) {
|
|
624
|
+
if (this.#reserved >= this.#maxProcesses) {
|
|
625
|
+
throw new CliError('native_process_limit', `The local process limit is reached, so service ${service.alias} was not started.`);
|
|
626
|
+
}
|
|
627
|
+
const runOwned = this.#runOwned.has(service.alias);
|
|
628
|
+
const registered = runOwned ? this.#runLaunchId(service.alias) : null;
|
|
629
|
+
if (runOwned && (typeof registered !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(registered))) {
|
|
630
|
+
throw new CliError('native_local_run_unregistered', `Shared service ${service.alias} was not started: this campaign's local run is not registered with the service yet.`);
|
|
631
|
+
}
|
|
632
|
+
this.#reserved++;
|
|
633
|
+
let reservedReleased = false;
|
|
634
|
+
const unreserve = () => { if (!reservedReleased) {
|
|
635
|
+
reservedReleased = true;
|
|
636
|
+
this.#reserved--;
|
|
637
|
+
} };
|
|
638
|
+
let owned = null;
|
|
639
|
+
let launchIntent = false;
|
|
640
|
+
let spawnCalled = false;
|
|
641
|
+
let registeredEntry = null;
|
|
642
|
+
try {
|
|
643
|
+
const command = service.command;
|
|
644
|
+
const root = this.#roots.get(service.root_alias);
|
|
645
|
+
if (!command || !root)
|
|
646
|
+
throw new CliError('native_start_invalid', `Service ${service.alias} has no approved command or root in this session.`, 2);
|
|
647
|
+
const cwd = await insideRoot(root, service.cwd);
|
|
648
|
+
const port = service.port_env ? await allocateLoopbackPort(signal) : this.#fixedPort(service);
|
|
649
|
+
const dependencyPorts = [];
|
|
650
|
+
for (const dependency of this.#dependencies.get(service.alias) ?? []) {
|
|
651
|
+
const handle = started.get(dependency), plan = this.#services.get(dependency);
|
|
652
|
+
if (handle?.port && plan?.port_env)
|
|
653
|
+
dependencyPorts.push([plan.port_env, handle.port]);
|
|
654
|
+
}
|
|
655
|
+
const env = buildServiceEnv({
|
|
656
|
+
service, secretNames: this.#secretNames.get(service.alias) ?? new Set(), bindingEnv: ctx.env, port, dependencyPorts,
|
|
657
|
+
});
|
|
658
|
+
const pattern = service.readiness.kind === 'stdout' ? service.readiness.pattern : null;
|
|
659
|
+
let window = '', seen = false;
|
|
660
|
+
const onStdout = pattern === null ? undefined : (text) => {
|
|
661
|
+
if (seen)
|
|
662
|
+
return;
|
|
663
|
+
window = (window + text).slice(-(pattern.length + 65_536));
|
|
664
|
+
if (window.includes(pattern))
|
|
665
|
+
seen = true;
|
|
666
|
+
else
|
|
667
|
+
window = window.slice(-(pattern.length - 1 || 1));
|
|
668
|
+
};
|
|
669
|
+
signal.throwIfAborted();
|
|
670
|
+
if (runOwned) {
|
|
671
|
+
if (!this.#beforeRunLaunch || !this.#captureRunObserved || !this.#runNotStarted)
|
|
672
|
+
throw new CliError('native_local_run_unregistered', 'Shared services require a durable original launch journal. Nothing was started.');
|
|
673
|
+
await this.#beforeRunLaunch(service.alias);
|
|
674
|
+
launchIntent = true;
|
|
675
|
+
signal.throwIfAborted();
|
|
676
|
+
}
|
|
677
|
+
spawnCalled = true;
|
|
678
|
+
owned = await OwnedProcess.spawn(`service ${service.alias}`, {
|
|
679
|
+
executable: command.executable, args: command.args, cwd, env, outputBytes: this.#outputBytes,
|
|
680
|
+
secrets: ctx.secrets, onStdout,
|
|
681
|
+
});
|
|
682
|
+
const proc = owned;
|
|
683
|
+
void proc.exited.then(unreserve);
|
|
684
|
+
const url = this.#readinessUrl(service, port);
|
|
685
|
+
let observed = null;
|
|
686
|
+
const handle = Object.freeze({
|
|
687
|
+
alias: service.alias, mode: 'launch', launchId: registered ?? randomUUID(), drillKey: service.per_drill ? ctx.drillKey : null,
|
|
688
|
+
pid: proc.pid, port,
|
|
689
|
+
url: url ? `${url.protocol}//${url.host}` : port !== null ? `http://127.0.0.1:${port}` : null,
|
|
690
|
+
output: () => proc.ring.text(), exited: proc.exited, exit: () => proc.exit, ready: () => observed,
|
|
691
|
+
});
|
|
692
|
+
// Spawned: from now on this run-owned launch is reported until the session ends, ready or not.
|
|
693
|
+
if (runOwned)
|
|
694
|
+
this.#runLaunched.set(service.alias, handle);
|
|
695
|
+
const entry = {
|
|
696
|
+
key, alias: service.alias, service, drillKey: handle.drillKey, handle, process: proc, release: () => undefined, stopReason: null, stopping: null,
|
|
697
|
+
};
|
|
698
|
+
this.#register(entry);
|
|
699
|
+
registeredEntry = entry;
|
|
700
|
+
if (runOwned) {
|
|
701
|
+
void proc.exited.then(() => this.#captureRunObserved?.()).catch(() => undefined);
|
|
702
|
+
await this.#captureRunObserved?.();
|
|
703
|
+
}
|
|
704
|
+
// Logged before readiness: a per-drill process that never became ready was still launched and is reaped below.
|
|
705
|
+
if (handle.drillKey !== null)
|
|
706
|
+
this.#drillLaunches.set(handle.drillKey, [...(this.#drillLaunches.get(handle.drillKey) ?? []), handle]);
|
|
707
|
+
try {
|
|
708
|
+
if (this.#stopAll)
|
|
709
|
+
throw closedError();
|
|
710
|
+
observed = await waitReady({ alias: service.alias, service, url, port, process: proc, stdoutSeen: () => seen }, signal);
|
|
711
|
+
if (this.#stopAll)
|
|
712
|
+
throw closedError();
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
await this.#stopEntry(entry, 'readiness_failed');
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
return entry;
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
if (!owned) {
|
|
722
|
+
unreserve();
|
|
723
|
+
if (runOwned && launchIntent && (!spawnCalled || error instanceof NoProcessStarted)) {
|
|
724
|
+
await this.#runNotStarted?.(service.alias);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
if (registeredEntry)
|
|
729
|
+
await this.#stopEntry(registeredEntry, 'launch_failed');
|
|
730
|
+
else
|
|
731
|
+
await owned.terminate(service.stop_timeout_ms);
|
|
732
|
+
if (runOwned)
|
|
733
|
+
await this.#captureRunObserved?.().catch(() => undefined);
|
|
734
|
+
}
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async #stopEntry(entry, reason) {
|
|
739
|
+
if (entry.stopping)
|
|
740
|
+
return entry.stopping;
|
|
741
|
+
entry.stopReason ??= reason;
|
|
742
|
+
entry.stopping = (async () => {
|
|
743
|
+
if (entry.process)
|
|
744
|
+
await entry.process.terminate(entry.service.stop_timeout_ms);
|
|
745
|
+
else
|
|
746
|
+
entry.release({ code: null, signal: null });
|
|
747
|
+
this.#forget(entry);
|
|
748
|
+
})();
|
|
749
|
+
return entry.stopping;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Stop one service. A launched service gets SIGTERM to its owned group, then
|
|
753
|
+
* SIGKILL after `stop_timeout_ms` if its leader is still alive. An attached
|
|
754
|
+
* service is only released (never signalled). Handles from another supervisor
|
|
755
|
+
* are ignored.
|
|
756
|
+
*/
|
|
757
|
+
async stop(handle, reason) {
|
|
758
|
+
const entry = this.#byHandle.get(handle);
|
|
759
|
+
if (!entry)
|
|
760
|
+
return;
|
|
761
|
+
await this.#stopEntry(entry, typeof reason === 'string' && reason ? reason.slice(0, 64) : 'requested');
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Every per-drill process launched for `drillKey`, including one stopped
|
|
765
|
+
* because it never became ready, in launch order; returned once, for the
|
|
766
|
+
* drill's signed close (call after stopDrill).
|
|
767
|
+
*/
|
|
768
|
+
takeDrillLaunches(drillKey) {
|
|
769
|
+
const launches = this.#drillLaunches.get(drillKey) ?? [];
|
|
770
|
+
this.#drillLaunches.delete(drillKey);
|
|
771
|
+
return launches;
|
|
772
|
+
}
|
|
773
|
+
/** Stop every per-drill service of `drillKey`, in reverse start order. */
|
|
774
|
+
async stopDrill(drillKey, reason = 'drill_finished') {
|
|
775
|
+
for (const entry of [...this.#order].reverse()) {
|
|
776
|
+
if (entry.drillKey === drillKey)
|
|
777
|
+
await this.#stopEntry(entry, reason).catch(() => undefined);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Refuse new starts, cancel pending readiness waits and stop every service in
|
|
782
|
+
* reverse start order. Idempotent: repeated calls return the same promise, so
|
|
783
|
+
* signal handlers can call it once safely.
|
|
784
|
+
*/
|
|
785
|
+
stopAll(reason = 'shutdown') {
|
|
786
|
+
this.#stopAll ??= (async () => {
|
|
787
|
+
this.#closing.abort();
|
|
788
|
+
await Promise.allSettled([...this.#pending.values()]);
|
|
789
|
+
for (const entry of [...this.#order].reverse())
|
|
790
|
+
await this.#stopEntry(entry, reason).catch(() => undefined);
|
|
791
|
+
this.#stopAllDone = true;
|
|
792
|
+
})();
|
|
793
|
+
return this.#stopAll;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Run one approved argv to completion as an owned process (own group on POSIX),
|
|
798
|
+
* with a constructed environment, a hard timeout and redacted bounded output.
|
|
799
|
+
* On timeout or abort the owned group gets SIGTERM, then SIGKILL after a short
|
|
800
|
+
* grace. A zero exit is reported evidence, not proof of any effect.
|
|
801
|
+
*/
|
|
802
|
+
export async function runOwnedCommand(input) {
|
|
803
|
+
input.signal.throwIfAborted();
|
|
804
|
+
if (/\.(?:bat|cmd)$/i.test(input.executable.trim()))
|
|
805
|
+
throw startFailed('the command');
|
|
806
|
+
const owned = await OwnedProcess.spawn('the command', {
|
|
807
|
+
executable: input.executable, args: input.args, cwd: input.cwd, env: input.env,
|
|
808
|
+
outputBytes: input.outputBytes, secrets: input.secrets,
|
|
809
|
+
});
|
|
810
|
+
let timedOut = false;
|
|
811
|
+
const stop = () => { void owned.terminate(2000); };
|
|
812
|
+
const timer = setTimeout(() => { timedOut = true; stop(); }, Math.max(1, input.timeoutMs));
|
|
813
|
+
input.signal.addEventListener('abort', stop, { once: true });
|
|
814
|
+
try {
|
|
815
|
+
const exit = await owned.exited;
|
|
816
|
+
// Let buffered output drain briefly before releasing the pipes.
|
|
817
|
+
if (owned.child.stdout && !owned.child.stdout.readableEnded) {
|
|
818
|
+
await new Promise(resolve => {
|
|
819
|
+
const timer = setTimeout(done, 500);
|
|
820
|
+
function done() { clearTimeout(timer); resolve(); }
|
|
821
|
+
owned.child.once('close', done);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
await owned.terminate(0);
|
|
825
|
+
return { code: exit.code, signal: exit.signal, timed_out: timedOut, output: owned.ring.text() };
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
clearTimeout(timer);
|
|
829
|
+
input.signal.removeEventListener('abort', stop);
|
|
830
|
+
}
|
|
831
|
+
}
|