firedrill-run 2.2.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -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 +465 -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 +1622 -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 +61 -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,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local IPC between the Firedrill companion process and other CLI processes of
|
|
3
|
+
* the same OS user (Plan20 section 3.6).
|
|
4
|
+
*
|
|
5
|
+
* Transport
|
|
6
|
+
* - POSIX: a Unix domain socket inside the private 0700 state directory. When
|
|
7
|
+
* that path exceeds the platform sun_path limit (non-production profile
|
|
8
|
+
* directories are long), a private 0700 directory owned by this user under
|
|
9
|
+
* the OS temporary directory is used instead. Anything at the socket path
|
|
10
|
+
* that is not a stale socket owned by this user is refused, never removed.
|
|
11
|
+
* - Windows: a per-user named pipe `\\.\pipe\firedrill-<sha256(user, home, origin) prefix>`.
|
|
12
|
+
* libuv creates the first pipe instance exclusively, so a pre-created
|
|
13
|
+
* (squatted) name makes the companion refuse to start.
|
|
14
|
+
* - Frames: 4-byte big-endian length + UTF-8 JSON, at most 64 KiB, one request
|
|
15
|
+
* per connection, 5 s I/O timeout (20 s for access-token so an in-memory
|
|
16
|
+
* refresh round-trip can finish).
|
|
17
|
+
*
|
|
18
|
+
* Authentication
|
|
19
|
+
* - A random 32-byte session nonce is written to a 0600 file (exclusive create)
|
|
20
|
+
* in the private state directory and removed on shutdown.
|
|
21
|
+
* - The nonce never crosses the socket. Each request carries a fresh client
|
|
22
|
+
* challenge and HMAC-SHA256(nonce, request); each response carries
|
|
23
|
+
* HMAC-SHA256(nonce, challenge + payload). Both sides compare in constant
|
|
24
|
+
* time, so a process that only squats the endpoint learns nothing and cannot
|
|
25
|
+
* forge a response. Request challenges are single-use within a companion.
|
|
26
|
+
*
|
|
27
|
+
* Trust model (honest scope)
|
|
28
|
+
* - This protects the session against other OS users and against endpoint
|
|
29
|
+
* squatting. It is NOT a defense against malware running as the same user:
|
|
30
|
+
* such a process can read the 0600 nonce file, connect, and obtain the
|
|
31
|
+
* short-lived access token, exactly as it could read an unlocked OS store.
|
|
32
|
+
* - On Windows the nonce file relies on the user-profile ACL (no POSIX modes).
|
|
33
|
+
* - Tokens are never logged, printed or written to disk by this module.
|
|
34
|
+
*/
|
|
35
|
+
import { connect, createServer } from 'node:net';
|
|
36
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
37
|
+
import { lstat, open, readFile, unlink, chmod } from 'node:fs/promises';
|
|
38
|
+
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import { Buffer } from 'node:buffer';
|
|
41
|
+
import { CliError, API_ORIGIN } from '../http.js';
|
|
42
|
+
import { ensurePrivateDirectory, privateDirectory } from '../credentials.js';
|
|
43
|
+
import { bootstrapProcessLock } from '../bootstrap-process-lock.js';
|
|
44
|
+
export const COMPANION_PROTOCOL = 'firedrill.companion-ipc.v1';
|
|
45
|
+
export const COMPANION_MAX_FRAME_BYTES = 64 * 1024;
|
|
46
|
+
export const COMPANION_IO_TIMEOUT_MS = 5000;
|
|
47
|
+
export const COMPANION_TOKEN_TIMEOUT_MS = 20000;
|
|
48
|
+
export const COMPANION_OPERATIONS = ['status', 'access-token', 'shutdown'];
|
|
49
|
+
const PIPE_PREFIX = '\\\\.\\pipe\\';
|
|
50
|
+
const NONCE_HEX = /^[a-f0-9]{64}$/;
|
|
51
|
+
const CHALLENGE = /^[A-Za-z0-9_-]{43}$/;
|
|
52
|
+
const PROOF = /^[a-f0-9]{64}$/;
|
|
53
|
+
const ERROR_CODE = /^[a-z][a-z0-9_]{0,79}$/;
|
|
54
|
+
const unavailable = () => new CliError('companion_unavailable', 'No Firedrill companion is running for this platform and user. Nothing was requested.', 2);
|
|
55
|
+
const unsafe = () => new CliError('companion_path_unsafe', 'The companion socket or nonce path is not a private item owned by this OS user. It was left untouched and nothing was requested.', 2);
|
|
56
|
+
const unverified = () => new CliError('companion_unverified', 'The local companion could not prove it holds this session. No credential was used.', 2);
|
|
57
|
+
const protocolError = () => new CliError('companion_protocol_invalid', 'The companion exchange was malformed or oversized. No credential was used.', 2);
|
|
58
|
+
function sunPathLimit() {
|
|
59
|
+
// sun_path is 104 bytes on macOS/BSD and 108 on Linux, including the NUL.
|
|
60
|
+
return process.platform === 'linux' ? 107 : 103;
|
|
61
|
+
}
|
|
62
|
+
function windowsUser() {
|
|
63
|
+
try {
|
|
64
|
+
return userInfo().username;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return process.env.USERNAME ?? '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function endpoints() {
|
|
71
|
+
const directory = await privateDirectory();
|
|
72
|
+
const nonce = join(directory, 'companion.nonce'), lock = join(directory, 'companion.lock');
|
|
73
|
+
if (process.platform === 'win32') {
|
|
74
|
+
const digest = createHash('sha256').update('firedrill.companion-pipe.v1\0').update(windowsUser()).update('\0')
|
|
75
|
+
.update(homedir()).update('\0').update(API_ORIGIN).digest('hex').slice(0, 32);
|
|
76
|
+
return { directory, nonce, lock, socket: `${PIPE_PREFIX}firedrill-${digest}`, windows: true };
|
|
77
|
+
}
|
|
78
|
+
const primary = join(directory, 'companion.sock');
|
|
79
|
+
if (Buffer.byteLength(primary) <= sunPathLimit())
|
|
80
|
+
return { directory, nonce, lock, socket: primary, windows: false };
|
|
81
|
+
const uid = process.getuid ? process.getuid() : 0;
|
|
82
|
+
const short = join(tmpdir(), `firedrill-${uid}-${createHash('sha256').update(directory).digest('hex').slice(0, 12)}`);
|
|
83
|
+
const socket = join(short, 's');
|
|
84
|
+
if (Buffer.byteLength(socket) > sunPathLimit())
|
|
85
|
+
throw new CliError('companion_socket_path_too_long', 'The companion socket path exceeds this platform limit. Set TMPDIR to a shorter private directory. No companion endpoint was opened.', 2);
|
|
86
|
+
await ensurePrivateDirectory(short);
|
|
87
|
+
return { directory, nonce, lock, socket, windows: false };
|
|
88
|
+
}
|
|
89
|
+
function ownedByUser(stat) {
|
|
90
|
+
return !process.getuid || stat.uid === process.getuid();
|
|
91
|
+
}
|
|
92
|
+
function hmac(nonce, ...parts) {
|
|
93
|
+
const mac = createHmac('sha256', nonce);
|
|
94
|
+
for (const part of parts)
|
|
95
|
+
mac.update(part).update('\0');
|
|
96
|
+
return mac.digest('hex');
|
|
97
|
+
}
|
|
98
|
+
function sameHex(a, b) {
|
|
99
|
+
const left = Buffer.from(a, 'hex'), right = Buffer.from(b, 'hex');
|
|
100
|
+
return left.length === 32 && right.length === 32 && timingSafeEqual(left, right);
|
|
101
|
+
}
|
|
102
|
+
function encodeFrame(value) {
|
|
103
|
+
const body = Buffer.from(JSON.stringify(value), 'utf8');
|
|
104
|
+
if (body.length > COMPANION_MAX_FRAME_BYTES)
|
|
105
|
+
throw protocolError();
|
|
106
|
+
const header = Buffer.alloc(4);
|
|
107
|
+
header.writeUInt32BE(body.length, 0);
|
|
108
|
+
return Buffer.concat([header, body]);
|
|
109
|
+
}
|
|
110
|
+
/** Read exactly one length-prefixed frame. Extra bytes, oversize frames and timeouts fail. */
|
|
111
|
+
function readFrame(socket, timeoutMs, signal) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
let received = 0, expected = -1, settled = false;
|
|
115
|
+
const finish = (error, value) => {
|
|
116
|
+
if (settled)
|
|
117
|
+
return;
|
|
118
|
+
settled = true;
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
signal?.removeEventListener('abort', onAbort);
|
|
121
|
+
socket.off('data', onData);
|
|
122
|
+
socket.off('end', onEnd);
|
|
123
|
+
socket.off('error', onError);
|
|
124
|
+
socket.off('close', onEnd);
|
|
125
|
+
if (error) {
|
|
126
|
+
socket.destroy();
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
resolve(value);
|
|
131
|
+
};
|
|
132
|
+
const onAbort = () => finish(signal?.reason instanceof Error ? signal.reason : new CliError('companion_cancelled', 'The companion request was cancelled. No credential was used.', 130));
|
|
133
|
+
const onError = () => finish(unavailable());
|
|
134
|
+
const onEnd = () => finish(protocolError());
|
|
135
|
+
const onData = (chunk) => {
|
|
136
|
+
received += chunk.length;
|
|
137
|
+
chunks.push(chunk);
|
|
138
|
+
if (received > COMPANION_MAX_FRAME_BYTES + 4)
|
|
139
|
+
return finish(protocolError());
|
|
140
|
+
const all = Buffer.concat(chunks);
|
|
141
|
+
if (expected < 0 && all.length >= 4) {
|
|
142
|
+
expected = all.readUInt32BE(0);
|
|
143
|
+
if (expected < 2 || expected > COMPANION_MAX_FRAME_BYTES)
|
|
144
|
+
return finish(protocolError());
|
|
145
|
+
}
|
|
146
|
+
if (expected >= 0 && all.length >= 4 + expected) {
|
|
147
|
+
if (all.length !== 4 + expected)
|
|
148
|
+
return finish(protocolError());
|
|
149
|
+
try {
|
|
150
|
+
finish(null, JSON.parse(all.subarray(4).toString('utf8')));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
finish(protocolError());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => finish(new CliError('companion_timeout', 'The companion did not answer in time. No credential was used.', 2)), timeoutMs);
|
|
158
|
+
if (signal?.aborted)
|
|
159
|
+
return onAbort();
|
|
160
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
161
|
+
socket.on('data', onData);
|
|
162
|
+
socket.once('end', onEnd);
|
|
163
|
+
socket.once('error', onError);
|
|
164
|
+
socket.once('close', onEnd);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function readNonce(path) {
|
|
168
|
+
let stat;
|
|
169
|
+
try {
|
|
170
|
+
stat = await lstat(path);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === 'ENOENT')
|
|
174
|
+
throw unavailable();
|
|
175
|
+
throw unsafe();
|
|
176
|
+
}
|
|
177
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !ownedByUser(stat) || stat.size !== 64
|
|
178
|
+
|| process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
179
|
+
throw unsafe();
|
|
180
|
+
const text = (await readFile(path, 'utf8')).trim();
|
|
181
|
+
if (!NONCE_HEX.test(text))
|
|
182
|
+
throw unsafe();
|
|
183
|
+
return Buffer.from(text, 'hex');
|
|
184
|
+
}
|
|
185
|
+
/** True when a companion nonce exists for this platform and user (it may be stale). */
|
|
186
|
+
export async function companionPresent() {
|
|
187
|
+
const { nonce } = await endpoints();
|
|
188
|
+
try {
|
|
189
|
+
await lstat(nonce);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error.code === 'ENOENT')
|
|
194
|
+
return false;
|
|
195
|
+
throw unsafe();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function connectTo(socketPath, signal) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const socket = connect({ path: socketPath });
|
|
201
|
+
const timer = setTimeout(() => { socket.destroy(); reject(unavailable()); }, COMPANION_IO_TIMEOUT_MS);
|
|
202
|
+
const onAbort = () => { clearTimeout(timer); socket.destroy(); reject(new CliError('companion_cancelled', 'The companion request was cancelled. No credential was used.', 130)); };
|
|
203
|
+
if (signal?.aborted)
|
|
204
|
+
return onAbort();
|
|
205
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
206
|
+
socket.once('connect', () => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); resolve(socket); });
|
|
207
|
+
// Persistent listener: a reset after connect must not become an unhandled 'error'.
|
|
208
|
+
socket.on('error', () => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); socket.destroy(); reject(unavailable()); });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export async function companionRequest(op, signal) {
|
|
212
|
+
if (!COMPANION_OPERATIONS.includes(op))
|
|
213
|
+
throw protocolError();
|
|
214
|
+
signal?.throwIfAborted();
|
|
215
|
+
const where = await endpoints();
|
|
216
|
+
const nonce = await readNonce(where.nonce);
|
|
217
|
+
try {
|
|
218
|
+
if (!where.windows) {
|
|
219
|
+
let stat;
|
|
220
|
+
try {
|
|
221
|
+
stat = await lstat(where.socket);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error.code === 'ENOENT')
|
|
225
|
+
throw unavailable();
|
|
226
|
+
throw unsafe();
|
|
227
|
+
}
|
|
228
|
+
if (!stat.isSocket() || !ownedByUser(stat))
|
|
229
|
+
throw unsafe();
|
|
230
|
+
}
|
|
231
|
+
const challenge = randomBytes(32).toString('base64url');
|
|
232
|
+
const socket = await connectTo(where.socket, signal);
|
|
233
|
+
try {
|
|
234
|
+
socket.write(encodeFrame({ protocol: COMPANION_PROTOCOL, op, challenge,
|
|
235
|
+
proof: hmac(nonce, 'request', COMPANION_PROTOCOL, op, challenge) }));
|
|
236
|
+
const frame = await readFrame(socket, op === 'access-token' ? COMPANION_TOKEN_TIMEOUT_MS : COMPANION_IO_TIMEOUT_MS, signal);
|
|
237
|
+
const response = frame;
|
|
238
|
+
if (!response || typeof response !== 'object' || response.protocol !== COMPANION_PROTOCOL
|
|
239
|
+
|| typeof response.payload !== 'string' || typeof response.proof !== 'string' || !PROOF.test(response.proof))
|
|
240
|
+
throw protocolError();
|
|
241
|
+
if (!sameHex(response.proof, hmac(nonce, 'response', COMPANION_PROTOCOL, op, challenge, response.payload)))
|
|
242
|
+
throw unverified();
|
|
243
|
+
let payload;
|
|
244
|
+
try {
|
|
245
|
+
payload = JSON.parse(response.payload);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw protocolError();
|
|
249
|
+
}
|
|
250
|
+
if (!payload || typeof payload !== 'object')
|
|
251
|
+
throw protocolError();
|
|
252
|
+
if (payload.ok !== true) {
|
|
253
|
+
const error = payload.error;
|
|
254
|
+
if (!error || typeof error.code !== 'string' || !ERROR_CODE.test(error.code) || typeof error.message !== 'string'
|
|
255
|
+
|| error.message.length > 1000)
|
|
256
|
+
throw protocolError();
|
|
257
|
+
const exitCode = typeof error.exit_code === 'number' && Number.isInteger(error.exit_code) && error.exit_code >= 1 && error.exit_code <= 130 ? error.exit_code : 1;
|
|
258
|
+
throw new CliError(error.code, error.message, exitCode);
|
|
259
|
+
}
|
|
260
|
+
const result = payload.result;
|
|
261
|
+
if (!result || typeof result !== 'object')
|
|
262
|
+
throw protocolError();
|
|
263
|
+
if (op === 'access-token') {
|
|
264
|
+
if (typeof result.access_token !== 'string' || !result.access_token || result.access_token.length > 16384
|
|
265
|
+
|| !Number.isSafeInteger(result.expires_at))
|
|
266
|
+
throw protocolError();
|
|
267
|
+
return { access_token: result.access_token, expires_at: result.expires_at };
|
|
268
|
+
}
|
|
269
|
+
if (op === 'shutdown') {
|
|
270
|
+
if (result.stopped !== true)
|
|
271
|
+
throw protocolError();
|
|
272
|
+
return { stopped: true };
|
|
273
|
+
}
|
|
274
|
+
if (result.protocol !== COMPANION_PROTOCOL || (result.mode !== 'persistent' && result.mode !== 'session')
|
|
275
|
+
|| !Number.isSafeInteger(result.pid) || typeof result.started_at !== 'string' || result.origin !== API_ORIGIN
|
|
276
|
+
|| typeof result.signed_in !== 'boolean')
|
|
277
|
+
throw protocolError();
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
socket.destroy();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
nonce.fill(0);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function removeStaleSocket(path) {
|
|
289
|
+
let stat;
|
|
290
|
+
try {
|
|
291
|
+
stat = await lstat(path);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code === 'ENOENT')
|
|
295
|
+
return;
|
|
296
|
+
throw unsafe();
|
|
297
|
+
}
|
|
298
|
+
if (!stat.isSocket() || !ownedByUser(stat))
|
|
299
|
+
throw unsafe();
|
|
300
|
+
const live = await connectTo(path).then(socket => { socket.destroy(); return true; }, () => false);
|
|
301
|
+
if (live)
|
|
302
|
+
throw new CliError('companion_already_running', 'A Firedrill companion is already serving this platform and user. No second companion was started.', 2);
|
|
303
|
+
await unlink(path);
|
|
304
|
+
}
|
|
305
|
+
async function removeOwnedFile(path) {
|
|
306
|
+
let stat;
|
|
307
|
+
try {
|
|
308
|
+
stat = await lstat(path);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error.code === 'ENOENT')
|
|
312
|
+
return;
|
|
313
|
+
throw unsafe();
|
|
314
|
+
}
|
|
315
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !ownedByUser(stat))
|
|
316
|
+
throw unsafe();
|
|
317
|
+
await unlink(path);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Start the companion IPC server in this process. Holds a process-lifetime lock
|
|
321
|
+
* so only one companion serves a platform origin per user. The caller (the
|
|
322
|
+
* companion command) wires SIGINT/SIGTERM/SIGHUP to handle.close('signal').
|
|
323
|
+
*/
|
|
324
|
+
export async function startCompanionServer(options) {
|
|
325
|
+
const where = await endpoints();
|
|
326
|
+
let lock;
|
|
327
|
+
try {
|
|
328
|
+
lock = await bootstrapProcessLock(where.lock);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
throw new CliError('companion_already_running', 'A Firedrill companion is already running for this platform and user, or local coordination is unavailable. No second companion was started.', 2);
|
|
332
|
+
}
|
|
333
|
+
const nonce = randomBytes(32);
|
|
334
|
+
const startedAt = new Date().toISOString();
|
|
335
|
+
const seen = new Set();
|
|
336
|
+
const sockets = new Set();
|
|
337
|
+
let server = null;
|
|
338
|
+
let closing = null, nonceWritten = false, listening = false;
|
|
339
|
+
let resolveClosed = () => { };
|
|
340
|
+
const closed = new Promise(resolve => { resolveClosed = resolve; });
|
|
341
|
+
let started = false;
|
|
342
|
+
const close = (reason = 'shutdown') => closing ??= (async () => {
|
|
343
|
+
// A companion that never started serving keeps its process credentials;
|
|
344
|
+
// once it served, every close ends the volatile session.
|
|
345
|
+
if (started) {
|
|
346
|
+
try {
|
|
347
|
+
await options.handlers.erase();
|
|
348
|
+
}
|
|
349
|
+
catch { /* Memory-only; references are dropped by the handler. */ }
|
|
350
|
+
}
|
|
351
|
+
for (const socket of sockets)
|
|
352
|
+
socket.destroy();
|
|
353
|
+
if (server)
|
|
354
|
+
await new Promise(resolve => server?.close(() => resolve()));
|
|
355
|
+
nonce.fill(0);
|
|
356
|
+
if (nonceWritten)
|
|
357
|
+
await removeOwnedFile(where.nonce).catch(() => { });
|
|
358
|
+
// Only a socket this process bound is ours to remove; a failed start must
|
|
359
|
+
// never delete a live companion's endpoint.
|
|
360
|
+
if (!where.windows && listening) {
|
|
361
|
+
await lstat(where.socket).then(stat => stat.isSocket() && ownedByUser(stat) ? unlink(where.socket) : undefined, () => { }).catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
await lock.close().catch(() => { });
|
|
364
|
+
resolveClosed();
|
|
365
|
+
try {
|
|
366
|
+
options.onShutdown?.(reason);
|
|
367
|
+
}
|
|
368
|
+
catch { /* Caller hook must not block cleanup. */ }
|
|
369
|
+
})();
|
|
370
|
+
lock.signal.addEventListener('abort', () => { void close('lock-lost'); }, { once: true });
|
|
371
|
+
const respond = (socket, op, challenge, payload) => {
|
|
372
|
+
const text = JSON.stringify(payload);
|
|
373
|
+
socket.end(encodeFrame({ protocol: COMPANION_PROTOCOL, payload: text, proof: hmac(nonce, 'response', COMPANION_PROTOCOL, op, challenge, text) }));
|
|
374
|
+
};
|
|
375
|
+
const handle = async (socket) => {
|
|
376
|
+
sockets.add(socket);
|
|
377
|
+
socket.on('error', () => { socket.destroy(); });
|
|
378
|
+
socket.once('close', () => sockets.delete(socket));
|
|
379
|
+
let request;
|
|
380
|
+
try {
|
|
381
|
+
request = await readFrame(socket, COMPANION_IO_TIMEOUT_MS);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
socket.destroy();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const value = request;
|
|
388
|
+
if (!value || typeof value !== 'object' || value.protocol !== COMPANION_PROTOCOL || typeof value.op !== 'string'
|
|
389
|
+
|| !COMPANION_OPERATIONS.includes(value.op) || typeof value.challenge !== 'string'
|
|
390
|
+
|| !CHALLENGE.test(value.challenge) || typeof value.proof !== 'string' || !PROOF.test(value.proof)
|
|
391
|
+
|| !sameHex(value.proof, hmac(nonce, 'request', COMPANION_PROTOCOL, value.op, value.challenge))
|
|
392
|
+
|| seen.has(value.challenge)) {
|
|
393
|
+
socket.destroy();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
seen.add(value.challenge);
|
|
397
|
+
if (seen.size > 4096)
|
|
398
|
+
seen.delete(seen.values().next().value);
|
|
399
|
+
const op = value.op, challenge = value.challenge;
|
|
400
|
+
if (closing) {
|
|
401
|
+
respond(socket, op, challenge, { ok: false, error: { code: 'companion_stopping', message: 'The companion is stopping. No credential was returned.', exit_code: 2 } });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const abort = new AbortController();
|
|
405
|
+
const timer = setTimeout(() => abort.abort(), COMPANION_TOKEN_TIMEOUT_MS);
|
|
406
|
+
socket.once('close', () => abort.abort());
|
|
407
|
+
try {
|
|
408
|
+
if (op === 'status') {
|
|
409
|
+
const summary = await options.handlers.status();
|
|
410
|
+
respond(socket, op, challenge, { ok: true, result: { protocol: COMPANION_PROTOCOL, mode: options.mode, pid: process.pid,
|
|
411
|
+
started_at: startedAt, origin: API_ORIGIN, signed_in: summary.signed_in, state: summary.state,
|
|
412
|
+
subject: summary.subject, expires_at: summary.expires_at } });
|
|
413
|
+
}
|
|
414
|
+
else if (op === 'access-token') {
|
|
415
|
+
const token = await options.handlers.accessToken(abort.signal);
|
|
416
|
+
respond(socket, op, challenge, { ok: true, result: { access_token: token.access_token, expires_at: token.expires_at } });
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
socket.once('finish', () => { void close('shutdown'); });
|
|
420
|
+
socket.once('close', () => { void close('shutdown'); });
|
|
421
|
+
respond(socket, op, challenge, { ok: true, result: { stopped: true } });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
// Only this client's own CliError codes/messages cross the socket; never raw errors.
|
|
426
|
+
respond(socket, op, challenge, { ok: false, error: error instanceof CliError && ERROR_CODE.test(error.code)
|
|
427
|
+
? { code: error.code, message: error.message.slice(0, 1000), exit_code: error.exitCode }
|
|
428
|
+
: { code: 'companion_operation_failed', message: 'The companion could not complete the request. No credential was returned.', exit_code: 1 } });
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
if (!where.windows)
|
|
436
|
+
await removeStaleSocket(where.socket);
|
|
437
|
+
else {
|
|
438
|
+
const live = await connectTo(where.socket).then(socket => { socket.destroy(); return true; }, () => false);
|
|
439
|
+
if (live)
|
|
440
|
+
throw new CliError('companion_already_running', 'A Firedrill companion is already serving this platform and user. No second companion was started.', 2);
|
|
441
|
+
}
|
|
442
|
+
// We hold the companion lock and no live endpoint answered: any nonce left is stale.
|
|
443
|
+
await removeOwnedFile(where.nonce);
|
|
444
|
+
server = createServer({ allowHalfOpen: false, pauseOnConnect: false }, socket => { void handle(socket); });
|
|
445
|
+
server.maxConnections = 16;
|
|
446
|
+
await new Promise((resolve, reject) => {
|
|
447
|
+
server?.once('error', error => reject(error.code === 'EADDRINUSE'
|
|
448
|
+
? new CliError('companion_already_running', 'The companion endpoint is already in use. No second companion was started.', 2)
|
|
449
|
+
: new CliError('companion_listen_failed', 'The companion endpoint could not be opened. No local access was granted.', 2)));
|
|
450
|
+
server?.listen({ path: where.socket, readableAll: false, writableAll: false }, () => { listening = true; resolve(); });
|
|
451
|
+
});
|
|
452
|
+
server.on('error', () => { void close('exit'); });
|
|
453
|
+
if (!where.windows)
|
|
454
|
+
await chmod(where.socket, 0o600);
|
|
455
|
+
const file = await open(where.nonce, 'wx', 0o600);
|
|
456
|
+
nonceWritten = true;
|
|
457
|
+
try {
|
|
458
|
+
await file.writeFile(nonce.toString('hex'), 'utf8');
|
|
459
|
+
await file.sync();
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
await file.close();
|
|
463
|
+
}
|
|
464
|
+
if (!where.windows)
|
|
465
|
+
await chmod(where.nonce, 0o600);
|
|
466
|
+
started = true;
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
await close('exit');
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
return { endpoint: where.socket, closed, close };
|
|
473
|
+
}
|