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,506 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { CliError } from '../http.js';
|
|
3
|
+
import { selectOne, terminalText } from '../prompts.js';
|
|
4
|
+
import { approveCredits } from './credit-approval.js';
|
|
5
|
+
import { requestInput } from '../request-input.js';
|
|
6
|
+
import { selectedWorkspace } from '../world-access.js';
|
|
7
|
+
import { applyNativePlan, cancelPreparedApply, cancelUnstartedApply, createApplyPlan, createNativeCampaign, estimatePlanFunding, findNativeApplyOperation, getApplyPlan, getNativeApplyOperation, getWorldRevision, latestWorldRevision, } from '../native/cloud-resources.js';
|
|
8
|
+
import { jitteredInterval, retryTransient, sleep } from '../native/control-plane.js';
|
|
9
|
+
import { activeTime } from '../native/contracts.js';
|
|
10
|
+
const MAX_INPUT_BYTES = 1024 * 1024;
|
|
11
|
+
/** Read one bounded JSON object from a file path, or from stdin when the path is "-". */
|
|
12
|
+
export async function readJsonInput(path, label, signal, maximumBytes = MAX_INPUT_BYTES) {
|
|
13
|
+
if (!path)
|
|
14
|
+
throw new CliError('input_required', `Provide ${label} as a file path, or - to read it from standard input.`, 2);
|
|
15
|
+
if (path !== '-')
|
|
16
|
+
return requestInput(path, signal, maximumBytes);
|
|
17
|
+
if (process.stdin.isTTY)
|
|
18
|
+
throw new CliError('input_required', `Pipe ${label} on standard input when using -. Typed input is not read.`, 2);
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let size = 0;
|
|
21
|
+
const onAbort = () => process.stdin.destroy();
|
|
22
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
try {
|
|
24
|
+
for await (const chunk of process.stdin) {
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
27
|
+
size += bytes.length;
|
|
28
|
+
if (size > maximumBytes)
|
|
29
|
+
throw new CliError('request_file_invalid', `${label} exceeds ${maximumBytes} bytes. Nothing was submitted.`, 2);
|
|
30
|
+
chunks.push(bytes);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
signal.removeEventListener('abort', onAbort);
|
|
35
|
+
}
|
|
36
|
+
signal.throwIfAborted();
|
|
37
|
+
try {
|
|
38
|
+
const raw = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
41
|
+
throw new Error();
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new CliError('request_file_invalid', `${label} on standard input is not a JSON object. Nothing was submitted.`, 2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Parse bounded JSON input into an object, with a stable error when it is not one. */
|
|
49
|
+
export async function readJsonObject(path, label, signal, maximumBytes = MAX_INPUT_BYTES) {
|
|
50
|
+
const raw = await readJsonInput(path, label, signal, maximumBytes);
|
|
51
|
+
let value;
|
|
52
|
+
try {
|
|
53
|
+
value = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
value = null;
|
|
57
|
+
}
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
59
|
+
throw new CliError('request_file_invalid', `${label} is not a JSON object. Nothing was submitted.`, 2);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
const scopeOf = (options) => selectedWorkspace({ workspace: options.workspace, signal: options.signal,
|
|
63
|
+
interactive: !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY, openBrowser: options.openBrowser && !options.json });
|
|
64
|
+
/** Plain words for the review's requirement codes; the code and resource stay exactly as the service returned them. */
|
|
65
|
+
const GAP_TEXT = {
|
|
66
|
+
resource_adapter_required: 'a saved resource needs a compatible execution adapter',
|
|
67
|
+
scenario_evaluation_required: 'select an evaluation for this scenario',
|
|
68
|
+
scenario_dependency_missing: 'an event refers to a dependency missing from this world',
|
|
69
|
+
real_dependency_event_unsupported: 'a simulated availability event cannot change a real account',
|
|
70
|
+
scenario_required: 'add at least one scenario',
|
|
71
|
+
parallel_state_boundary_required: 'use isolated state for each drill or choose serial execution',
|
|
72
|
+
local_state_reset_required: 'configure a reset for the local state before running independent drills',
|
|
73
|
+
configuration_mapping_required: 'declare the required build or runtime configuration',
|
|
74
|
+
dependency_mapping_incomplete: 'map each required dependency to a hosted tool or an explicitly authorized real connection',
|
|
75
|
+
dependency_contract_mismatch: 'the selected tool contract does not match the agent dependency',
|
|
76
|
+
tool_operation_unavailable: 'the selected tool is missing a required operation',
|
|
77
|
+
tool_protocol_mismatch: 'the tool connection protocol does not match the agent dependency',
|
|
78
|
+
library_parameters_require_binding: 'this library version declares parameters: bind it with firedrill library bind <version-id> --file <values.json> '
|
|
79
|
+
+ 'and reference the bound version instead',
|
|
80
|
+
};
|
|
81
|
+
/** Characters of one bound parameter value shown in the review; the full value is in the bound version. */
|
|
82
|
+
const BOUND_PREVIEW = 80;
|
|
83
|
+
/** How each hosted tool runs, from the review's `tool_execution` (root lanes; an unlisted lane is shown by its code). */
|
|
84
|
+
const LANE_TEXT = {
|
|
85
|
+
pooled: 'shared tool worker (reviewed catalog version)',
|
|
86
|
+
isolated: 'isolated runtime (own run time, cleanup and cost ceiling per drill)',
|
|
87
|
+
};
|
|
88
|
+
function laneLines(review) {
|
|
89
|
+
const lanes = Array.isArray(review.tool_execution) ? review.tool_execution : [];
|
|
90
|
+
const real = new Set(review.real_dependency_grants);
|
|
91
|
+
const dependencies = Array.isArray(review.dependencies) ? review.dependencies : [];
|
|
92
|
+
const lines = lanes.filter(item => typeof item.alias === 'string').map(item => ` ${terminalText(item.alias)}: hosted tool, `
|
|
93
|
+
+ (typeof item.lane === 'string' && Object.hasOwn(LANE_TEXT, item.lane) ? LANE_TEXT[item.lane] : terminalText(String(item.lane))));
|
|
94
|
+
for (const dependency of dependencies) {
|
|
95
|
+
if (typeof dependency.alias === 'string' && dependency.mode === 'real') {
|
|
96
|
+
lines.push(` ${terminalText(dependency.alias)}: real account${real.has(dependency.alias) ? ' (grant still needed)' : ''}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return lines.length ? ['Dependencies:', ...lines] : [];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The exact parameter values fixed in bound library versions this plan uses
|
|
103
|
+
* (root's `parameter_bindings`), shown before approval because they decide
|
|
104
|
+
* what the drills say and check. Values are service data, terminal-escaped.
|
|
105
|
+
*/
|
|
106
|
+
function bindingLines(review) {
|
|
107
|
+
const items = Array.isArray(review.parameter_bindings) ? review.parameter_bindings : [];
|
|
108
|
+
const lines = [];
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const binding = item.binding && typeof item.binding === 'object' ? item.binding : null;
|
|
111
|
+
if (typeof item.revision_id !== 'string' || !binding || !binding.values || typeof binding.values !== 'object')
|
|
112
|
+
continue;
|
|
113
|
+
const values = Object.entries(binding.values).map(([name, value]) => {
|
|
114
|
+
const text = typeof value === 'string' ? JSON.stringify(value.length > BOUND_PREVIEW ? `${value.slice(0, BOUND_PREVIEW)}...` : value) : String(value);
|
|
115
|
+
return `${terminalText(name)}=${terminalText(text)}`;
|
|
116
|
+
});
|
|
117
|
+
lines.push(` ${terminalText(item.revision_id)}${typeof binding.source_revision_id === 'string' ? ` (bound from ${terminalText(binding.source_revision_id)})` : ''}: `
|
|
118
|
+
+ (values.length ? values.join(', ') : 'all parameters at their defaults'));
|
|
119
|
+
}
|
|
120
|
+
return lines.length ? ['Parameter values fixed in bound library versions:', ...lines] : [];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Root's evolution review for v3 scenarios (absent for earlier ones): how each
|
|
124
|
+
* scenario's schedule behaves without the agent. A world-only continuation is
|
|
125
|
+
* approved together with the plan, so it is spelled out before the approval.
|
|
126
|
+
*/
|
|
127
|
+
function evolutionLines(review) {
|
|
128
|
+
const items = Array.isArray(review.evolution) ? review.evolution : [];
|
|
129
|
+
const lines = items.filter(item => typeof item.scenario_id === 'string').map(item => {
|
|
130
|
+
const timed = typeof item.timed_occurrences === 'number' && item.timed_occurrences > 0
|
|
131
|
+
? `${item.timed_occurrences} timed event(s)${typeof item.max_after_active_ms === 'number' ? `, the last at ${activeTime(item.max_after_active_ms)} of active time` : ''}`
|
|
132
|
+
: 'no timed events';
|
|
133
|
+
const mode = item.mode === 'world_only' && typeof item.continuation_seconds === 'number'
|
|
134
|
+
? `the world keeps evolving for up to ${activeTime(item.continuation_seconds * 1000)} without the agent`
|
|
135
|
+
: item.mode === 'pause_with_agent' ? 'the schedule pauses while the agent is disconnected' : terminalText(String(item.mode));
|
|
136
|
+
return ` ${terminalText(item.scenario_id)}: ${timed}; ${mode}`;
|
|
137
|
+
});
|
|
138
|
+
if (!lines.length)
|
|
139
|
+
return [];
|
|
140
|
+
return ['Scenario timing (real active time, never accelerated):', ...lines,
|
|
141
|
+
...items.some(item => item.mode === 'world_only')
|
|
142
|
+
? [' Approving this plan approves that world-only continuation. It never extends a deadline, funding or capacity, and never starts or resumes the agent.']
|
|
143
|
+
: []];
|
|
144
|
+
}
|
|
145
|
+
/** Human summary of an effect-free plan review. Values are service data, so every field is terminal-escaped. */
|
|
146
|
+
function reviewText(plan, requestKey) {
|
|
147
|
+
const review = plan.review;
|
|
148
|
+
const nested = review.changes && typeof review.changes === 'object' ? review.changes.changes : undefined;
|
|
149
|
+
const changes = Array.isArray(nested) ? nested : [];
|
|
150
|
+
const gaps = review.gaps;
|
|
151
|
+
const text = (value) => terminalText(typeof value === 'string' ? value : value === null || value === undefined ? '-' : JSON.stringify(value));
|
|
152
|
+
return [
|
|
153
|
+
`Plan ${plan.id}${plan.expired ? ' (expired)' : ''}`,
|
|
154
|
+
`World revision: ${plan.world_revision_id}`,
|
|
155
|
+
`Review digest: ${plan.review_digest}`,
|
|
156
|
+
`Expires: ${terminalText(plan.expires_at)}`,
|
|
157
|
+
typeof review.requested_drills === 'number' ? `Requested drills: ${review.requested_drills}` : null,
|
|
158
|
+
typeof review.requires_serial === 'boolean' ? `Serial execution: ${review.requires_serial ? 'yes' : 'no'}` : null,
|
|
159
|
+
changes.length ? 'Changes:' : 'Changes: none since the previous declaration',
|
|
160
|
+
...changes.map(change => ` ${text(change.change)} ${text(change.field)} ${text(change.identity)}`),
|
|
161
|
+
...laneLines(review),
|
|
162
|
+
...bindingLines(review),
|
|
163
|
+
...evolutionLines(review),
|
|
164
|
+
gaps.length ? 'Unresolved requirements (apply is refused until these are fixed):' : 'Unresolved requirements: none',
|
|
165
|
+
...gaps.map(gap => ` ${text(gap.code)}${gap.resource_id ? ` (${text(gap.resource_id)})` : ''}`
|
|
166
|
+
+ (typeof gap.code === 'string' && Object.hasOwn(GAP_TEXT, gap.code) ? `: ${GAP_TEXT[gap.code]}` : '')),
|
|
167
|
+
review.real_dependency_grants.length ? `Real-access grants still needed: ${review.real_dependency_grants.map(terminalText).join(', ')}` : null,
|
|
168
|
+
`Gates checked at apply: ${review.required_gates.map(terminalText).join(', ')}`,
|
|
169
|
+
'This review allocates nothing, starts nothing and spends no credits.',
|
|
170
|
+
requestKey ? `Request key: ${requestKey} (replay with --world-revision ${plan.world_revision_id} --request ${requestKey})` : null,
|
|
171
|
+
].filter((line) => line !== null).join('\n');
|
|
172
|
+
}
|
|
173
|
+
/** Service units as an exact decimal amount when the scale is a power of ten. */
|
|
174
|
+
export function amount(units, scale, denomination) {
|
|
175
|
+
const name = terminalText(denomination);
|
|
176
|
+
if (!/^10*$/.test(String(scale)))
|
|
177
|
+
return `${units} units (${scale} per ${name})`;
|
|
178
|
+
const digits = String(scale).length - 1;
|
|
179
|
+
const fraction = digits ? String(units % scale).padStart(digits, '0').replace(/0+$/, '') : '';
|
|
180
|
+
return `${Math.floor(units / scale)}${fraction ? `.${fraction}` : ''} ${name}`;
|
|
181
|
+
}
|
|
182
|
+
/** Plain words for root's quote components; a component not listed here is shown by its code. */
|
|
183
|
+
const COMPONENT_TEXT = {
|
|
184
|
+
participants: 'synthetic participants', evaluation: 'evaluation', tool_state: 'catalog tool state',
|
|
185
|
+
baseline: 'tool preparation', isolated_runtime: 'isolated custom tools',
|
|
186
|
+
};
|
|
187
|
+
/** A whole-second ceiling in the largest exact unit. */
|
|
188
|
+
function span(seconds) {
|
|
189
|
+
return seconds % 3600 === 0 ? `${seconds / 3600} h` : seconds % 60 === 0 ? `${seconds / 60} min` : `${seconds} s`;
|
|
190
|
+
}
|
|
191
|
+
/** The ceilings a quote freezes, in words. */
|
|
192
|
+
function quoteLimits(quote) {
|
|
193
|
+
if (quote.participant_limits)
|
|
194
|
+
return ` (${terminalText(quote.participant_limits.model)}, up to ${quote.participant_limits.max_steps} steps)`;
|
|
195
|
+
const limits = quote.isolated_limits;
|
|
196
|
+
if (!limits)
|
|
197
|
+
return '';
|
|
198
|
+
const memory = limits.memory_mb % 1024 === 0 ? `${limits.memory_mb / 1024} GiB` : `${limits.memory_mb} MiB`;
|
|
199
|
+
return ` (each tool per drill: up to ${span(limits.duration_seconds)} running plus up to ${span(limits.cleanup_seconds)} cleanup;`
|
|
200
|
+
+ ` ${limits.cpu_units} CPU, ${memory} memory, ${limits.disk_gb} GB disk)`;
|
|
201
|
+
}
|
|
202
|
+
/** Human summary of a cloud-work estimate. It reserves nothing and never charges local target compute. */
|
|
203
|
+
function estimateText(estimate) {
|
|
204
|
+
const money = (units) => amount(units, estimate.credit_scale, estimate.denomination);
|
|
205
|
+
return [
|
|
206
|
+
`Cloud-work estimate (maximum, nothing reserved): ${money(estimate.maximum_units)}`,
|
|
207
|
+
...estimate.quotes.map(quote => ` ${COMPONENT_TEXT[quote.component] ?? terminalText(quote.component)}: up to ${money(quote.maximum_units)}`
|
|
208
|
+
+ quoteLimits(quote)),
|
|
209
|
+
estimate.reservation_policy === 'baseline_then_admitted_drills' && estimate.initial_reservation_units !== null
|
|
210
|
+
? `Apply first reserves ${money(estimate.initial_reservation_units)} for tool preparation (the baseline); each drill reserves its share only when it is`
|
|
211
|
+
+ ' admitted, so the whole maximum is never held up front.'
|
|
212
|
+
: null,
|
|
213
|
+
`Local target compute is not charged. Estimate expires: ${terminalText(estimate.expires_at)}`,
|
|
214
|
+
].filter((line) => line !== null).join('\n');
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `plan --world <world-id> [--world-revision <id>] [--request <key>]` creates an
|
|
218
|
+
* effect-free review of the world's newest (or pinned) declaration;
|
|
219
|
+
* `plan show <plan-id>` reads a saved review.
|
|
220
|
+
*/
|
|
221
|
+
export async function planCommand(args, options, emit) {
|
|
222
|
+
if (args[0] === 'show') {
|
|
223
|
+
if (args.length !== 2 || options.world !== undefined || options.worldRevision !== undefined || options.request !== undefined)
|
|
224
|
+
throw new CliError('arguments_invalid', 'Use firedrill plan show <plan-id>.', 2);
|
|
225
|
+
const scope = await scopeOf(options);
|
|
226
|
+
const plan = await getApplyPlan(scope, { planId: args[1], signal: options.signal });
|
|
227
|
+
emit('plan', plan, reviewText(plan));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (args.length || !options.world)
|
|
231
|
+
throw new CliError('arguments_invalid', 'Use firedrill plan --world <world-id> [--world-revision <revision-id>], or firedrill plan show <plan-id>.', 2);
|
|
232
|
+
// A replay reviews the revision the original did; resolving the newest again could review a different declaration.
|
|
233
|
+
if (options.request !== undefined && options.worldRevision === undefined)
|
|
234
|
+
throw new CliError('arguments_invalid', 'A plan replay needs the world revision the original reviewed. Rerun with --world-revision <id> '
|
|
235
|
+
+ '(the plan\'s world_revision_id, printed with its request key) and the same --request. Nothing was submitted.', 2);
|
|
236
|
+
const scope = await scopeOf(options);
|
|
237
|
+
const revision = options.worldRevision !== undefined
|
|
238
|
+
? await getWorldRevision(scope, { worldId: options.world, revisionId: options.worldRevision, signal: options.signal })
|
|
239
|
+
: await latestWorldRevision(scope, { worldId: options.world, signal: options.signal });
|
|
240
|
+
const plan = await createApplyPlan(scope, { worldRevisionId: revision.id, worldDigest: revision.definition_digest,
|
|
241
|
+
requestKey: options.request, signal: options.signal });
|
|
242
|
+
// The estimate is informational here; apply requires it again before approval.
|
|
243
|
+
let estimate = null, estimateProblem = null;
|
|
244
|
+
if (!plan.review.gaps.length && !plan.expired) {
|
|
245
|
+
try {
|
|
246
|
+
estimate = await estimatePlanFunding(scope, { planId: plan.id, reviewDigest: plan.review_digest, signal: options.signal });
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (options.signal.aborted)
|
|
250
|
+
throw error;
|
|
251
|
+
estimateProblem = error instanceof CliError ? error.code : 'estimate_unavailable';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
emit('plan', { ...plan, estimate, estimate_error: estimateProblem }, reviewText(plan, plan.request_key)
|
|
255
|
+
+ (estimate ? `\n${estimateText(estimate)}` : estimateProblem ? `\nCloud-work estimate unavailable (${estimateProblem}).` : '')
|
|
256
|
+
+ (plan.review.gaps.length ? '' : `\nAfter the human approves it: firedrill apply ${plan.id} --approve --follow`));
|
|
257
|
+
}
|
|
258
|
+
const APPLY_FOLLOW_MS = 5_000;
|
|
259
|
+
const PREPARING = new Set(['accepted', 'queued', 'provisioning', 'reconciling']);
|
|
260
|
+
/** Plain words for a preparation state; none of them means a target may start. */
|
|
261
|
+
function preparationText(operation) {
|
|
262
|
+
const capacity = operation.capacity;
|
|
263
|
+
const words = {
|
|
264
|
+
accepted: 'approved; preparation credits reserved', queued: 'queued for preparation', provisioning: 'preparing cloud capacity',
|
|
265
|
+
reconciling: 'reconciling an uncertain preparation step', ready: 'ready', failed: 'failed', cancelled: 'cancelled',
|
|
266
|
+
};
|
|
267
|
+
return `Preparation ${operation.id}: ${words[operation.state]}`
|
|
268
|
+
+ (operation.failure_code ? ` (${terminalText(operation.failure_code)})` : '')
|
|
269
|
+
+ (capacity && capacity.reason ? `; capacity ${terminalText(capacity.state)}: ${terminalText(capacity.reason)}` : '');
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* A stable request key per preparation and purpose (for `funding`, per apply
|
|
273
|
+
* request key), so a rerun replays instead of asking twice.
|
|
274
|
+
*/
|
|
275
|
+
function operationKey(purpose, operationId) {
|
|
276
|
+
const hex = createHash('sha256').update(`firedrill.native-${purpose}.v1\0${operationId}`).digest('hex');
|
|
277
|
+
const variant = ((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16);
|
|
278
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
279
|
+
}
|
|
280
|
+
const preparedCancelled = (operation) => operation.state === 'cancelled' && operation.failure_code === 'prepared_unused_cancelled';
|
|
281
|
+
const unstartedCancelled = (operation) => operation.state === 'cancelled' && operation.failure_code === 'cancelled_before_creation';
|
|
282
|
+
/** The service alone proves no effects. A lost response never triggers an automatic second POST. */
|
|
283
|
+
async function cancelUnstarted(scope, plan, operation, requestKey, options, emit) {
|
|
284
|
+
const recovery = `firedrill apply ${plan.id} --request ${requestKey} --cancel`;
|
|
285
|
+
const effect = 'Cancels this queued preparation only if no preparation effect has started. Its unused credit hold is released. Saved records are retained; this does not stop a running agent or delete data.';
|
|
286
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
287
|
+
let approved = options.approve === true;
|
|
288
|
+
if (!approved && interactive) {
|
|
289
|
+
approved = await selectOne(`Cancel queued preparation ${operation.id}?`, [
|
|
290
|
+
{ value: false, label: 'Keep it', detail: 'Nothing is cancelled.' },
|
|
291
|
+
{ value: true, label: 'Cancel preparation', detail: effect },
|
|
292
|
+
], options.signal);
|
|
293
|
+
}
|
|
294
|
+
if (!approved)
|
|
295
|
+
throw new CliError('apply_cancel_approval_required', `${effect} Review it, then rerun: ${recovery} --approve. Nothing was cancelled.`, 2);
|
|
296
|
+
const cancelKey = operationKey('unstarted-cancel', operation.id);
|
|
297
|
+
emit('apply_cancel_request', { operation_id: operation.id, request_key: cancelKey, recovery_command: `${recovery} --approve` }, `Cancelling queued preparation ${operation.id}. If the response is interrupted, inspect the original and use the same recovery command: ${recovery} --approve.`);
|
|
298
|
+
let receipt;
|
|
299
|
+
try {
|
|
300
|
+
receipt = await cancelUnstartedApply(scope, { operationId: operation.id, requestKey: cancelKey, reviewDigest: plan.review_digest, signal: options.signal });
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (options.signal.aborted)
|
|
304
|
+
throw error;
|
|
305
|
+
const current = await getNativeApplyOperation(scope, { operationId: operation.id, signal: AbortSignal.timeout(15_000) }).catch(() => null);
|
|
306
|
+
if (current)
|
|
307
|
+
emit('apply_operation', current, unstartedCancelled(current)
|
|
308
|
+
? `${preparationText(current)}. The preparation is cancelled, but this read does not verify the cancellation receipt.`
|
|
309
|
+
: `${preparationText(current)}. The cancellation receipt was not confirmed.`);
|
|
310
|
+
// Even a cancelled readback cannot prove this request's receipt. A later
|
|
311
|
+
// explicit same-key invocation may recover it; never automatically retry.
|
|
312
|
+
if (error instanceof CliError && error.httpStatus !== null && error.httpStatus >= 400 && error.httpStatus < 500)
|
|
313
|
+
throw error;
|
|
314
|
+
throw new CliError('native_apply_cancel_unconfirmed', `The cancellation receipt was not confirmed. Read the original preparation; to recover the same cancellation receipt, use: ${recovery} --approve. No second cancellation was sent.`, 1);
|
|
315
|
+
}
|
|
316
|
+
emit('apply_cancelled', { ...receipt, plan_id: plan.id }, `Queued preparation ${receipt.operation_id} cancelled before effects started. Its unused credit hold was released. Saved records are retained. Receipt ${receipt.receipt_digest}.`);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* `apply <plan-id> --request <key> --cancel` cancels an explicitly eligible
|
|
320
|
+
* unstarted queue entry or a ready preparation without a campaign, after approval.
|
|
321
|
+
* Root releases the unused reserved
|
|
322
|
+
* run credits; the prepared data is retained for normal retention, not purged
|
|
323
|
+
* by this request. The cancellation key is derived from the preparation, so a
|
|
324
|
+
* rerun replays the original receipt instead of sending a second cancellation.
|
|
325
|
+
*/
|
|
326
|
+
async function cancelPrepared(scope, plan, operation, requestKey, options, emit) {
|
|
327
|
+
const recovery = `firedrill apply ${plan.id} --request ${requestKey} --cancel`;
|
|
328
|
+
if (operation.can_request_unstarted_cancellation || unstartedCancelled(operation)) {
|
|
329
|
+
return cancelUnstarted(scope, plan, operation, requestKey, options, emit);
|
|
330
|
+
}
|
|
331
|
+
if (preparedCancelled(operation)) {
|
|
332
|
+
emit('apply_operation', operation, `${preparationText(operation)}. It was already cancelled; nothing else was sent.`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!operation.can_request_prepared_cancellation) {
|
|
336
|
+
const why = operation.state === 'ready'
|
|
337
|
+
? 'It already has a campaign. Cancel that instead: firedrill simulation cancel <campaign-id>.'
|
|
338
|
+
: PREPARING.has(operation.state) ? `It is not ready yet. Wait with firedrill apply ${plan.id} --request ${requestKey} --follow, then cancel.`
|
|
339
|
+
: `It is already ${operation.state}.`;
|
|
340
|
+
throw new CliError('native_apply_cancel_unavailable', `${preparationText(operation)}. Only a ready preparation with no campaign can be cancelled here. ${why} Nothing was cancelled.`, 2);
|
|
341
|
+
}
|
|
342
|
+
const effect = 'Releases the unused reserved run credits for this prepared world (preparation usage is already settled). No campaign can be created from it afterwards. The prepared data is kept until normal retention removes it; this does not purge it.';
|
|
343
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
344
|
+
let approved = options.approve === true;
|
|
345
|
+
if (!approved && interactive) {
|
|
346
|
+
approved = await selectOne(`Cancel prepared world ${operation.id}?`, [
|
|
347
|
+
{ value: false, label: 'Keep it', detail: 'Nothing is cancelled.' },
|
|
348
|
+
{ value: true, label: 'Cancel it', detail: effect },
|
|
349
|
+
], options.signal);
|
|
350
|
+
}
|
|
351
|
+
if (!approved)
|
|
352
|
+
throw new CliError('apply_cancel_approval_required', `${effect} Review it, then rerun with --approve. Nothing was cancelled.`, 2);
|
|
353
|
+
const cancelKey = operationKey('prepared-cancel', operation.id);
|
|
354
|
+
emit('apply_cancel_request', { operation_id: operation.id, request_key: cancelKey, recovery_command: `${recovery} --approve` }, `Cancelling prepared world ${operation.id}. If this is interrupted, rerun: ${recovery} --approve (it replays the same request).`);
|
|
355
|
+
let receipt;
|
|
356
|
+
try {
|
|
357
|
+
receipt = await cancelPreparedApply(scope, { operationId: operation.id, requestKey: cancelKey, reviewDigest: plan.review_digest, signal: options.signal });
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (options.signal.aborted)
|
|
361
|
+
throw error;
|
|
362
|
+
// A rejected or unanswered request is resolved from the preparation itself, never by a second cancellation key.
|
|
363
|
+
const current = await getNativeApplyOperation(scope, { operationId: operation.id, signal: AbortSignal.timeout(15_000) }).catch(() => null);
|
|
364
|
+
if (current && preparedCancelled(current)) {
|
|
365
|
+
emit('apply_operation', current, `${preparationText(current)}. The cancellation is recorded${error instanceof CliError && error.code === 'idempotency_conflict' ? ' under an earlier request' : ''}; its receipt was not received here.`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (error instanceof CliError && error.httpStatus !== null && error.httpStatus >= 400 && error.httpStatus < 500)
|
|
369
|
+
throw error;
|
|
370
|
+
throw new CliError('native_apply_cancel_unconfirmed', `The cancellation may or may not have reached Firedrill. Rerun: ${recovery} --approve (the same request is replayed, never sent twice).`, 1);
|
|
371
|
+
}
|
|
372
|
+
emit('apply_cancelled', { ...receipt, plan_id: plan.id }, `Prepared world ${receipt.operation_id} cancelled. Unused reserved run credits were released. Its prepared data is kept until normal retention removes it. Receipt ${receipt.receipt_digest}.`);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* `apply <plan-id> [--approve] [--request <key>] [--follow]` re-reads the
|
|
376
|
+
* review, refuses expired or incomplete plans and asks for explicit approval
|
|
377
|
+
* with the maximum cloud-work cost in view. Approval reserves the preparation
|
|
378
|
+
* first (drill work is reserved in bounded waves as drills are admitted, within
|
|
379
|
+
* the approved maximum) and queues preparation; the command shows the real preparation state and,
|
|
380
|
+
* once the service reports it ready, creates the campaign (drill slots only).
|
|
381
|
+
* Nothing here starts a target: `firedrill run --campaign` does, on this machine.
|
|
382
|
+
* `--request` with the printed key reads the original preparation (no second
|
|
383
|
+
* approval or charge). When none is recorded yet, it reviews the same quotes
|
|
384
|
+
* again and, once approved, sends the same request under the same key, which
|
|
385
|
+
* the service applies at most once. `--follow` waits until it is ready, failed
|
|
386
|
+
* or cancelled.
|
|
387
|
+
*/
|
|
388
|
+
export async function applyCommand(args, options, emit) {
|
|
389
|
+
if (args.length !== 1 || options.world !== undefined || options.worldRevision !== undefined)
|
|
390
|
+
throw new CliError('arguments_invalid', 'Use firedrill apply <plan-id> with the plan ID returned by firedrill plan.', 2);
|
|
391
|
+
if (options.request !== undefined && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(options.request))
|
|
392
|
+
throw new CliError('arguments_invalid', 'Use --request with the apply request key Firedrill printed. Nothing was applied.', 2);
|
|
393
|
+
if (options.cancel && (options.request === undefined || options.follow))
|
|
394
|
+
throw new CliError('arguments_invalid', 'Use firedrill apply <plan-id> --request <key> --cancel with the apply request key Firedrill printed. Nothing was cancelled.', 2);
|
|
395
|
+
if (options.cancel && options.approveCredits !== undefined)
|
|
396
|
+
throw new CliError('arguments_invalid', '--approve-credits does not apply to cancellation. Nothing was cancelled.', 2);
|
|
397
|
+
const scope = await scopeOf(options);
|
|
398
|
+
const retry = { signal: options.signal, attempts: 4 };
|
|
399
|
+
const plan = await retryTransient(() => getApplyPlan(scope, { planId: args[0], signal: options.signal }), retry);
|
|
400
|
+
const requestKey = (options.request ?? randomUUID()).toLowerCase();
|
|
401
|
+
const recovery = `firedrill apply ${plan.id} --request ${requestKey}${options.follow ? ' --follow' : ''}`;
|
|
402
|
+
// A retained preparation under this key is read, never approved or charged again.
|
|
403
|
+
let operation = options.request ? await retryTransient(() => findNativeApplyOperation(scope, { requestKey, signal: options.signal }), retry) : null;
|
|
404
|
+
let landedFirst = false;
|
|
405
|
+
if (operation && operation.plan_id !== plan.id) {
|
|
406
|
+
throw new CliError('idempotency_conflict', 'That request key belongs to a different plan. Use the key printed for this plan. Nothing was applied.', 2);
|
|
407
|
+
}
|
|
408
|
+
if (options.cancel) {
|
|
409
|
+
if (!operation)
|
|
410
|
+
throw new CliError('native_apply_not_found', 'No preparation was found under that request key. Nothing was cancelled.', 2);
|
|
411
|
+
return cancelPrepared(scope, plan, operation, requestKey, options, emit);
|
|
412
|
+
}
|
|
413
|
+
if (!operation) {
|
|
414
|
+
if (plan.expired)
|
|
415
|
+
throw new CliError('native_plan_expired', 'This review expired. Run firedrill plan again. Nothing was applied.', 2);
|
|
416
|
+
if (plan.review.gaps.length) {
|
|
417
|
+
emit('plan', plan, reviewText(plan));
|
|
418
|
+
throw new CliError('native_plan_requirements_unmet', 'Resolve the listed requirements and create a new plan. Nothing was applied.', 2);
|
|
419
|
+
}
|
|
420
|
+
// A human approves with the maximum cloud-work cost in view; without an estimate nothing is applied.
|
|
421
|
+
// The estimate's key follows the apply key, so a rerun under it gets the original quotes back and sends the
|
|
422
|
+
// same request, which the service replays instead of refusing as a different budget under the same key.
|
|
423
|
+
const estimateKey = operationKey('funding', requestKey);
|
|
424
|
+
let estimate = await retryTransient(() => estimatePlanFunding(scope, { planId: plan.id, reviewDigest: plan.review_digest,
|
|
425
|
+
requestKey: estimateKey, signal: options.signal }), retry);
|
|
426
|
+
// Expired quotes can never be reserved, so a rerun reviews fresh ones instead of failing the same way every time.
|
|
427
|
+
if (options.request && !(Date.parse(estimate.expires_at) > Date.now())) {
|
|
428
|
+
const renewalKey = randomUUID();
|
|
429
|
+
estimate = await retryTransient(() => estimatePlanFunding(scope, { planId: plan.id, reviewDigest: plan.review_digest,
|
|
430
|
+
requestKey: renewalKey, signal: options.signal }), retry);
|
|
431
|
+
}
|
|
432
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
433
|
+
let approved = options.approve === true;
|
|
434
|
+
if (!approved && interactive)
|
|
435
|
+
process.stderr.write(reviewText(plan) + '\n');
|
|
436
|
+
emit('plan_estimate', estimate, estimateText(estimate));
|
|
437
|
+
if (!approved && interactive) {
|
|
438
|
+
approved = await selectOne(`Apply plan ${plan.id} to the selected workspace?`, [
|
|
439
|
+
{ value: false, label: 'Not now', detail: 'Nothing is applied and no credits are reserved.' },
|
|
440
|
+
{ value: true, label: 'Apply plan', detail: 'Reserves preparation now and drill work as drills are admitted, within the approved maximum above. Prepares this exact plan; no target starts.' },
|
|
441
|
+
], options.signal);
|
|
442
|
+
}
|
|
443
|
+
if (!approved) {
|
|
444
|
+
throw new CliError('apply_approval_required', options.request
|
|
445
|
+
? `No preparation is recorded under request ${requestKey} yet, so the original may not have arrived. After the human approves the estimate above, `
|
|
446
|
+
+ `rerun: ${recovery} --approve. It is sent under the same key, so it is never applied or charged twice. Nothing was applied.`
|
|
447
|
+
: 'Review the plan, then rerun with --approve. Nothing was applied.', 2);
|
|
448
|
+
}
|
|
449
|
+
const creditApproval = await approveCredits(estimate.quotes, options, interactive, `${recovery} --approve`);
|
|
450
|
+
// Printed before sending, so an interrupted or unanswered request can be recovered without a second charge.
|
|
451
|
+
emit('apply_request', { plan_id: plan.id, request_key: requestKey, recovery_command: recovery }, `Applying plan ${plan.id}. If this is interrupted, check it with: ${recovery}`);
|
|
452
|
+
try {
|
|
453
|
+
operation = await applyNativePlan(scope, { planId: plan.id, reviewDigest: plan.review_digest,
|
|
454
|
+
quoteIds: estimate.quotes.map(quote => quote.id), budgetDigest: estimate.digest, requestKey, signal: options.signal, creditApproval });
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (options.signal.aborted)
|
|
458
|
+
throw error;
|
|
459
|
+
if (error instanceof CliError && error.code === 'idempotency_conflict') {
|
|
460
|
+
// The key already holds a request with another budget: the original landed after the lookup above
|
|
461
|
+
// (or was sent with other quotes). Read and continue with it; nothing is applied or charged again.
|
|
462
|
+
const original = await findNativeApplyOperation(scope, { requestKey, signal: options.signal }).catch(() => undefined);
|
|
463
|
+
options.signal.throwIfAborted();
|
|
464
|
+
if (original === undefined) {
|
|
465
|
+
throw new CliError('native_apply_unconfirmed', `A preparation is already recorded under this request key, but it could not be read. Nothing else was applied. Check it with: ${recovery}`, 1);
|
|
466
|
+
}
|
|
467
|
+
if (original === null || original.plan_id !== plan.id)
|
|
468
|
+
throw error;
|
|
469
|
+
operation = original;
|
|
470
|
+
landedFirst = true;
|
|
471
|
+
}
|
|
472
|
+
else if (error instanceof CliError && error.httpStatus !== null && error.httpStatus >= 400 && error.httpStatus < 500) {
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
throw new CliError('native_apply_unconfirmed', `The apply request may or may not have reached Firedrill. Check it with: ${recovery}`, 1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!operation)
|
|
481
|
+
throw new CliError('native_apply_unconfirmed', `The apply result could not be read. Check it with: ${recovery}`, 1);
|
|
482
|
+
emit('apply_operation', operation, preparationText(operation)
|
|
483
|
+
+ (landedFirst ? '\nThe original request under this key landed first, so this is that preparation with its own approved budget; nothing was applied or charged again.' : ''));
|
|
484
|
+
while (options.follow && PREPARING.has(operation.state)) {
|
|
485
|
+
if (!await sleep(jitteredInterval(APPLY_FOLLOW_MS), options.signal))
|
|
486
|
+
break;
|
|
487
|
+
const operationId = operation.id;
|
|
488
|
+
const next = await retryTransient(() => getNativeApplyOperation(scope, { operationId, signal: options.signal }), { signal: options.signal, attempts: 4 });
|
|
489
|
+
if (next.state !== operation.state || next.state_revision !== operation.state_revision)
|
|
490
|
+
emit('apply_operation', next, preparationText(next));
|
|
491
|
+
operation = next;
|
|
492
|
+
}
|
|
493
|
+
options.signal.throwIfAborted();
|
|
494
|
+
if (operation.state === 'failed' || operation.state === 'cancelled') {
|
|
495
|
+
throw new CliError(operation.failure_code && /^[a-z0-9_]{1,64}$/.test(operation.failure_code) ? operation.failure_code : `native_preparation_${operation.state}`, `Preparation ${operation.state}. Reserved credits are released by the service. Create a new plan to try again.`, 1);
|
|
496
|
+
}
|
|
497
|
+
if (operation.state !== 'ready') {
|
|
498
|
+
emit('apply_pending', { operation_id: operation.id, state: operation.state, recovery_command: `firedrill apply ${plan.id} --request ${requestKey} --follow` }, `Not ready yet; no target has started. Check again with: firedrill apply ${plan.id} --request ${requestKey} --follow\n`
|
|
499
|
+
+ `Rerunning that creates its campaign once ready. If it is no longer wanted, once ready release its unused credits instead: firedrill apply ${plan.id} --request ${requestKey} --cancel`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// Ready: allocate the campaign's drill slots. Still nothing starts until a runner on this machine takes them.
|
|
503
|
+
const campaign = await createNativeCampaign(scope, { operationId: operation.id, requestKey: operationKey('campaign', operation.id), signal: options.signal });
|
|
504
|
+
const runCommand = `firedrill run --plan <path> --descriptor <path> --campaign ${campaign.campaign_id}`;
|
|
505
|
+
emit('campaign_created', { ...campaign, operation_id: operation.id, next_command: runCommand }, `Campaign ${campaign.campaign_id}: ${campaign.planned_drills} drill(s) allocated; none started.\nRun them on this machine: ${runCommand}\nWatch: firedrill watch ${campaign.campaign_id} --follow`);
|
|
506
|
+
}
|