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,526 @@
|
|
|
1
|
+
// `firedrill tools definitions [list] | show | recipients | grants | share |
|
|
2
|
+
// share-request | withdraw-share-request | revoke | copy | copy-request |
|
|
3
|
+
// withdraw-copy-request | copy-status | withdraw-copy | dependents`: reusing a
|
|
4
|
+
// tool definition across workspaces of one organization.
|
|
5
|
+
//
|
|
6
|
+
// - Sharing grants one exact inspected tool version to another workspace, read
|
|
7
|
+
// only or read and copy. Only an administrator of the owner workspace shares,
|
|
8
|
+
// recovers or withdraws a sharing request, or revokes, and only after the
|
|
9
|
+
// human approves. A grant shares the definition only: never a running
|
|
10
|
+
// instance, session, state, credentials or runtime rights.
|
|
11
|
+
// - Using a shared version always needs an exact private copy. The package is
|
|
12
|
+
// copied server-side into this workspace, then inspected here like any
|
|
13
|
+
// upload. A copy is usable only once that inspection accepted it with a new
|
|
14
|
+
// private tool version; "copied" alone is not ready. This is not a tool's
|
|
15
|
+
// state fork (`tools state fork`).
|
|
16
|
+
// - Request keys are printed before anything is sent. The same key returns the
|
|
17
|
+
// original; a lost answer is read back or withdrawn, never sent under a new key.
|
|
18
|
+
// Nothing here starts an instance, runs anything or spends anything.
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { CliError } from '../http.js';
|
|
21
|
+
import { selectOne, terminalText } from '../prompts.js';
|
|
22
|
+
import { selectedWorkspace } from '../world-access.js';
|
|
23
|
+
import { errorCode, retryTransient } from '../native/control-plane.js';
|
|
24
|
+
import { answerLost } from '../native/metadata-requests.js';
|
|
25
|
+
import { checkCopy, copyStatus, copyToolDefinition, getToolCopy, getToolDefinition, listToolDefinitions, listToolDependents, listToolGrants, listToolRecipients, recoverToolCopy, recoverToolGrant, revokeToolGrant, shareToolDefinition, toolReuseId, withdrawToolCopy, withdrawToolCopyRequest, withdrawToolGrantRequest, } from '../native/tool-reuse.js';
|
|
26
|
+
import { answeredRefusal } from '../native/world-reuse.js';
|
|
27
|
+
/** `firedrill tools <TOOL_REUSE_VERB> ...` is this module's command group. */
|
|
28
|
+
export const TOOL_REUSE_VERB = 'definitions';
|
|
29
|
+
const RECOVER_MS = 15_000;
|
|
30
|
+
/** `--kind` names: a copy is what root calls a definition fork. */
|
|
31
|
+
const DEPENDENT_KINDS = { world: 'world', copy: 'fork' };
|
|
32
|
+
const CMD = `firedrill tools ${TOOL_REUSE_VERB}`;
|
|
33
|
+
const tty = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
34
|
+
const scopeOf = (options) => selectedWorkspace({ workspace: options.workspace, signal: options.signal,
|
|
35
|
+
interactive: tty(options), openBrowser: options.openBrowser && !options.json });
|
|
36
|
+
const sharedHere = (scope, row) => row.owner_workspace_id !== scope.workspace.toLowerCase();
|
|
37
|
+
const rethrow = (error) => { throw error; };
|
|
38
|
+
function pageNumber(value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!/^[1-9][0-9]{0,6}$/.test(value))
|
|
42
|
+
throw new CliError('arguments_invalid', 'Use --page with a number of 1 or more.', 2);
|
|
43
|
+
return Number(value);
|
|
44
|
+
}
|
|
45
|
+
/** Who owns a tool version and what this workspace may do with it. */
|
|
46
|
+
export function toolAccess(scope, row) {
|
|
47
|
+
const state = row.state === 'inspected' ? '' : `; ${row.state}, so it cannot be copied`;
|
|
48
|
+
if (sharedHere(scope, row)) {
|
|
49
|
+
return `shared from workspace ${row.owner_workspace_id}: ${row.can_fork ? 'read and copy privately' : 'read only'}${state}`;
|
|
50
|
+
}
|
|
51
|
+
return `owned by this workspace${state}`;
|
|
52
|
+
}
|
|
53
|
+
/** Why a tool version cannot be copied here, with the one step that changes that. */
|
|
54
|
+
function uncopyableText(row) {
|
|
55
|
+
if (row.state !== 'inspected')
|
|
56
|
+
return `This tool version is ${row.state}, so it cannot be copied.`;
|
|
57
|
+
return `Workspace ${row.owner_workspace_id} shared this tool version read only, so it cannot be copied here. Ask its owner for read-and-copy access.`;
|
|
58
|
+
}
|
|
59
|
+
function definitionLine(scope, row) {
|
|
60
|
+
return ` ${row.id} ${terminalText(row.name)} ${terminalText(row.version)} (${terminalText(row.key)}) ${toolAccess(scope, row)}`;
|
|
61
|
+
}
|
|
62
|
+
function grantLine(grant) {
|
|
63
|
+
return ` ${grant.id} workspace ${grant.recipient_workspace_id} ${grant.allow_use ? 'read and copy' : 'read only'} `
|
|
64
|
+
+ `shared ${terminalText(grant.created_at)}${grant.revoked_at ? ` revoked ${terminalText(grant.revoked_at)}` : ''}`;
|
|
65
|
+
}
|
|
66
|
+
function copyText(copy, lead) {
|
|
67
|
+
const status = copyStatus(copy);
|
|
68
|
+
return [
|
|
69
|
+
`${lead}: copy ${copy.id} of tool version ${copy.source_implementation_id}: ${status.text}.`,
|
|
70
|
+
`Pinned to archive ${copy.archive_sha256} and contract ${copy.contract_digest}. Nothing was started.`,
|
|
71
|
+
copy.cleanup_pending ? 'Its unused private package is still being cleaned up.' : null,
|
|
72
|
+
status.ready ? `Use private tool version ${copy.upload.implementation_id} like any tool version of this workspace.`
|
|
73
|
+
: copy.state === 'pending' || copy.state === 'copying' || copy.state === 'copied' ? `Check it again with: ${CMD} copy-status ${copy.id}` : null,
|
|
74
|
+
copy.state === 'pending' ? `Stop it while it is pending with: ${CMD} withdraw-copy ${copy.id}` : null,
|
|
75
|
+
`Request key (rerun with --request-key to replay): ${copy.request_key}`,
|
|
76
|
+
].filter((line) => line !== null).join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** Root answers 404 for a page past the last one; that is not a missing version. */
|
|
79
|
+
const pastEnd = (page, otherwise) => (error) => {
|
|
80
|
+
if ((page ?? 1) > 1 && error instanceof CliError && error.httpStatus === 404) {
|
|
81
|
+
throw new CliError('page_not_found', `There is no page ${page}. Start again from --page 1.`, 2);
|
|
82
|
+
}
|
|
83
|
+
return otherwise(error);
|
|
84
|
+
};
|
|
85
|
+
/** Only an organization administrator shares, recovers or withdraws a sharing request, revokes or lists recipients. */
|
|
86
|
+
const adminOnly = (error) => {
|
|
87
|
+
if (errorCode(error) === 'admin_required') {
|
|
88
|
+
throw new CliError('native_tool_admin_required', 'Only an administrator of this organization can share tool versions, recover or withdraw a '
|
|
89
|
+
+ 'sharing request, revoke sharing or list recipient workspaces. Nothing was shared or changed.', 3, error instanceof CliError ? error.httpStatus : null);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
};
|
|
93
|
+
const notReadable = (error) => {
|
|
94
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
95
|
+
throw new CliError('native_tool_definition_not_found', `That is not an available tool version this workspace can read. Find them with ${CMD}.`, 2);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
};
|
|
99
|
+
/** Grants and sharing requests are reached only through the workspace that owns the version; any other gets 404. */
|
|
100
|
+
const ownerOnly = (error) => {
|
|
101
|
+
if (errorCode(error) === 'admin_required')
|
|
102
|
+
return adminOnly(error);
|
|
103
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
104
|
+
throw new CliError('native_tool_definition_not_found', 'That is not an available tool version this workspace owns. Only the owner workspace lists '
|
|
105
|
+
+ `a version's grants or shares it; find this workspace's versions with ${CMD}. A grant of a version that is no longer available can still be revoked: `
|
|
106
|
+
+ `${CMD} revoke <tool-version-id> <grant-id>.`, 2);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
};
|
|
110
|
+
const notMine = (error) => {
|
|
111
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
112
|
+
throw new CliError('native_tool_copy_not_found', `That is not one of your tool copies in this workspace. List them with ${CMD} dependents <tool-version-id> --kind copy.`, 2);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
};
|
|
116
|
+
/** Root's original-grant read is administrator-only, so reading a fresh key confirms that before the human is asked. It changes nothing. */
|
|
117
|
+
const confirmAdministrator = (scope, implementationId, signal) => recoverToolGrant(scope, { implementationId, requestKey: randomUUID(), signal }).catch(ownerOnly);
|
|
118
|
+
async function approval(options, question, yes, no) {
|
|
119
|
+
if (options.approve === true)
|
|
120
|
+
return true;
|
|
121
|
+
if (!tty(options))
|
|
122
|
+
return false;
|
|
123
|
+
return selectOne(question, [{ value: false, label: 'Not now', detail: no }, { value: true, ...yes }], options.signal);
|
|
124
|
+
}
|
|
125
|
+
/** Each verb's arguments and flags, checked before anything is read or sent. */
|
|
126
|
+
export function checkToolReuseArguments(args, options) {
|
|
127
|
+
const verb = args[0] ?? 'list';
|
|
128
|
+
const rest = args[0] === undefined ? [] : args.slice(1);
|
|
129
|
+
const takes = {
|
|
130
|
+
list: ['search', 'page'], show: [], recipients: ['search', 'page'], grants: ['page'], share: ['request', 'approve'], 'share-request': [],
|
|
131
|
+
'withdraw-share-request': ['approve'], revoke: ['approve'], copy: ['request', 'approve'], 'copy-request': [], 'withdraw-copy-request': ['approve'],
|
|
132
|
+
'copy-status': [], 'withdraw-copy': ['approve'], dependents: ['kind', 'page'],
|
|
133
|
+
};
|
|
134
|
+
const positionals = {
|
|
135
|
+
list: [], show: ['tool version id'], recipients: [], grants: ['tool version id'], share: ['tool version id', 'workspace id', 'access'],
|
|
136
|
+
'share-request': ['tool version id', 'request key'], 'withdraw-share-request': ['tool version id', 'request key'],
|
|
137
|
+
revoke: ['tool version id', 'grant id'], copy: ['tool version id'], 'copy-request': ['request key'], 'withdraw-copy-request': ['request key'],
|
|
138
|
+
'copy-status': ['copy id'], 'withdraw-copy': ['copy id'], dependents: ['tool version id'],
|
|
139
|
+
};
|
|
140
|
+
const flags = { json: null, signal: null, workspace: null, openBrowser: null, search: '--search',
|
|
141
|
+
page: '--page', kind: '--kind', request: '--request-key', approve: '--approve-tool-change' };
|
|
142
|
+
const usage = () => new CliError('arguments_invalid', `Use ${CMD} [--search <prefix>], ${CMD} show <tool-version-id>, ${CMD} recipients, `
|
|
143
|
+
+ `${CMD} grants <tool-version-id>, ${CMD} share <tool-version-id> <workspace-id> read|use, ${CMD} share-request|withdraw-share-request `
|
|
144
|
+
+ `<tool-version-id> <request-key>, ${CMD} revoke <tool-version-id> <grant-id>, ${CMD} copy <tool-version-id>, ${CMD} copy-request|`
|
|
145
|
+
+ `withdraw-copy-request <request-key>, ${CMD} copy-status|withdraw-copy <copy-id> or ${CMD} dependents <tool-version-id> [--kind world|copy].`, 2);
|
|
146
|
+
if (!Object.hasOwn(takes, verb) || rest.length !== positionals[verb].length)
|
|
147
|
+
throw usage();
|
|
148
|
+
const extra = Object.keys(flags)
|
|
149
|
+
.filter((flag) => flags[flag] !== null && options[flag] !== undefined && options[flag] !== false && !takes[verb].includes(flag));
|
|
150
|
+
if (extra.length)
|
|
151
|
+
throw new CliError('arguments_invalid', `${CMD} ${verb} does not take ${extra.map((flag) => flags[flag]).join(', ')}.`, 2);
|
|
152
|
+
positionals[verb].forEach((label, index) => {
|
|
153
|
+
if (label === 'access') {
|
|
154
|
+
if (rest[index] !== 'read' && rest[index] !== 'use')
|
|
155
|
+
throw usage();
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
toolReuseId(rest[index], label);
|
|
159
|
+
});
|
|
160
|
+
if (options.request !== undefined)
|
|
161
|
+
toolReuseId(options.request, 'request key');
|
|
162
|
+
if (options.kind !== undefined && !Object.hasOwn(DEPENDENT_KINDS, options.kind)) {
|
|
163
|
+
throw new CliError('arguments_invalid', `Use --kind ${Object.keys(DEPENDENT_KINDS).join('|')} with ${CMD} dependents.`, 2);
|
|
164
|
+
}
|
|
165
|
+
if (options.search !== undefined && [...options.search].length > 120)
|
|
166
|
+
throw new CliError('arguments_invalid', 'Use a name prefix of up to 120 characters.', 2);
|
|
167
|
+
pageNumber(options.page);
|
|
168
|
+
}
|
|
169
|
+
/** `share <tool-version-id> <workspace-id> read|use [--request-key <key>] [--approve-tool-change]`. */
|
|
170
|
+
async function share(scope, args, options, emit) {
|
|
171
|
+
const [, version, recipientArg, mode] = args;
|
|
172
|
+
const signal = options.signal;
|
|
173
|
+
const recipient = toolReuseId(recipientArg, 'workspace id');
|
|
174
|
+
if (recipient === scope.workspace.toLowerCase()) {
|
|
175
|
+
throw new CliError('arguments_invalid', 'Share with another workspace of this organization; this one already owns the tool. Nothing was sent.', 2);
|
|
176
|
+
}
|
|
177
|
+
const allowUse = mode === 'use';
|
|
178
|
+
const access = allowUse ? 'read and copy privately' : 'read only';
|
|
179
|
+
const requestKey = options.request === undefined ? randomUUID() : toolReuseId(options.request, 'request key');
|
|
180
|
+
const versionId = toolReuseId(version, 'tool version id');
|
|
181
|
+
// A grant already made under this key is shown, never made again. It is read
|
|
182
|
+
// before the version itself: after a lost answer the version may no longer be
|
|
183
|
+
// available, but its original grant still is.
|
|
184
|
+
const original = options.request === undefined ? null
|
|
185
|
+
: await recoverToolGrant(scope, { implementationId: versionId, requestKey, signal }).catch(ownerOnly);
|
|
186
|
+
if (original) {
|
|
187
|
+
if (original.recipient_workspace_id !== recipient || original.allow_use !== allowUse) {
|
|
188
|
+
throw new CliError('idempotency_conflict', `Request ${requestKey} shared this tool version with workspace ${original.recipient_workspace_id} `
|
|
189
|
+
+ `(${original.allow_use ? 'read and copy' : 'read only'}). Nothing was changed; share differently under a new request key.`, 2);
|
|
190
|
+
}
|
|
191
|
+
emit('tool_definition_grant', original, `Already shared under request ${requestKey}:\n${grantLine(original)}`
|
|
192
|
+
+ (original.revoked_at ? '\nThis grant was revoked. A request key is never renewed: to share again, start without --request-key.' : ''));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const row = await getToolDefinition(scope, { implementationId: versionId, signal }).catch(notReadable);
|
|
196
|
+
const rerun = `${CMD} share ${row.id} ${recipient} ${mode} --request-key ${requestKey}`;
|
|
197
|
+
const read = `${CMD} share-request ${row.id} ${requestKey}`;
|
|
198
|
+
if (sharedHere(scope, row)) {
|
|
199
|
+
throw new CliError('native_tool_share_owner_only', `Only workspace ${row.owner_workspace_id}, which owns this tool version, can share it. Nothing was sent.`, 2);
|
|
200
|
+
}
|
|
201
|
+
// The original-request read is administrator-only, so it confirms that before
|
|
202
|
+
// the human is asked. A supplied key was already read above.
|
|
203
|
+
if (options.request === undefined)
|
|
204
|
+
await confirmAdministrator(scope, row.id, signal);
|
|
205
|
+
emit('tool_definition_share_review', { version: row, recipient_workspace_id: recipient, allow_use: allowUse, request_key: requestKey }, [
|
|
206
|
+
`Share tool ${terminalText(row.name)} ${terminalText(row.version)} (version ${row.id}) with workspace ${recipient}: ${access}.`,
|
|
207
|
+
allowUse ? 'It can read this exact definition and copy its package into a private version of its own, which is inspected there before use.'
|
|
208
|
+
: 'It can read this exact definition (services, operations and inspection), but not copy it.',
|
|
209
|
+
'Never shared: running instances, sessions, saved state, credentials or runtime rights. It can never edit this version.',
|
|
210
|
+
'Revoking later stops new reads and copies, including a copy still pending or copying; copies that finished copying stay its own.',
|
|
211
|
+
row.state === 'inspected' ? null : `This version is ${row.state}, so that workspace cannot copy it.`,
|
|
212
|
+
].filter((line) => line !== null).join('\n'));
|
|
213
|
+
const approved = await approval(options, `Share this tool version with workspace ${recipient} (${access})?`, { label: 'Share it', detail: `Grants ${access} access to this exact definition only.` }, 'Nothing is shared.');
|
|
214
|
+
if (!approved)
|
|
215
|
+
throw new CliError('tool_share_consent_required', `Nothing was shared. After the human approves: ${rerun} --approve-tool-change`, 2);
|
|
216
|
+
if (options.json)
|
|
217
|
+
emit('request_prepared', { request_key: requestKey }, '');
|
|
218
|
+
else
|
|
219
|
+
process.stderr.write(`Request key: ${requestKey} (if interrupted, rerun ${rerun} --approve-tool-change; ${read} reads it back)\n`);
|
|
220
|
+
let grant;
|
|
221
|
+
try {
|
|
222
|
+
// The same key and body replay the original grant, so a retry never shares twice.
|
|
223
|
+
grant = await retryTransient(() => shareToolDefinition(scope, { implementationId: row.id, recipientWorkspaceId: recipient, allowUse, requestKey, signal }), { signal, attempts: 3 });
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (errorCode(error) === 'admin_required')
|
|
227
|
+
adminOnly(error);
|
|
228
|
+
if (errorCode(error) === 'native_tool_definition_request_withdrawn') {
|
|
229
|
+
throw new CliError('native_tool_definition_request_withdrawn', `Sharing request ${requestKey} was withdrawn, so it will never land. Share again `
|
|
230
|
+
+ 'without --request-key only if the human still wants it.', 2);
|
|
231
|
+
}
|
|
232
|
+
if (errorCode(error) === 'native_tool_definition_grant_exists') {
|
|
233
|
+
throw new CliError('native_tool_definition_grant_exists', `Workspace ${recipient} already has an active grant for this version. List it with `
|
|
234
|
+
+ `${CMD} grants ${row.id}; to change its access, revoke it first. Nothing was shared.`, 2);
|
|
235
|
+
}
|
|
236
|
+
// A refusal with the service's own code wrote nothing; only an answer that never arrived is recovered by its key.
|
|
237
|
+
if (signal.aborted || !answerLost(error) || answeredRefusal(error)) {
|
|
238
|
+
if (error instanceof CliError && error.httpStatus === 404) {
|
|
239
|
+
throw new CliError('native_tool_recipient_unavailable', 'That workspace is not another active workspace of this organization, or this tool '
|
|
240
|
+
+ `version is no longer available. Find recipients with ${CMD} recipients. Nothing was shared.`, 2);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
const recovered = await recoverToolGrant(scope, { implementationId: row.id, requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
245
|
+
if (!recovered) {
|
|
246
|
+
throw new CliError('native_tool_share_unconfirmed', `The share was not confirmed and may still land. Read it with ${read}, or rerun ${rerun} `
|
|
247
|
+
+ '--approve-tool-change, which returns the original instead of sharing twice. Before sharing differently, withdraw it: '
|
|
248
|
+
+ `${CMD} withdraw-share-request ${row.id} ${requestKey} --approve-tool-change. Never share it again under a new key.`);
|
|
249
|
+
}
|
|
250
|
+
if (recovered.recipient_workspace_id !== recipient || recovered.allow_use !== allowUse) {
|
|
251
|
+
throw new CliError('native_response_unconfirmed', `Request ${requestKey} recorded a different grant, not this share. Nothing else was sent. `
|
|
252
|
+
+ `Read it with ${read}.`, 2);
|
|
253
|
+
}
|
|
254
|
+
grant = recovered;
|
|
255
|
+
}
|
|
256
|
+
emit('tool_definition_grant', grant, `Shared tool ${terminalText(row.name)} ${terminalText(row.version)} with workspace ${grant.recipient_workspace_id}: `
|
|
257
|
+
+ `${grant.allow_use ? 'read and copy privately' : 'read only'}.\n${grantLine(grant)}\nRevoke it with ${CMD} revoke ${row.id} ${grant.id}`);
|
|
258
|
+
}
|
|
259
|
+
/** `withdraw-share-request <tool-version-id> <request-key> [--approve-tool-change]`. */
|
|
260
|
+
async function withdrawShareRequest(scope, implementationId, requestKey, options, emit) {
|
|
261
|
+
const signal = options.signal;
|
|
262
|
+
const landed = (grant) => `This sharing request already landed, so nothing was withdrawn:\n${grantLine(grant)}\n`
|
|
263
|
+
+ (grant.revoked_at ? 'It is already revoked.' : `Revoke it instead: ${CMD} revoke ${implementationId} ${grant.id} --approve-tool-change`);
|
|
264
|
+
const original = await recoverToolGrant(scope, { implementationId, requestKey, signal }).catch(ownerOnly);
|
|
265
|
+
if (original) {
|
|
266
|
+
emit('tool_definition_grant', original, landed(original));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const approved = await approval(options, `Withdraw sharing request ${requestKey} so it can never land?`, { label: 'Withdraw', detail: 'If it already landed, the grant is shown instead and kept.' }, 'Nothing is changed.');
|
|
270
|
+
if (!approved) {
|
|
271
|
+
throw new CliError('tool_share_consent_required', `Nothing was withdrawn. After the human approves: ${CMD} withdraw-share-request ${implementationId} `
|
|
272
|
+
+ `${requestKey} --approve-tool-change`, 2);
|
|
273
|
+
}
|
|
274
|
+
const outcome = await retryTransient(() => withdrawToolGrantRequest(scope, { implementationId, requestKey, signal }), { signal, attempts: 3 })
|
|
275
|
+
.catch(ownerOnly);
|
|
276
|
+
if (outcome.state === 'committed') {
|
|
277
|
+
emit('tool_definition_grant', outcome.receipt, landed(outcome.receipt));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
emit('tool_definition_share_withdrawal', { state: 'withdrawn', implementation_id: implementationId, request_key: requestKey }, `Withdrawn. Sharing request ${requestKey} will never land. Share again without --request-key only if the human still wants it.`);
|
|
281
|
+
}
|
|
282
|
+
/** `revoke <tool-version-id> <grant-id> [--approve-tool-change]`. */
|
|
283
|
+
async function revoke(scope, implementationId, grantId, options, emit) {
|
|
284
|
+
const signal = options.signal;
|
|
285
|
+
const missing = () => new CliError('native_tool_grant_not_found', 'That is not a sharing grant of a tool version this workspace owns. Nothing was '
|
|
286
|
+
+ `changed. List them with ${CMD} grants ${implementationId}.`, 2);
|
|
287
|
+
// Revoked by its exact id: root's revoke needs no listing (which also skips
|
|
288
|
+
// versions no longer available), and grant history is never searched.
|
|
289
|
+
await confirmAdministrator(scope, implementationId, signal);
|
|
290
|
+
const approved = await approval(options, `Revoke sharing grant ${grantId} of tool version ${implementationId}?`, { label: 'Revoke', detail: 'Stops that workspace\'s new reads and copies, including a copy still pending or copying. '
|
|
291
|
+
+ 'Copies that finished copying are its own and are not removed.' }, 'Nothing is changed.');
|
|
292
|
+
if (!approved) {
|
|
293
|
+
throw new CliError('tool_share_consent_required', `Nothing was changed. After the human approves: ${CMD} revoke ${implementationId} ${grantId} `
|
|
294
|
+
+ '--approve-tool-change', 2);
|
|
295
|
+
}
|
|
296
|
+
const revoked = await retryTransient(() => revokeToolGrant(scope, { implementationId, grantId, signal }), { signal, attempts: 3 })
|
|
297
|
+
.catch((error) => {
|
|
298
|
+
if (error instanceof CliError && error.httpStatus === 404)
|
|
299
|
+
throw missing();
|
|
300
|
+
return ownerOnly(error);
|
|
301
|
+
});
|
|
302
|
+
// Idempotent: a grant revoked earlier is returned as it is, with its original revocation time.
|
|
303
|
+
emit('tool_definition_grant', revoked, `Grant ${revoked.id} is revoked (since ${terminalText(revoked.revoked_at)}).\n${grantLine(revoked)}\n`
|
|
304
|
+
+ `Workspace ${revoked.recipient_workspace_id} can no longer read or copy this tool version, and a copy of it still pending or copying stops as rejected. `
|
|
305
|
+
+ 'Copies that finished copying stay its own.');
|
|
306
|
+
}
|
|
307
|
+
/** `copy <tool-version-id> [--request-key <key>] [--approve-tool-change]`. */
|
|
308
|
+
async function copy(scope, sourceId, options, emit) {
|
|
309
|
+
const signal = options.signal;
|
|
310
|
+
const requestKey = options.request === undefined ? randomUUID() : toolReuseId(options.request, 'request key');
|
|
311
|
+
// A copy already made under this key is shown, never made again. Root
|
|
312
|
+
// returns it before checking current access, so this works after a revoke.
|
|
313
|
+
if (options.request !== undefined) {
|
|
314
|
+
const original = await recoverToolCopy(scope, { requestKey, signal });
|
|
315
|
+
if (original) {
|
|
316
|
+
if (original.source_implementation_id !== sourceId) {
|
|
317
|
+
throw new CliError('idempotency_conflict', `Request ${requestKey} copied tool version ${original.source_implementation_id}, not ${sourceId}. `
|
|
318
|
+
+ 'Nothing was sent; copy this version without --request-key.', 2);
|
|
319
|
+
}
|
|
320
|
+
emit('tool_definition_copy', original, copyText(original, 'Already started under this request key'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const source = await getToolDefinition(scope, { implementationId: sourceId, signal }).catch(notReadable);
|
|
325
|
+
if (!source.can_fork)
|
|
326
|
+
throw new CliError('native_tool_copy_not_allowed', `${uncopyableText(source)} Nothing was sent.`, 2);
|
|
327
|
+
const rerun = `${CMD} copy ${source.id} --request-key ${requestKey}`;
|
|
328
|
+
emit('tool_definition_copy_review', { source, request_key: requestKey }, [
|
|
329
|
+
`Copy tool ${terminalText(source.name)} ${terminalText(source.version)} (version ${source.id}`
|
|
330
|
+
+ `${sharedHere(scope, source) ? `, shared from workspace ${source.owner_workspace_id}` : ''}) into a private package of this workspace.`,
|
|
331
|
+
`The copy is exact: archive ${source.archive_sha256}, contract ${source.contract_digest}.`,
|
|
332
|
+
'This workspace then inspects it like any upload; only an accepted copy becomes a usable private tool version.',
|
|
333
|
+
'Nothing is started. No running instance, session, saved state, credentials or runtime rights come with it.',
|
|
334
|
+
// The copy worker re-checks the grant and the source until the package is copied.
|
|
335
|
+
'Once copied, later changes or a revoke at the source do not touch the copy. Until then, a revoke or the source becoming unavailable '
|
|
336
|
+
+ 'stops it as rejected.',
|
|
337
|
+
].join('\n'));
|
|
338
|
+
const approved = await approval(options, 'Copy this tool version into a private package of this workspace?', { label: 'Copy it', detail: 'Copies the exact package here, then inspects it.' }, 'Nothing is copied.');
|
|
339
|
+
if (!approved)
|
|
340
|
+
throw new CliError('tool_copy_consent_required', `Nothing was copied. After the human approves: ${rerun} --approve-tool-change`, 2);
|
|
341
|
+
if (options.json)
|
|
342
|
+
emit('request_prepared', { request_key: requestKey }, '');
|
|
343
|
+
else
|
|
344
|
+
process.stderr.write(`Request key: ${requestKey} (if interrupted, rerun ${rerun} --approve-tool-change; ${CMD} copy-request ${requestKey} reads it back)\n`);
|
|
345
|
+
let result;
|
|
346
|
+
try {
|
|
347
|
+
// The same key returns the original copy, so a retry never copies twice.
|
|
348
|
+
result = await retryTransient(() => copyToolDefinition(scope, { source, requestKey, signal }), { signal, attempts: 3 });
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (errorCode(error) === 'native_tool_definition_request_withdrawn') {
|
|
352
|
+
throw new CliError('native_tool_definition_request_withdrawn', `Copy request ${requestKey} was withdrawn, so it will never land. Copy again without `
|
|
353
|
+
+ '--request-key only if the human still wants it.', 2);
|
|
354
|
+
}
|
|
355
|
+
// A refusal with the service's own code wrote nothing; only an answer that never arrived is recovered by its key.
|
|
356
|
+
if (signal.aborted || !answerLost(error) || answeredRefusal(error)) {
|
|
357
|
+
if ((error instanceof CliError && error.httpStatus === 404) || errorCode(error) === 'native_tool_definition_unavailable') {
|
|
358
|
+
throw new CliError('native_tool_copy_unavailable', 'This tool version can no longer be copied here: its share was revoked or it is no longer '
|
|
359
|
+
+ 'available. Nothing was copied.', 2);
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
const recovered = await recoverToolCopy(scope, { requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
|
|
364
|
+
if (!recovered) {
|
|
365
|
+
throw new CliError('native_tool_copy_unconfirmed', `The copy was not confirmed and may still start (request ${requestKey}). Read it with `
|
|
366
|
+
+ `${CMD} copy-request ${requestKey}, or rerun ${rerun} --approve-tool-change, which returns the original instead of copying twice. `
|
|
367
|
+
+ `Before deciding differently, withdraw it: ${CMD} withdraw-copy-request ${requestKey} --approve-tool-change. Never copy it again under a new key.`);
|
|
368
|
+
}
|
|
369
|
+
result = checkCopy(recovered, { sourceId: source.id, archive: source.archive_sha256, contract: source.contract_digest, requestKey });
|
|
370
|
+
}
|
|
371
|
+
emit('tool_definition_copy', result, copyText(result, 'Copy started'));
|
|
372
|
+
}
|
|
373
|
+
/** `withdraw-copy-request <request-key> [--approve-tool-change]`: make an unanswered copy request unable to land. */
|
|
374
|
+
async function withdrawCopyRequest(scope, requestKey, options, emit) {
|
|
375
|
+
const signal = options.signal;
|
|
376
|
+
const landed = (original) => copyText(original, 'This request already landed, so nothing was withdrawn');
|
|
377
|
+
const original = await recoverToolCopy(scope, { requestKey, signal });
|
|
378
|
+
if (original) {
|
|
379
|
+
emit('tool_definition_copy', original, landed(original));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const approved = await approval(options, `Withdraw copy request ${requestKey} so it can never land?`, { label: 'Withdraw', detail: 'If it already landed, the copy is shown instead.' }, 'Nothing is changed.');
|
|
383
|
+
if (!approved) {
|
|
384
|
+
throw new CliError('tool_copy_consent_required', `Nothing was withdrawn. After the human approves: ${CMD} withdraw-copy-request ${requestKey} `
|
|
385
|
+
+ '--approve-tool-change', 2);
|
|
386
|
+
}
|
|
387
|
+
const outcome = await retryTransient(() => withdrawToolCopyRequest(scope, { requestKey, signal }), { signal, attempts: 3 });
|
|
388
|
+
if (outcome.state === 'committed') {
|
|
389
|
+
emit('tool_definition_copy', outcome.receipt, landed(outcome.receipt));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
emit('tool_definition_copy_withdrawal', { state: 'withdrawn', request_key: requestKey }, `Withdrawn. Copy request ${requestKey} will never land. Copy again without --request-key only if the human still wants it.`);
|
|
393
|
+
}
|
|
394
|
+
/** `withdraw-copy <copy-id> [--approve-tool-change]`: stop one of your copies while it is still pending. */
|
|
395
|
+
async function withdrawCopy(scope, copyId, options, emit) {
|
|
396
|
+
const signal = options.signal;
|
|
397
|
+
const current = await getToolCopy(scope, { copyId, signal }).catch(notMine);
|
|
398
|
+
if (current.state !== 'pending') {
|
|
399
|
+
emit('tool_definition_copy', current, copyText(current, current.state === 'withdrawn' ? 'Already withdrawn'
|
|
400
|
+
: 'Only a pending copy can be withdrawn, so nothing was sent'));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const approved = await approval(options, `Withdraw copy ${copyId} before it starts?`, { label: 'Withdraw', detail: 'If copying already began, it is shown as it is instead.' }, 'Nothing is changed.');
|
|
404
|
+
if (!approved)
|
|
405
|
+
throw new CliError('tool_copy_consent_required', `Nothing was withdrawn. After the human approves: ${CMD} withdraw-copy ${copyId} --approve-tool-change`, 2);
|
|
406
|
+
const after = await retryTransient(() => withdrawToolCopy(scope, { copyId, signal }), { signal, attempts: 3 }).catch(notMine);
|
|
407
|
+
emit('tool_definition_copy', after, copyText(after, after.state === 'withdrawn' ? 'Withdrawn'
|
|
408
|
+
: 'Copying had already begun, so it was not withdrawn'));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* `tools definitions [list] [--search <prefix>] [--page <n>] | show <id> | recipients | grants <id>
|
|
412
|
+
* | share <id> <workspace-id> read|use | share-request|withdraw-share-request <id> <key> | revoke <id> <grant-id>
|
|
413
|
+
* | copy <id> | copy-request|withdraw-copy-request <key> | copy-status|withdraw-copy <copy-id> | dependents <id> [--kind world|copy]`.
|
|
414
|
+
* Sharing, revoking, copying and withdrawing need --approve-tool-change (or a prompt); keyed writes take --request-key.
|
|
415
|
+
*/
|
|
416
|
+
export async function toolReuseCommand(args, options, emit) {
|
|
417
|
+
checkToolReuseArguments(args, options);
|
|
418
|
+
const [verb = 'list', first, second] = args;
|
|
419
|
+
const signal = options.signal;
|
|
420
|
+
const scope = await scopeOf(options);
|
|
421
|
+
if (verb === 'share')
|
|
422
|
+
return share(scope, args, options, emit);
|
|
423
|
+
if (verb === 'withdraw-share-request')
|
|
424
|
+
return withdrawShareRequest(scope, toolReuseId(first, 'tool version id'), toolReuseId(second, 'request key'), options, emit);
|
|
425
|
+
if (verb === 'revoke')
|
|
426
|
+
return revoke(scope, toolReuseId(first, 'tool version id'), toolReuseId(second, 'grant id'), options, emit);
|
|
427
|
+
if (verb === 'copy')
|
|
428
|
+
return copy(scope, toolReuseId(first, 'tool version id'), options, emit);
|
|
429
|
+
if (verb === 'withdraw-copy-request')
|
|
430
|
+
return withdrawCopyRequest(scope, toolReuseId(first, 'request key'), options, emit);
|
|
431
|
+
if (verb === 'withdraw-copy')
|
|
432
|
+
return withdrawCopy(scope, toolReuseId(first, 'copy id'), options, emit);
|
|
433
|
+
if (verb === 'list') {
|
|
434
|
+
const number = pageNumber(options.page);
|
|
435
|
+
const page = await listToolDefinitions(scope, { search: options.search, page: number, signal }).catch(pastEnd(number, rethrow));
|
|
436
|
+
emit('tool_definitions', page, [
|
|
437
|
+
`Tool versions this workspace can read, newest first, page ${page.page} (${page.count} in all):`,
|
|
438
|
+
...page.results.map((row) => definitionLine(scope, row)),
|
|
439
|
+
page.results.length ? null : ' none',
|
|
440
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
441
|
+
`Read one: ${CMD} show <tool-version-id>. Copy a shared one privately: ${CMD} copy <tool-version-id>`,
|
|
442
|
+
].filter((line) => line !== null).join('\n'));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (verb === 'show') {
|
|
446
|
+
const row = await getToolDefinition(scope, { implementationId: toolReuseId(first, 'tool version id'), signal }).catch(notReadable);
|
|
447
|
+
emit('tool_definition', row, [
|
|
448
|
+
`Tool ${terminalText(row.name)} ${terminalText(row.version)} (${terminalText(row.key)}): version ${row.id}`,
|
|
449
|
+
row.description ? terminalText(row.description) : null,
|
|
450
|
+
`Access: ${toolAccess(scope, row)}`,
|
|
451
|
+
`State: ${row.state}${row.inspected_at ? `, inspected ${terminalText(row.inspected_at)}` : ''}${row.revoked_at ? `, revoked ${terminalText(row.revoked_at)}` : ''}`,
|
|
452
|
+
`Archive ${row.archive_sha256} contract ${row.contract_digest}`,
|
|
453
|
+
`Interfaces: ${row.interfaces.length ? row.interfaces.map(terminalText).join(', ') : 'none declared'}`,
|
|
454
|
+
'Services:',
|
|
455
|
+
...(row.services ?? []).map((one) => ` ${terminalText(one.key)} ${terminalText(one.name)} ${one.operation_count} operation(s)`
|
|
456
|
+
+ `${one.visual_interface_declared ? ' visual interface declared' : ''}`),
|
|
457
|
+
row.can_fork && sharedHere(scope, row) ? `Copy it privately: ${CMD} copy ${row.id}` : null,
|
|
458
|
+
sharedHere(scope, row) ? null : `Share it: ${CMD} share ${row.id} <workspace-id> read|use`,
|
|
459
|
+
].filter((line) => line !== null).join('\n'));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (verb === 'recipients') {
|
|
463
|
+
const number = pageNumber(options.page);
|
|
464
|
+
const page = await listToolRecipients(scope, { search: options.search, page: number, signal }).catch(pastEnd(number, adminOnly));
|
|
465
|
+
emit('tool_definition_recipients', page, [
|
|
466
|
+
`Workspaces of this organization a tool version can be shared with, page ${page.page} (${page.count} in all):`,
|
|
467
|
+
...page.results.map((one) => ` ${one.id} ${terminalText(one.name)}`),
|
|
468
|
+
page.results.length ? null : ' none',
|
|
469
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
470
|
+
`Share a version: ${CMD} share <tool-version-id> <workspace-id> read|use`,
|
|
471
|
+
].filter((line) => line !== null).join('\n'));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (verb === 'grants') {
|
|
475
|
+
const implementationId = toolReuseId(first, 'tool version id');
|
|
476
|
+
const number = pageNumber(options.page);
|
|
477
|
+
const page = await listToolGrants(scope, { implementationId, page: number, signal }).catch(pastEnd(number, ownerOnly));
|
|
478
|
+
emit('tool_definition_grants', { implementation_id: implementationId, ...page }, [
|
|
479
|
+
`Sharing grants of tool version ${implementationId}, page ${page.page} (${page.count} in all, revoked ones included):`,
|
|
480
|
+
...page.results.map(grantLine),
|
|
481
|
+
page.results.length ? null : ' none',
|
|
482
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
483
|
+
`Share it: ${CMD} share ${implementationId} <workspace-id> read|use`,
|
|
484
|
+
].filter((line) => line !== null).join('\n'));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (verb === 'share-request') {
|
|
488
|
+
const implementationId = toolReuseId(first, 'tool version id');
|
|
489
|
+
const requestKey = toolReuseId(second, 'request key');
|
|
490
|
+
const grant = await recoverToolGrant(scope, { implementationId, requestKey, signal }).catch(ownerOnly);
|
|
491
|
+
emit('tool_definition_grant', grant ?? { implementation_id: implementationId, request_key: requestKey, state: 'not_found' }, grant ? grantLine(grant).trim()
|
|
492
|
+
: `None of your sharing requests for this tool version is recorded under ${requestKey}. An unanswered request may still arrive. Rerun the `
|
|
493
|
+
+ `original share with the same --request-key (it returns the original instead of sharing twice), or withdraw it first: `
|
|
494
|
+
+ `${CMD} withdraw-share-request ${implementationId} ${requestKey}. Never share it under a new key.`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (verb === 'copy-request') {
|
|
498
|
+
const requestKey = toolReuseId(first, 'request key');
|
|
499
|
+
const original = await recoverToolCopy(scope, { requestKey, signal });
|
|
500
|
+
emit('tool_definition_copy', original ?? { request_key: requestKey, state: 'not_found' }, original ? copyText(original, 'Copy')
|
|
501
|
+
: `None of your copy requests is recorded under ${requestKey}, or it was withdrawn. An unanswered request may still land. Rerun the original `
|
|
502
|
+
+ `copy with --request-key ${requestKey} (it returns the original instead of copying twice), or withdraw it first: `
|
|
503
|
+
+ `${CMD} withdraw-copy-request ${requestKey}. Never copy it again under a new key.`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (verb === 'copy-status') {
|
|
507
|
+
const current = await getToolCopy(scope, { copyId: toolReuseId(first, 'copy id'), signal }).catch(notMine);
|
|
508
|
+
emit('tool_definition_copy', current, copyText(current, 'Copy'));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const implementationId = toolReuseId(first, 'tool version id');
|
|
512
|
+
const kind = (options.kind ?? 'world');
|
|
513
|
+
const number = pageNumber(options.page);
|
|
514
|
+
const page = await listToolDependents(scope, { implementationId, kind: DEPENDENT_KINDS[kind], page: number, signal }).catch(pastEnd(number, notReadable));
|
|
515
|
+
emit('tool_definition_dependents', { implementation_id: implementationId, kind, ...page }, [
|
|
516
|
+
`${kind === 'world' ? 'World revisions in this workspace that use' : 'Your private copies of'} tool version ${implementationId}, `
|
|
517
|
+
+ `page ${page.page} (${page.count} in all):`,
|
|
518
|
+
...page.results.map((one) => 'world_id' in one
|
|
519
|
+
? ` world ${one.world_id} revision ${one.revision} (${one.id}) ${terminalText(one.created_at)}`
|
|
520
|
+
: ` copy ${one.id} ${copyStatus(one).text} ${terminalText(one.created_at)}`),
|
|
521
|
+
page.results.length ? null : ' none',
|
|
522
|
+
page.next ? `More: --page ${page.page + 1}` : null,
|
|
523
|
+
'Other workspaces\' worlds and copies are never shown here.',
|
|
524
|
+
kind === 'world' ? `Your private copies of it: ${CMD} dependents ${implementationId} --kind copy` : null,
|
|
525
|
+
].filter((line) => line !== null).join('\n'));
|
|
526
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { api } from '../auth.js';
|
|
3
|
+
import { CliError } from '../http.js';
|
|
4
|
+
import { selectMany, selectOne, terminalText } from '../prompts.js';
|
|
5
|
+
import { catalogRead, catalogPageMatches, catalogSetupMatches } from '../catalog-contracts.js';
|
|
6
|
+
import { sleep } from '../native/control-plane.js';
|
|
7
|
+
import { SETUP_MAX_SELECTIONS } from '../native/tool-setups.js';
|
|
8
|
+
/** Stable child identity: retries of the same start/release never adopt twice. */
|
|
9
|
+
function adoptionKey(start, release) {
|
|
10
|
+
const hex = createHash('sha256').update(`firedrill.start.catalog.v1:${start}:${release}`).digest('hex');
|
|
11
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
12
|
+
}
|
|
13
|
+
export async function chooseStartCatalog(scope, saved, options) {
|
|
14
|
+
const query = options.search ?? '';
|
|
15
|
+
if ([...query].length > 200)
|
|
16
|
+
throw new CliError('arguments_invalid', 'Use --search with at most 200 characters.', 2);
|
|
17
|
+
const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
18
|
+
const prefix = `/api/workspaces/${scope.workspace}/tools`;
|
|
19
|
+
const releases = [];
|
|
20
|
+
let more = false;
|
|
21
|
+
for (let page = 1; page <= 5; page++) {
|
|
22
|
+
const value = await api(`${prefix}/library?page=${page}&q=${encodeURIComponent(query)}&category=`, auth);
|
|
23
|
+
catalogRead(value, catalogPageMatches(value, scope.workspace, query, page, ''));
|
|
24
|
+
releases.push(...value.results.filter(row => row.available && row.saved_version?.state !== 'revoked'));
|
|
25
|
+
more = !!value.next;
|
|
26
|
+
if (!more)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const savedIds = new Set(saved.map(row => row.implementation_id));
|
|
30
|
+
const choices = saved.map(row => ({
|
|
31
|
+
value: { kind: 'saved', id: row.implementation_id }, label: `${row.name} ${row.version}`,
|
|
32
|
+
detail: `Saved · ${row.services.map(service => service.service_key).join(', ')}`,
|
|
33
|
+
}));
|
|
34
|
+
for (const release of releases) {
|
|
35
|
+
if (release.saved_version && savedIds.has(release.saved_version.id))
|
|
36
|
+
continue;
|
|
37
|
+
choices.push({ value: { kind: 'catalog', release }, label: `${release.name} ${release.version}`,
|
|
38
|
+
detail: `Catalog · ${release.operation_count} operations${release.saved_version ? ' · saved version' : ' · save before review'}` });
|
|
39
|
+
}
|
|
40
|
+
if (!choices.length)
|
|
41
|
+
throw new CliError('tool_catalog_empty', 'No available tools match. Try tools start --search <name>, or upload your own tool package.', 2);
|
|
42
|
+
if (more)
|
|
43
|
+
process.stderr.write('Catalog is limited to five pages; use tools start --search <name> to narrow it.\n');
|
|
44
|
+
const selected = await selectMany(`Choose up to ${SETUP_MAX_SELECTIONS} tools (Space toggles, Enter confirms)`, choices, options.signal);
|
|
45
|
+
if (!selected.length || selected.length > SETUP_MAX_SELECTIONS)
|
|
46
|
+
throw new CliError('tool_selection_invalid', `Choose 1–${SETUP_MAX_SELECTIONS} tools. Nothing was started.`, 2);
|
|
47
|
+
const unsaved = selected.filter((row) => row.kind === 'catalog' && !row.release.saved_version);
|
|
48
|
+
if (unsaved.length) {
|
|
49
|
+
const approved = await selectOne(`Save ${unsaved.length} selected catalog tool version(s) to this workspace? This does not start tools or approve credits.`, [
|
|
50
|
+
{ value: false, label: 'Cancel' }, { value: true, label: 'Save selected versions and continue to review' },
|
|
51
|
+
], options.signal);
|
|
52
|
+
if (!approved)
|
|
53
|
+
throw new CliError('canceled', 'Nothing was saved or started.', 2);
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(`Tool start request: ${options.requestKey}\nContinue with firedrill tools start --workspace ${scope.workspace} --request-key ${options.requestKey}\n`);
|
|
56
|
+
const ids = [];
|
|
57
|
+
for (const choice of selected) {
|
|
58
|
+
if (choice.kind === 'saved') {
|
|
59
|
+
ids.push(choice.id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (choice.release.saved_version) {
|
|
63
|
+
ids.push(choice.release.saved_version.id);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const key = adoptionKey(options.requestKey, choice.release.id);
|
|
67
|
+
const body = { release_id: choice.release.id, duration_seconds: null,
|
|
68
|
+
install_consent: true, start_consent: false, usage_quote_id: null };
|
|
69
|
+
const read = async () => {
|
|
70
|
+
const value = await api(`${prefix}/catalog-requests/${key}`, auth);
|
|
71
|
+
return catalogRead(value, catalogSetupMatches(value, { workspace: scope.workspace, actor: scope.owner }, { key, input: body }));
|
|
72
|
+
};
|
|
73
|
+
process.stderr.write(`Saving ${terminalText(choice.release.name)}: catalog request ${key}. Recovery: firedrill tools catalog-status ${key} --workspace ${scope.workspace}\n`);
|
|
74
|
+
let receipt;
|
|
75
|
+
try {
|
|
76
|
+
const value = await api(`${prefix}/catalog-setups`, { ...auth, method: 'POST', idempotencyKey: key, body });
|
|
77
|
+
receipt = catalogRead(value, catalogSetupMatches(value, { workspace: scope.workspace, actor: scope.owner }, { key, input: body }));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
receipt = await read();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new CliError('catalog_acceptance_unconfirmed', `Catalog request ${key} is unconfirmed. Recover that original request; do not create another start key. No tools were started.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Await only the original package inspection; bounded, no runtime allocation.
|
|
88
|
+
for (let attempt = 0; attempt < 20 && ['preparing', 'inspecting'].includes(receipt.state); attempt++) {
|
|
89
|
+
await sleep(1500, options.signal);
|
|
90
|
+
receipt = await read();
|
|
91
|
+
}
|
|
92
|
+
if (receipt.state !== 'saved' || !receipt.implementation_id) {
|
|
93
|
+
throw new CliError('catalog_save_pending', `Catalog request ${key} is ${receipt.state}. Inspect the original request, then continue tools start with request key ${options.requestKey}. No tools were started.`, 2);
|
|
94
|
+
}
|
|
95
|
+
ids.push(receipt.implementation_id);
|
|
96
|
+
}
|
|
97
|
+
if (new Set(ids).size !== ids.length)
|
|
98
|
+
throw new CliError('tool_selection_invalid', 'The selection resolves to duplicate saved versions. Choose each version once. No tools were started.', 2);
|
|
99
|
+
return ids;
|
|
100
|
+
}
|