firedrill-run 2.2.4 → 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,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native companion session (Plan20 W09, spec 3.5).
|
|
3
|
+
*
|
|
4
|
+
* `runCompanion(options)` runs one companion for an approved local plan:
|
|
5
|
+
* - verifies the in-memory approval, descriptor digest, plan roots and pinned
|
|
6
|
+
* files before anything opens;
|
|
7
|
+
* - opens the cloud session (reconnecting with capped exponential backoff and
|
|
8
|
+
* jitter on transient failures) and heartbeats at the server interval with jitter;
|
|
9
|
+
* - follows one selected campaign over root's single companion channel for this
|
|
10
|
+
* device (companion-channel.ts): its invalidation hints wake the offer loop
|
|
11
|
+
* and the drills, so nothing polls root per drill or on an idle timer, and
|
|
12
|
+
* nothing is read or started while the channel waits for a valid resync;
|
|
13
|
+
* - takes the campaign's offers made to its runner device (or requests them
|
|
14
|
+
* for its waiting slots) and accepts only those whose descriptor digest
|
|
15
|
+
* matches the approved plan and whose offer and deadline are live, with
|
|
16
|
+
* bounded concurrency (`effectiveParallelism`, 1 when any state domain is serial);
|
|
17
|
+
* an offer it will not run is rejected so the drill is not left waiting;
|
|
18
|
+
* - runs an accepted drill with the driver from the approved local plan and
|
|
19
|
+
* this client's own step and evidence ceilings, bounded by root's deadline;
|
|
20
|
+
* simulated dependencies go through the drill's loopback proxy, and identity
|
|
21
|
+
* stays empty until root delivers it;
|
|
22
|
+
* - pauses new steps while disconnected (connection gate) and never replays an
|
|
23
|
+
* in-flight step after reconnecting (the drill runner keeps it `unknown`);
|
|
24
|
+
* - when the plan keeps shared services running across drills, keeps the
|
|
25
|
+
* selected campaign's local run with root (local-run.ts): reconciled at start,
|
|
26
|
+
* registered before the first offer, takes only that campaign's offers, and
|
|
27
|
+
* closed after every service stopped, before the session closes;
|
|
28
|
+
* - on revocation stops admitting work and drains; on the caller's signal stops
|
|
29
|
+
* admitting, lets in-flight steps finish within their bounds (hard abort after
|
|
30
|
+
* a grace period), stops every supervised process once, closes the session and
|
|
31
|
+
* removes run scratch.
|
|
32
|
+
*
|
|
33
|
+
* Cloud messages reference the plan only by digest; no command, argv or path is
|
|
34
|
+
* accepted from them. Approval stays in memory for this process.
|
|
35
|
+
*/
|
|
36
|
+
import { EVIDENCE_MAX_TOTAL_BYTES, NATIVE_ID, nativeError } from './contracts.js';
|
|
37
|
+
import { normalizeDescriptor } from './canonical.js';
|
|
38
|
+
import { assertPlanApproved, verifyPlanFiles } from './plan.js';
|
|
39
|
+
import { resolvePlanRoots } from './roots.js';
|
|
40
|
+
import { effectiveParallelism, planStateActions } from './target-state.js';
|
|
41
|
+
import { readinessDomains } from './readiness.js';
|
|
42
|
+
import { validateDependencyRoutes } from './bindings.js';
|
|
43
|
+
import { Supervisor } from './supervisor.js';
|
|
44
|
+
import { ConnectionGate, backoffMs, errorCode, isAbort, isRevocation, isTransient, jitteredInterval, retryTransient, sleep, } from './control-plane.js';
|
|
45
|
+
import { ServicePool, freshForDrill, runDrill } from './drill-runner.js';
|
|
46
|
+
import { createRunScratch } from './scratch.js';
|
|
47
|
+
import { CliError } from '../http.js';
|
|
48
|
+
import { LocalRun, checkRunCompatible, runOwnedAliases } from './local-run.js';
|
|
49
|
+
const DEFAULT_HEARTBEAT_MS = 15_000;
|
|
50
|
+
const MIN_HEARTBEAT_MS = 1_000;
|
|
51
|
+
const MAX_HEARTBEAT_MS = 300_000;
|
|
52
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
53
|
+
const RECONNECT_CAP_MS = 60_000;
|
|
54
|
+
const CLAIM_IDLE_BASE_MS = 1_000;
|
|
55
|
+
const CLAIM_IDLE_CAP_MS = 15_000;
|
|
56
|
+
const CLOSE_TIMEOUT_MS = 10_000;
|
|
57
|
+
const MAX_DECLINED_REMEMBERED = 1_024;
|
|
58
|
+
/** This client's own ceilings for one drill; root bounds participant steps and the drill deadline. */
|
|
59
|
+
const LOCAL_MAX_STEPS = 1_000;
|
|
60
|
+
const LOCAL_STEP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
61
|
+
/** A drill whose deadline leaves less than this is declined rather than started. */
|
|
62
|
+
const MIN_DRILL_MS = 60_000;
|
|
63
|
+
function heartbeatInterval(value) {
|
|
64
|
+
return typeof value === 'number' && Number.isSafeInteger(value)
|
|
65
|
+
? Math.min(MAX_HEARTBEAT_MS, Math.max(MIN_HEARTBEAT_MS, value)) : DEFAULT_HEARTBEAT_MS;
|
|
66
|
+
}
|
|
67
|
+
function abortPromise(signal) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
if (signal.aborted)
|
|
70
|
+
resolve();
|
|
71
|
+
else
|
|
72
|
+
signal.addEventListener('abort', () => resolve(), { once: true });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Why an offer is declined (reason code), or null when it may be accepted. */
|
|
76
|
+
function admissionProblem(offer, plan, approval) {
|
|
77
|
+
if (!offer || typeof offer.assignment_id !== 'string' || !NATIVE_ID.test(offer.assignment_id)
|
|
78
|
+
|| typeof offer.attempt_id !== 'string' || !NATIVE_ID.test(offer.attempt_id))
|
|
79
|
+
return 'native_assignment_invalid';
|
|
80
|
+
if (offer.state !== 'offered')
|
|
81
|
+
return 'native_offer_not_offered';
|
|
82
|
+
// The descriptor digest also binds the driver profile and adapter digest.
|
|
83
|
+
if (offer.descriptor_sha256 !== plan.descriptor_sha256 || offer.descriptor_sha256 !== approval.descriptor_sha256) {
|
|
84
|
+
return 'native_descriptor_mismatch';
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
assertPlanApproved(approval, plan);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return errorCode(error) ?? 'native_plan_changed';
|
|
91
|
+
}
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (!(Date.parse(offer.offer_expires_at) > now))
|
|
94
|
+
return 'native_offer_expired';
|
|
95
|
+
if (!(Date.parse(offer.deadline_at) - now > MIN_DRILL_MS))
|
|
96
|
+
return 'native_deadline_too_short';
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The runnable drill for an accepted receipt: identity, fence and lease from
|
|
101
|
+
* root; driver and plan digest from the approved local plan; step, timeout and
|
|
102
|
+
* evidence ceilings from this client, with elapsed time bounded by root's deadline.
|
|
103
|
+
*/
|
|
104
|
+
function runnableAssignment(receipt, descriptorSha256, plan, descriptor, approval) {
|
|
105
|
+
return {
|
|
106
|
+
assignment_id: receipt.assignment_id, attempt_id: receipt.attempt_id, campaign_id: receipt.campaign_id, drill_id: receipt.attempt_id,
|
|
107
|
+
lease_epoch: receipt.lease_epoch, state_generation: receipt.state_generation, lease_expires_at: receipt.lease_expires_at,
|
|
108
|
+
descriptor_sha256: descriptorSha256, plan_sha256: approval.plan_sha256,
|
|
109
|
+
driver: { profile: plan.driver.profile, adapter_sha256: descriptor.driver.adapter_sha256 },
|
|
110
|
+
identity: null,
|
|
111
|
+
budgets: { max_steps: LOCAL_MAX_STEPS, max_elapsed_ms: Math.max(1, Date.parse(receipt.deadline_at) - Date.now()),
|
|
112
|
+
step_timeout_ms: LOCAL_STEP_TIMEOUT_MS, max_observation_bytes: EVIDENCE_MAX_TOTAL_BYTES },
|
|
113
|
+
step_cursor: 0, observation_ack: receipt.observation_sequence, observation_elapsed_ms: receipt.observation_elapsed_ms,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function runCompanion(options) {
|
|
117
|
+
const { controlPlane, plan, descriptor, capabilities, output, approval } = options;
|
|
118
|
+
// Local preconditions, before any cloud call.
|
|
119
|
+
assertPlanApproved(approval, plan);
|
|
120
|
+
const normalized = normalizeDescriptor(descriptor);
|
|
121
|
+
if (normalized.descriptor_sha256 !== plan.descriptor_sha256 || approval.descriptor_sha256 !== plan.descriptor_sha256) {
|
|
122
|
+
throw nativeError('plan_changed', 'The companion was not started: the approved plan was made for a different descriptor.', 2);
|
|
123
|
+
}
|
|
124
|
+
const driverCapability = capabilities.drivers.find((entry) => entry.profile === plan.driver.profile);
|
|
125
|
+
if (!driverCapability?.available) {
|
|
126
|
+
throw nativeError('driver_unavailable', `The companion was not started: the ${plan.driver.profile} driver is unavailable on this machine.`);
|
|
127
|
+
}
|
|
128
|
+
const roots = await resolvePlanRoots(plan);
|
|
129
|
+
await verifyPlanFiles(plan, normalized, roots);
|
|
130
|
+
// Checks that depend only on the approved plan, run again for every drill: a plan that fails them can never be
|
|
131
|
+
// admitted, and every drill accepted for it would use up a campaign slot. So nothing is taken.
|
|
132
|
+
readinessDomains(descriptor, planStateActions(descriptor, plan));
|
|
133
|
+
validateDependencyRoutes(descriptor, plan, { freshForDrill: freshForDrill(plan) });
|
|
134
|
+
const parallelism = Math.max(1, Math.min(effectiveParallelism(descriptor, plan), Number.isSafeInteger(capabilities.max_parallel_drills) && capabilities.max_parallel_drills > 0 ? capabilities.max_parallel_drills : 1));
|
|
135
|
+
// Shared launched services live for the campaign's local run, which root keeps per campaign and device.
|
|
136
|
+
const runAliases = runOwnedAliases(plan);
|
|
137
|
+
const campaignId = typeof options.campaignId === 'string' && options.campaignId ? options.campaignId.toLowerCase() : null;
|
|
138
|
+
// Root keeps one companion channel per device, for one campaign; without it nothing but per-drill polling would
|
|
139
|
+
// wake the drills, so a companion runs exactly the selected campaign's drills.
|
|
140
|
+
const wakes = controlPlane.wakes ?? null;
|
|
141
|
+
if (!campaignId || !wakes || wakes.campaign_id !== campaignId) {
|
|
142
|
+
throw new CliError('native_campaign_required', 'Select the campaign to run with --campaign <campaign-id>: the companion follows one campaign\'s offers and drills over a single channel. Nothing was started.', 2);
|
|
143
|
+
}
|
|
144
|
+
if (runAliases.length > 0) {
|
|
145
|
+
checkRunCompatible(descriptor, runAliases);
|
|
146
|
+
}
|
|
147
|
+
// Stop (graceful) and abort (hard) controls.
|
|
148
|
+
const stopping = new AbortController();
|
|
149
|
+
const hard = new AbortController();
|
|
150
|
+
const heartbeatStop = new AbortController();
|
|
151
|
+
let graceTimer = null;
|
|
152
|
+
let stopReason = 'stopped';
|
|
153
|
+
const beginShutdown = (reason) => {
|
|
154
|
+
if (stopping.signal.aborted)
|
|
155
|
+
return;
|
|
156
|
+
stopReason = reason;
|
|
157
|
+
stopping.abort();
|
|
158
|
+
graceTimer = setTimeout(() => hard.abort(), Math.max(0, options.shutdownGraceMs ?? 30_000));
|
|
159
|
+
graceTimer.unref?.();
|
|
160
|
+
output('native_companion_stopping', { reason }, 'Stopping: no new drills are admitted; in-flight drills finish their current step.');
|
|
161
|
+
};
|
|
162
|
+
const onCallerStop = () => beginShutdown('stopped');
|
|
163
|
+
if (options.signal.aborted)
|
|
164
|
+
onCallerStop();
|
|
165
|
+
else
|
|
166
|
+
options.signal.addEventListener('abort', onCallerStop, { once: true });
|
|
167
|
+
const counts = { completed: 0, failed: 0, cancelled: 0, incomplete: 0, declined: 0, evidence_incomplete: 0 };
|
|
168
|
+
const gate = new ConnectionGate();
|
|
169
|
+
let sessionId = null;
|
|
170
|
+
let revoked = false;
|
|
171
|
+
let fatal = null;
|
|
172
|
+
const markRevoked = () => {
|
|
173
|
+
if (revoked)
|
|
174
|
+
return;
|
|
175
|
+
revoked = true;
|
|
176
|
+
output('native_authorization_revoked', {}, 'Authorization was revoked: no new work is admitted and in-flight drills stop before their next step.');
|
|
177
|
+
beginShutdown('revoked');
|
|
178
|
+
};
|
|
179
|
+
const fail = (error) => { if (fatal === null)
|
|
180
|
+
fatal = error; beginShutdown('failed'); };
|
|
181
|
+
let scratch = null;
|
|
182
|
+
let supervisor = null;
|
|
183
|
+
let localRun = null;
|
|
184
|
+
const active = new Set();
|
|
185
|
+
const reconciliationPending = (reason) => {
|
|
186
|
+
if (stopping.signal.aborted)
|
|
187
|
+
return;
|
|
188
|
+
output('native_campaign_reconciliation_pending', { campaign_id: campaignId, reason_code: reason }, 'Cloud reconciliation remains pending. Stopping local work; original evidence, unknown outcomes and funding holds remain with the service.');
|
|
189
|
+
fail(new CliError(reason, 'Local work stopped with cloud reconciliation pending. Do not repeat an uncertain target action.', 2));
|
|
190
|
+
};
|
|
191
|
+
let heartbeat = Promise.resolve();
|
|
192
|
+
/** The offer loop's wake-ups from the campaign channel, and the channel state notices. */
|
|
193
|
+
let offers = null;
|
|
194
|
+
let unwatchChannel = () => { };
|
|
195
|
+
try {
|
|
196
|
+
scratch = await createRunScratch();
|
|
197
|
+
const perDrill = plan.services.filter((service) => service.mode === 'launch' && service.per_drill).length;
|
|
198
|
+
supervisor = new Supervisor({
|
|
199
|
+
plan, normalized, approval, roots,
|
|
200
|
+
maxProcesses: Math.min(1024, Math.max(1, plan.services.length - perDrill + perDrill * parallelism)),
|
|
201
|
+
outputBytes: options.outputBytes ?? 256 * 1024,
|
|
202
|
+
// Shared services start only under the launch ids registered for the campaign's local run.
|
|
203
|
+
runOwned: new Set(runAliases), runLaunchId: (alias) => localRun?.launchId(alias) ?? null,
|
|
204
|
+
beforeRunLaunch: async (alias) => {
|
|
205
|
+
if (!localRun)
|
|
206
|
+
throw new CliError('native_local_run_unregistered', 'Register the local run before launching shared services.');
|
|
207
|
+
await localRun.beforeLaunch(alias);
|
|
208
|
+
},
|
|
209
|
+
captureRunObserved: async () => { if (localRun)
|
|
210
|
+
await localRun.captureObserved(); },
|
|
211
|
+
runNotStarted: async (alias) => { if (localRun)
|
|
212
|
+
await localRun.notStarted(alias); },
|
|
213
|
+
});
|
|
214
|
+
const activeSupervisor = supervisor;
|
|
215
|
+
const services = new ServicePool({ supervisor, plan, descriptor, signal: hard.signal });
|
|
216
|
+
// Open the session, reconnecting on transient failures until stopped.
|
|
217
|
+
const offered = { ...capabilities, max_parallel_drills: parallelism, isolation: 'native-trusted' };
|
|
218
|
+
let opened = null;
|
|
219
|
+
for (let attempt = 0; !opened && !stopping.signal.aborted; attempt++) {
|
|
220
|
+
try {
|
|
221
|
+
opened = await controlPlane.openSession(offered, AbortSignal.any([stopping.signal, hard.signal]));
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (stopping.signal.aborted)
|
|
225
|
+
break;
|
|
226
|
+
if (isRevocation(error)) {
|
|
227
|
+
markRevoked();
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
if (!isTransient(error))
|
|
231
|
+
throw error;
|
|
232
|
+
output('native_disconnected', { attempt: attempt + 1 }, 'The Firedrill service is unreachable; retrying the session.');
|
|
233
|
+
await sleep(backoffMs(attempt, RECONNECT_BASE_MS, RECONNECT_CAP_MS), stopping.signal);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (opened) {
|
|
237
|
+
if (typeof opened.session_id !== 'string' || !NATIVE_ID.test(opened.session_id)) {
|
|
238
|
+
throw nativeError('disconnected', 'The companion session could not be confirmed: the session identifier is malformed.');
|
|
239
|
+
}
|
|
240
|
+
const id = opened.session_id;
|
|
241
|
+
sessionId = id;
|
|
242
|
+
const heartbeatMs = heartbeatInterval(opened.heartbeat_ms);
|
|
243
|
+
output('native_companion_started', { session_id: id, parallelism, driver: plan.driver.profile, isolation: 'native-trusted' }, `Companion connected (${parallelism} parallel drill${parallelism === 1 ? '' : 's'}, ${plan.driver.profile}). Native mode runs your services as trusted local processes; it is not a sandbox.`);
|
|
244
|
+
heartbeat = runHeartbeat({ controlPlane, sessionId: id, heartbeatMs, gate, output, stop: heartbeatStop.signal, hard: hard.signal,
|
|
245
|
+
onRevoked: markRevoked, onFatal: fail });
|
|
246
|
+
// The campaign's local run: reconciled with root before any offer; registered just before the first one.
|
|
247
|
+
if (runAliases.length > 0 && campaignId) {
|
|
248
|
+
// Root keeps the run per campaign and device. The session is the runner device's presence, so its id is the device key.
|
|
249
|
+
localRun = await LocalRun.open({
|
|
250
|
+
controlPlane, campaignId, deviceKey: id, descriptorSha256: plan.descriptor_sha256, aliases: runAliases,
|
|
251
|
+
observe: () => activeSupervisor.runStates(), output,
|
|
252
|
+
}, AbortSignal.any([stopping.signal, hard.signal]));
|
|
253
|
+
// A run that takes no more drills here (closed, uncertain, or left by an earlier companion) is only closed.
|
|
254
|
+
if (localRun.retired)
|
|
255
|
+
fail(new CliError(localRun.retired.code, localRun.retired.message, 2));
|
|
256
|
+
}
|
|
257
|
+
const run = localRun;
|
|
258
|
+
const claimOptions = run ? { campaignOnly: true, beforeOffer: run.beforeOffer, runRegistered: () => run.registered } : undefined;
|
|
259
|
+
const declined = new Set();
|
|
260
|
+
const decline = (assignment, reason) => {
|
|
261
|
+
counts.declined++;
|
|
262
|
+
const key = `${assignment?.assignment_id}\0${assignment?.attempt_id}`;
|
|
263
|
+
declined.add(key);
|
|
264
|
+
if (declined.size > MAX_DECLINED_REMEMBERED)
|
|
265
|
+
declined.delete(declined.values().next().value);
|
|
266
|
+
output('native_assignment_declined', { assignment_id: typeof assignment?.assignment_id === 'string' && NATIVE_ID.test(assignment.assignment_id) ? assignment.assignment_id : null, reason }, `Declined an offered drill (${reason}). Nothing was started for it.`);
|
|
267
|
+
};
|
|
268
|
+
const loopSignal = AbortSignal.any([stopping.signal, hard.signal]);
|
|
269
|
+
// The campaign channel wakes this loop; while it waits for a valid resync nothing is read, claimed or stepped.
|
|
270
|
+
if (wakes) {
|
|
271
|
+
let held = false;
|
|
272
|
+
unwatchChannel = wakes.onState((state, code) => {
|
|
273
|
+
if (state === 'refused' || (state === 'reconnecting' && code === 'native_companion_stream_busy')) {
|
|
274
|
+
if (held)
|
|
275
|
+
return;
|
|
276
|
+
held = true;
|
|
277
|
+
output('native_channel_held', { state, error_code: code }, state === 'refused'
|
|
278
|
+
? 'The service refused this runner\'s campaign channel (access changed): no new drill or step starts until access is confirmed again.'
|
|
279
|
+
: 'Another companion holds this runner\'s campaign channel: no new drill or step starts until it is free.');
|
|
280
|
+
}
|
|
281
|
+
else if (state === 'live' && held) {
|
|
282
|
+
held = false;
|
|
283
|
+
output('native_channel_resumed', {}, 'The campaign channel is confirmed again; drills continue.');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
offers = wakes.subscribe({ kind: 'campaign', id: campaignId });
|
|
287
|
+
}
|
|
288
|
+
let idle = 0;
|
|
289
|
+
/**
|
|
290
|
+
* An admitted offer whose accept outcome is unknown (the request may have
|
|
291
|
+
* landed). Its byte-identical accept is re-sent before anything new is
|
|
292
|
+
* claimed, so root returns the original answer and no accepted drill is stranded.
|
|
293
|
+
*/
|
|
294
|
+
let acceptUnknown = null;
|
|
295
|
+
while (!stopping.signal.aborted) {
|
|
296
|
+
if (!await gate.wait(stopping.signal))
|
|
297
|
+
break;
|
|
298
|
+
if (wakes && !await wakes.live(stopping.signal))
|
|
299
|
+
break;
|
|
300
|
+
if (active.size >= parallelism) {
|
|
301
|
+
await Promise.race([...active, abortPromise(stopping.signal)]);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// Read before the canonical claim, so a wake-up that lands during it is not lost.
|
|
305
|
+
const generation = offers?.generation ?? 0;
|
|
306
|
+
const resumed = acceptUnknown;
|
|
307
|
+
acceptUnknown = null;
|
|
308
|
+
let claimed = resumed;
|
|
309
|
+
if (!resumed) {
|
|
310
|
+
try {
|
|
311
|
+
claimed = await controlPlane.claimAssignment(id, loopSignal, claimOptions);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (stopping.signal.aborted || isAbort(error, loopSignal))
|
|
315
|
+
break;
|
|
316
|
+
if (isRevocation(error)) {
|
|
317
|
+
markRevoked();
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (isTransient(error)) {
|
|
321
|
+
if (gate.disconnected())
|
|
322
|
+
output('native_disconnected', {}, 'Connection lost; new steps are paused until it returns.');
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
fail(error);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!claimed) {
|
|
330
|
+
// Empty pages are not completion. First drain locally active drills,
|
|
331
|
+
// then ask the authoritative campaign for sealed target-work closure.
|
|
332
|
+
// Shared process shutdown and its signed run close still happen in
|
|
333
|
+
// finally, independently of later cloud accounting settlement.
|
|
334
|
+
if (active.size) {
|
|
335
|
+
await Promise.race([...active, abortPromise(stopping.signal)]);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (controlPlane.targetWorkComplete || controlPlane.targetDispatchComplete) {
|
|
339
|
+
try {
|
|
340
|
+
if (await controlPlane.targetWorkComplete?.(loopSignal)) {
|
|
341
|
+
output('native_target_work_complete', { campaign_id: campaignId }, 'All target work is closed. Stopping shared local services; cloud cleanup and accounting may still be pending.');
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
if (await controlPlane.targetDispatchComplete?.(loopSignal)) {
|
|
345
|
+
output('native_target_dispatch_complete', { campaign_id: campaignId }, 'All target dispatch is closed. Stopping local services; cloud sealing, assessment and accounting may still be pending.');
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (stopping.signal.aborted || isAbort(error, loopSignal))
|
|
351
|
+
break;
|
|
352
|
+
if (isRevocation(error)) {
|
|
353
|
+
markRevoked();
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (isTransient(error)) {
|
|
357
|
+
gate.disconnected();
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
fail(error);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Nothing to take: the campaign's next change (an offer, a slot, admission) wakes this loop.
|
|
365
|
+
if (offers)
|
|
366
|
+
await offers.wait(generation, stopping.signal);
|
|
367
|
+
else
|
|
368
|
+
await sleep(backoffMs(idle++, CLAIM_IDLE_BASE_MS, CLAIM_IDLE_CAP_MS), stopping.signal);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
idle = 0;
|
|
372
|
+
if (stopping.signal.aborted)
|
|
373
|
+
break;
|
|
374
|
+
const offer = claimed;
|
|
375
|
+
// Best effort: a rejected offer frees the drill now instead of at offer expiry.
|
|
376
|
+
const reject = () => { void controlPlane.rejectAssignment(offer, loopSignal).catch(() => undefined); };
|
|
377
|
+
if (!resumed) {
|
|
378
|
+
if (declined.has(`${offer.assignment_id}\0${offer.attempt_id}`)) {
|
|
379
|
+
if (offers)
|
|
380
|
+
await offers.wait(generation, stopping.signal);
|
|
381
|
+
else
|
|
382
|
+
await sleep(backoffMs(0, CLAIM_IDLE_BASE_MS, CLAIM_IDLE_CAP_MS), stopping.signal);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
// Readiness preconditions: digests, driver, lease, and pinned files unchanged on disk.
|
|
386
|
+
const problem = admissionProblem(offer, plan, approval);
|
|
387
|
+
if (problem) {
|
|
388
|
+
decline(offer, problem);
|
|
389
|
+
reject();
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
await verifyPlanFiles(plan, normalized, roots);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
// A changed approved file voids the approval for every later drill.
|
|
397
|
+
decline(offer, errorCode(error) ?? 'native_plan_changed');
|
|
398
|
+
reject();
|
|
399
|
+
fail(error);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
let acceptance;
|
|
404
|
+
try {
|
|
405
|
+
acceptance = await retryTransient(() => controlPlane.acceptAssignment(offer, loopSignal), { signal: loopSignal, attempts: 3, onTransient: () => { gate.disconnected(); } });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (stopping.signal.aborted)
|
|
409
|
+
break;
|
|
410
|
+
if (isRevocation(error)) {
|
|
411
|
+
markRevoked();
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
// Outcome unknown: re-send the same accept next, after the connection returns. Nothing started.
|
|
415
|
+
if (isTransient(error)) {
|
|
416
|
+
acceptUnknown = offer;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
fail(error);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
if (!acceptance?.accepted || !acceptance.receipt) {
|
|
423
|
+
output('native_assignment_not_accepted', { assignment_id: offer.assignment_id, reason: typeof acceptance?.reason === 'string' && /^[a-z_]{1,64}$/.test(acceptance.reason) ? acceptance.reason : null }, 'The offered drill was not confirmed by the service. Nothing was started for it.');
|
|
424
|
+
// The plane does not hand out a refused offer again. One refused for a reason other than its expiry is
|
|
425
|
+
// rejected, so the drill is freed now (root's expiry settles an expired one). The next claim waits like
|
|
426
|
+
// a declined one, so repeated refusals never become a tight loop of reads and signed requests.
|
|
427
|
+
if (!acceptance?.receipt && acceptance?.reason !== 'native_offer_expired')
|
|
428
|
+
reject();
|
|
429
|
+
if (offers)
|
|
430
|
+
await offers.wait(generation, stopping.signal);
|
|
431
|
+
else
|
|
432
|
+
await sleep(backoffMs(0, CLAIM_IDLE_BASE_MS, CLAIM_IDLE_CAP_MS), stopping.signal);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
output('native_assignment_accepted', { assignment_id: offer.assignment_id, attempt_id: offer.attempt_id,
|
|
436
|
+
campaign_id: offer.campaign_id, deadline_at: offer.deadline_at }, 'Accepted an offered drill. It starts when the service has prepared it.');
|
|
437
|
+
const assignment = runnableAssignment(acceptance.receipt, offer.descriptor_sha256, plan, descriptor, approval);
|
|
438
|
+
const run = runDrill(assignment, {
|
|
439
|
+
controlPlane, plan, descriptor, roots, services, scratch, gate, output,
|
|
440
|
+
stopSignal: stopping.signal, abortSignal: hard.signal, isRevoked: () => revoked,
|
|
441
|
+
observationBufferBytes: options.observationBufferBytes, localRun,
|
|
442
|
+
}).then((report) => {
|
|
443
|
+
counts[report.outcome]++;
|
|
444
|
+
if (!report.evidence?.evidence_complete)
|
|
445
|
+
counts.evidence_incomplete++;
|
|
446
|
+
if (report.reason_code === 'native_participant_outcome_unknown' || report.unknown_steps > 0
|
|
447
|
+
|| report.completion === 'not-reported') {
|
|
448
|
+
reconciliationPending(report.reason_code === 'native_participant_outcome_unknown'
|
|
449
|
+
? report.reason_code : 'native_campaign_reconciliation_pending');
|
|
450
|
+
}
|
|
451
|
+
}, () => {
|
|
452
|
+
counts.failed++;
|
|
453
|
+
counts.evidence_incomplete++;
|
|
454
|
+
reconciliationPending('native_campaign_reconciliation_pending');
|
|
455
|
+
})
|
|
456
|
+
.finally(() => { active.delete(run); });
|
|
457
|
+
active.add(run);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
fail(error);
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
beginShutdown(stopReason);
|
|
466
|
+
// Drain: drills stop before their next step; the grace timer hard-aborts stragglers.
|
|
467
|
+
await Promise.allSettled([...active]);
|
|
468
|
+
if (supervisor)
|
|
469
|
+
await supervisor.stopAll().catch(() => undefined);
|
|
470
|
+
if (localRun)
|
|
471
|
+
await localRun.captureObserved(true).catch((error) => {
|
|
472
|
+
output('native_local_run_journal_pending', { error_code: errorCode(error) }, 'Final shared-service observations could not be saved. Cleanup remains unresolved; do not assume the processes were recorded as stopped.');
|
|
473
|
+
});
|
|
474
|
+
// Every shared service is stopped: report how each ended and close the campaign's local run,
|
|
475
|
+
// while the device session (and its heartbeat) is still live; root refuses both once it stopped.
|
|
476
|
+
if (localRun && sessionId && !revoked) {
|
|
477
|
+
const run = localRun;
|
|
478
|
+
const wait = Math.max(0, options.runCloseWaitMs ?? 5 * 60_000);
|
|
479
|
+
await run.close(AbortSignal.timeout(wait + 60_000), AbortSignal.timeout(wait)).catch((error) => {
|
|
480
|
+
output('native_local_run_close_pending', { error_code: errorCode(error) }, `The campaign's local run was not closed. ${run.closeLaterHint}`);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
// Every drill drained and the local run is settled: the campaign channel ends with the session.
|
|
484
|
+
offers?.close();
|
|
485
|
+
unwatchChannel();
|
|
486
|
+
wakes?.close();
|
|
487
|
+
heartbeatStop.abort();
|
|
488
|
+
await heartbeat.catch(() => undefined);
|
|
489
|
+
if (sessionId && !revoked) {
|
|
490
|
+
await controlPlane.closeSession(sessionId, AbortSignal.timeout(CLOSE_TIMEOUT_MS)).catch((error) => {
|
|
491
|
+
output('native_session_close_unconfirmed', { error_code: errorCode(error) }, 'The companion session close was not confirmed by the service.');
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (scratch)
|
|
495
|
+
await scratch.cleanup();
|
|
496
|
+
if (graceTimer)
|
|
497
|
+
clearTimeout(graceTimer);
|
|
498
|
+
options.signal.removeEventListener('abort', onCallerStop);
|
|
499
|
+
}
|
|
500
|
+
const result = {
|
|
501
|
+
session_id: sessionId, ended: fatal !== null ? 'failed' : revoked ? 'revoked' : stopReason,
|
|
502
|
+
error_code: fatal !== null ? (errorCode(fatal) ?? 'native_companion_failed') : revoked ? 'native_authorization_revoked' : null,
|
|
503
|
+
drills: counts,
|
|
504
|
+
};
|
|
505
|
+
output('native_companion_stopped', result, `Companion stopped (${result.ended}): ${counts.completed} completed, ${counts.failed} failed, ${counts.cancelled} cancelled, `
|
|
506
|
+
+ `${counts.incomplete} incomplete, ${counts.declined} declined; ${counts.evidence_incomplete} with incomplete evidence.`);
|
|
507
|
+
if (fatal !== null)
|
|
508
|
+
throw fatal;
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Heartbeat at the server interval (jittered, never above it) while connected;
|
|
513
|
+
* while disconnected, probe with capped exponential backoff and jitter and
|
|
514
|
+
* reopen the gate on the first success. A runner-reported disconnect wakes the
|
|
515
|
+
* loop early. Heartbeats continue while drills drain so their leases stay live.
|
|
516
|
+
*/
|
|
517
|
+
async function runHeartbeat(options) {
|
|
518
|
+
const { controlPlane, gate, output } = options;
|
|
519
|
+
let failures = 0;
|
|
520
|
+
while (!options.stop.aborted) {
|
|
521
|
+
const delay = gate.connected ? jitteredInterval(options.heartbeatMs) : backoffMs(failures, RECONNECT_BASE_MS, RECONNECT_CAP_MS);
|
|
522
|
+
const wake = new AbortController();
|
|
523
|
+
const unsubscribe = gate.onChange(() => { if (!gate.connected)
|
|
524
|
+
wake.abort(); });
|
|
525
|
+
await sleep(delay, AbortSignal.any([options.stop, wake.signal]));
|
|
526
|
+
unsubscribe();
|
|
527
|
+
if (options.stop.aborted)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
const response = await controlPlane.heartbeat(options.sessionId, AbortSignal.any([options.hard, options.stop, AbortSignal.timeout(Math.max(MIN_HEARTBEAT_MS, options.heartbeatMs))]));
|
|
531
|
+
failures = 0;
|
|
532
|
+
if (gate.reconnected())
|
|
533
|
+
output('native_reconnected', {}, 'Connection restored; paused drills continue with their next step.');
|
|
534
|
+
if (response?.revoked === true) {
|
|
535
|
+
options.onRevoked();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (options.stop.aborted)
|
|
541
|
+
return;
|
|
542
|
+
if (isRevocation(error)) {
|
|
543
|
+
options.onRevoked();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (isTransient(error) || isAbort(error)) {
|
|
547
|
+
failures++;
|
|
548
|
+
if (gate.disconnected())
|
|
549
|
+
output('native_disconnected', {}, 'Connection lost; new steps are paused until it returns.');
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
options.onFatal(error);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|