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,196 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { api } from './auth.js';
|
|
3
|
+
import { line } from './configuration.js';
|
|
4
|
+
import { CliError } from './http.js';
|
|
5
|
+
import { selectOne } from './prompts.js';
|
|
6
|
+
import { selectedWorld } from './world-access.js';
|
|
7
|
+
import { identifier } from './world-contracts.js';
|
|
8
|
+
import { checkpointContents } from './world-checkpoint-scopes.js';
|
|
9
|
+
import { sessionStateActionMatches, sessionStateRequestMatches, sessionCheckpointsPageMatches, sessionCheckpointActionsPageMatches } from '../../contracts/session-state-contracts.js';
|
|
10
|
+
import { worldSessionMatches, worldSessionsPageMatches } from '../../contracts/session-contracts.js';
|
|
11
|
+
export const CHECKPOINT_ACTIONS = ['checkpoints', 'checkpoint', 'restore', 'checkpoint-actions', 'checkpoint-request'];
|
|
12
|
+
const text = (row) => `${row.id} ${row.name}\n${row.available ? 'Saved' : row.state} · ${row.captured_at ?? row.created_at}`
|
|
13
|
+
+ (row.record_count === null ? '' : ` · ${row.record_count} records`) + `\n${checkpointContents(row.scope)}` + (row.problem ? '\n' + row.problem : '');
|
|
14
|
+
const actionText = (row) => `${row.id} ${row.kind === 'restore' ? 'Restore' : 'Save'}${row.name ? ' · ' + row.name : ''}\n`
|
|
15
|
+
+ (row.state === 'observed' ? row.kind === 'restore' ? row.restored_at ? `Restored ${row.restored_at}` : 'Restore not confirmed'
|
|
16
|
+
: row.captured ? row.checkpoint_available ? 'Saved' : 'Capture recorded; checkpoint file unavailable' : 'Not saved' : row.state) + (row.problem ? '\n' + row.problem : '');
|
|
17
|
+
/** Uses the same original command intent and retained history as the browser.
|
|
18
|
+
* A lost reply is read by the original key, never a second POST or restore. */
|
|
19
|
+
export async function worldSessionCheckpoints(args, options, output) {
|
|
20
|
+
const action = args[0], writing = ['checkpoint', 'restore'].includes(action);
|
|
21
|
+
if (!CHECKPOINT_ACTIONS.includes(action) || (action === 'checkpoint-request' ? args.length !== 3 : args.length > (writing ? 3 : 2))
|
|
22
|
+
|| options.local !== undefined || options.managed !== undefined || options.device !== undefined || options.grant !== undefined
|
|
23
|
+
|| options.seconds !== undefined || options.dataset !== undefined || options.shareDatasetWithDevice !== undefined
|
|
24
|
+
|| options.checkpoint !== undefined || options.approveRealSession !== undefined || options.acknowledgeSharedState !== undefined
|
|
25
|
+
|| options.name !== undefined && action !== 'checkpoint'
|
|
26
|
+
|| options.run !== undefined && !writing
|
|
27
|
+
|| options.shareCheckpointWithDevice !== undefined && action !== 'restore'
|
|
28
|
+
|| options.page !== undefined && !['checkpoints', 'checkpoint-actions'].includes(action)
|
|
29
|
+
|| options.search !== undefined && !['checkpoints', 'checkpoint-actions', 'restore'].includes(action)
|
|
30
|
+
|| options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page) || (options.search?.length ?? 0) > 200) {
|
|
31
|
+
throw new CliError('arguments_invalid', 'Use sessions checkpoints, checkpoint, restore, checkpoint-actions or checkpoint-request with the matching session and options.', 2);
|
|
32
|
+
}
|
|
33
|
+
const tty = !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
34
|
+
const scope = await selectedWorld({ ...options, interactive: tty });
|
|
35
|
+
const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
36
|
+
let sessionId = args[1];
|
|
37
|
+
if (!sessionId) {
|
|
38
|
+
if (!tty)
|
|
39
|
+
throw new CliError('arguments_invalid', 'Provide the session ID from firedrill sessions.', 2);
|
|
40
|
+
let page = 1;
|
|
41
|
+
while (!sessionId) {
|
|
42
|
+
const list = await api(`${scope.prefix}/runtime-sessions?page=${page}`, auth);
|
|
43
|
+
if (!worldSessionsPageMatches(list, { actor: scope.owner, workspace: scope.workspace, world: scope.world }))
|
|
44
|
+
throw new CliError('world_session_receipt_invalid', 'Session history could not be confirmed in this world.');
|
|
45
|
+
const choices = list.results.map(row => ({ value: row.id, label: row.id, detail: `${row.execution_location} · ${row.state}` }));
|
|
46
|
+
if (page > 1)
|
|
47
|
+
choices.push({ value: 'previous', label: 'Previous page', detail: '' });
|
|
48
|
+
if (list.next)
|
|
49
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
50
|
+
choices.push({ value: 'cancel', label: 'Cancel', detail: '' });
|
|
51
|
+
const choice = await selectOne('World session', choices, options.signal);
|
|
52
|
+
if (choice === 'cancel')
|
|
53
|
+
throw new CliError('canceled', 'No checkpoint action was submitted.', 130);
|
|
54
|
+
if (choice === 'previous')
|
|
55
|
+
page--;
|
|
56
|
+
else if (choice === 'next')
|
|
57
|
+
page++;
|
|
58
|
+
else
|
|
59
|
+
sessionId = identifier(choice);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
sessionId = identifier(sessionId);
|
|
63
|
+
const base = `${scope.prefix}/runtime-sessions/${sessionId}`;
|
|
64
|
+
const identity = { actor: scope.owner, workspace: scope.workspace, world: scope.world, session: sessionId };
|
|
65
|
+
const kind = action === 'restore' ? 'restore' : 'save';
|
|
66
|
+
const unconfirmed = () => new CliError('world_session_acceptance_unconfirmed', 'Inspect the original checkpoint request. Its acceptance could not be confirmed; it was not resubmitted.', 2);
|
|
67
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
68
|
+
function continuation(value) {
|
|
69
|
+
return `\n\nPage ${value.page}; ${value.count.toLocaleString()} matching entries.${value.next
|
|
70
|
+
? `\nContinue: firedrill sessions ${action} ${sessionId} --world ${scope.world} --workspace ${scope.workspace} --page ${value.page + 1}${options.search ? ' --search ' + quote(options.search) : ''}${options.json ? ' --json' : ''}` : ''}`;
|
|
71
|
+
}
|
|
72
|
+
async function lookup(key, present = true) {
|
|
73
|
+
const saved = await api(`${base}/checkpoint-intent?request_key=${identifier(key)}`, auth);
|
|
74
|
+
if (!sessionStateRequestMatches(saved, identity, key, writing ? kind : saved.request?.kind ?? 'save'))
|
|
75
|
+
throw unconfirmed();
|
|
76
|
+
if (present)
|
|
77
|
+
output('world_checkpoint_request', saved, saved.request ? actionText(saved.request)
|
|
78
|
+
: `Request ${key} is not recorded yet. A delayed original request may still arrive. Inspect it again or close the session; do not submit a replacement.`);
|
|
79
|
+
return saved;
|
|
80
|
+
}
|
|
81
|
+
if (action === 'checkpoint-request') {
|
|
82
|
+
await lookup(identifier(args[2]));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (action === 'checkpoint-actions') {
|
|
86
|
+
const page = await api(`${base}/checkpoint-actions?page=${options.page ?? '1'}&q=${encodeURIComponent(options.search ?? '')}`, auth);
|
|
87
|
+
if (!sessionCheckpointActionsPageMatches(page, identity, options.search))
|
|
88
|
+
throw unconfirmed();
|
|
89
|
+
output('world_checkpoint_actions', page, (page.results.map(actionText).join('\n\n') || 'No checkpoint actions in this session.') + continuation(page));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (action === 'checkpoints') {
|
|
93
|
+
const page = await api(`${base}/checkpoints?page=${options.page ?? '1'}&q=${encodeURIComponent(options.search ?? '')}`, auth);
|
|
94
|
+
if (!sessionCheckpointsPageMatches(page, identity, options.search))
|
|
95
|
+
throw unconfirmed();
|
|
96
|
+
output('world_checkpoints', page, (page.results.map(text).join('\n\n') || 'No matching checkpoints in this world.') + continuation(page));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const session = await api(base, auth);
|
|
100
|
+
if (!worldSessionMatches(session, { actor: scope.owner, workspace: scope.workspace, world: scope.world }, { id: sessionId }))
|
|
101
|
+
throw new CliError('world_session_receipt_invalid', 'The selected session could not be confirmed.');
|
|
102
|
+
if (session.id !== sessionId || session.world_id !== scope.world || !['managed', 'local'].includes(session.execution_location)
|
|
103
|
+
|| typeof session.reset_count !== 'string' || !/^(0|[1-9][0-9]{0,18})$/.test(session.reset_count)) {
|
|
104
|
+
throw new CliError('world_session_receipt_invalid', 'The selected session could not be confirmed. No checkpoint action was submitted.');
|
|
105
|
+
}
|
|
106
|
+
if (!session.can_control || session.terminal_at || !session.observation_current || !session.checkpoints_available || session.voice_call_open) {
|
|
107
|
+
throw new CliError('world_checkpoint_unavailable', 'Use a current, controllable session with checkpoint support and no open voice call.', 2);
|
|
108
|
+
}
|
|
109
|
+
let checkpoint = null, name = options.name ?? args[2];
|
|
110
|
+
if (action === 'restore') {
|
|
111
|
+
let page = 1;
|
|
112
|
+
while (!checkpoint) {
|
|
113
|
+
const data = await api(`${base}/checkpoints?page=${page}&q=${encodeURIComponent(options.search ?? '')}${args[2] ? '&checkpoint_id=' + identifier(args[2]) : ''}`, auth);
|
|
114
|
+
if (!sessionCheckpointsPageMatches(data, identity, options.search, args[2] ? identifier(args[2]) : null))
|
|
115
|
+
throw unconfirmed();
|
|
116
|
+
if (data.pending_action || data.uncertain_restore)
|
|
117
|
+
throw new CliError('world_session_request_pending', 'Inspect checkpoint activity. A preceding action is pending or uncertain; do not start another restore.', 2);
|
|
118
|
+
if (args[2]) {
|
|
119
|
+
checkpoint = data.results.find(row => row.id === identifier(args[2])) ?? null;
|
|
120
|
+
// Exact IDs use a server-side filter instead of scanning every page.
|
|
121
|
+
if (!checkpoint?.available || !checkpoint.compatible)
|
|
122
|
+
throw new CliError('world_checkpoint_incompatible', 'Choose an available checkpoint for this exact source, configuration and schema.', 2);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (!tty)
|
|
126
|
+
throw new CliError('arguments_invalid', 'Provide a checkpoint ID from sessions checkpoints.', 2);
|
|
127
|
+
const choices = data.results.filter(row => row.available && row.compatible).map(row => ({ value: row.id, label: row.name, detail: row.captured_at ?? row.created_at }));
|
|
128
|
+
if (page > 1)
|
|
129
|
+
choices.push({ value: 'previous', label: 'Previous page', detail: '' });
|
|
130
|
+
if (data.next)
|
|
131
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
132
|
+
choices.push({ value: 'cancel', label: 'Cancel', detail: '' });
|
|
133
|
+
const choice = await selectOne('Checkpoint to restore', choices, options.signal);
|
|
134
|
+
if (choice === 'cancel')
|
|
135
|
+
throw new CliError('canceled', 'No restore was submitted.', 130);
|
|
136
|
+
if (choice === 'previous')
|
|
137
|
+
page--;
|
|
138
|
+
else if (choice === 'next')
|
|
139
|
+
page++;
|
|
140
|
+
else
|
|
141
|
+
checkpoint = data.results.find(row => row.id === choice) ?? null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
output('world_checkpoint_review', checkpoint, text(checkpoint));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (!name && tty)
|
|
148
|
+
name = await line('Checkpoint name', 120, options.signal);
|
|
149
|
+
if (!name?.trim() || name.length > 120 || /[\u0000-\u001f\u007f]/.test(name))
|
|
150
|
+
throw new CliError('arguments_invalid', 'Provide a checkpoint name up to 120 characters.', 2);
|
|
151
|
+
if (options.name !== undefined && args[2] !== undefined)
|
|
152
|
+
throw new CliError('arguments_invalid', 'Use a positional name or --name, not both.', 2);
|
|
153
|
+
}
|
|
154
|
+
let share = options.shareCheckpointWithDevice === true;
|
|
155
|
+
if (action === 'restore' && session.execution_location === 'local') {
|
|
156
|
+
if (!share && tty)
|
|
157
|
+
share = await selectOne('Download this checkpoint to the session’s original device?', [
|
|
158
|
+
{ value: false, label: 'Cancel' }, { value: true, label: 'Allow this checkpoint download', detail: 'Private runtime input outside the project. Device copies and backups cannot be erased remotely.' }
|
|
159
|
+
], options.signal);
|
|
160
|
+
if (!share)
|
|
161
|
+
throw new CliError('local_checkpoint_consent_required', 'Use --share-checkpoint-with-device for this exact download. --run alone is not download approval.', 2);
|
|
162
|
+
}
|
|
163
|
+
else if (share)
|
|
164
|
+
throw new CliError('arguments_invalid', 'Use --share-checkpoint-with-device only when restoring a local session.', 2);
|
|
165
|
+
if (!options.run && (!tty || !await selectOne(action === 'restore' ? 'Replace current database records and restart the agent?' : 'Briefly pause the agent and save committed database records?', [
|
|
166
|
+
{ value: false, label: 'Cancel' }, { value: true, label: action === 'restore' ? 'Restore selected checkpoint' : 'Save checkpoint',
|
|
167
|
+
detail: `${checkpointContents(checkpoint?.scope ?? session.checkpoint_scope)}. Agent memory, other files and real-account changes are not included. Open tool views close.` }
|
|
168
|
+
], options.signal))) {
|
|
169
|
+
throw new CliError('world_session_consent_required', 'No checkpoint action was submitted. Use --run to approve this exact action in automation.', 2);
|
|
170
|
+
}
|
|
171
|
+
const key = randomUUID();
|
|
172
|
+
output('world_checkpoint_request_key', { session_id: sessionId, request_key: key }, `Request: ${key}\nRecover with sessions checkpoint-request ${sessionId} ${key}. Do not repeat an uncertain restore.`);
|
|
173
|
+
let result;
|
|
174
|
+
const body = checkpoint
|
|
175
|
+
? { expected_reset_count: session.reset_count, checkpoint_id: checkpoint.id, ...(share ? { share_checkpoint_with_device: true } : {}) }
|
|
176
|
+
: { expected_reset_count: session.reset_count, name: name };
|
|
177
|
+
const selected = { key, counter: session.reset_count, ...(checkpoint ? { checkpoint: checkpoint.id } : {}) };
|
|
178
|
+
try {
|
|
179
|
+
result = await api(`${base}/${action === 'restore' ? 'checkpoint-restore' : 'checkpoints'}`, { ...auth,
|
|
180
|
+
method: 'POST', idempotencyKey: key, body });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (options.signal.aborted)
|
|
184
|
+
throw error;
|
|
185
|
+
const saved = await lookup(key, false);
|
|
186
|
+
if (!saved.request)
|
|
187
|
+
throw new CliError('world_session_acceptance_unconfirmed', `Inspect sessions checkpoint-request ${sessionId} ${key}. No checkpoint action was resubmitted.`, 2);
|
|
188
|
+
result = saved.request;
|
|
189
|
+
}
|
|
190
|
+
if (!sessionStateActionMatches(result, identity, kind, selected))
|
|
191
|
+
throw unconfirmed();
|
|
192
|
+
const original = await lookup(key, false);
|
|
193
|
+
if (!sessionStateRequestMatches(original, identity, key, kind, selected) || original.request?.id !== result.id)
|
|
194
|
+
throw unconfirmed();
|
|
195
|
+
output('world_checkpoint_action', original.request, actionText(original.request));
|
|
196
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { api } from './auth.js';
|
|
3
|
+
import { CliError } from './http.js';
|
|
4
|
+
import { selectOne } from './prompts.js';
|
|
5
|
+
import { selectedWorld } from './world-access.js';
|
|
6
|
+
import { identifier } from './world-contracts.js';
|
|
7
|
+
import { requestInput } from './request-input.js';
|
|
8
|
+
import { worldAllocationMatches, worldQuoteMatches, worldExtensionMatches, worldTimelineMatches, worldTimelineCanonical, worldTimelineIntentMatches, worldScheduledEventMatches, resourcePageMatches, worldCredits } from '../../contracts/world-lifetime-contracts.js';
|
|
9
|
+
import { worldSessionMatches } from '../../contracts/session-contracts.js';
|
|
10
|
+
import { applicationCheckpointMatches } from '../../contracts/application-state-contracts.js';
|
|
11
|
+
const tty = (options) => !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
12
|
+
export const WORLD_LIFECYCLE_ACTIONS = ['quote', 'timelines', 'timeline-events', 'schedule', 'timeline-control', 'extension-quote', 'extend', 'extensions', 'extension-status',
|
|
13
|
+
'timeline-request', 'timeline-apply', 'timeline-withdraw',
|
|
14
|
+
'application-checkpoints', 'application-checkpoint', 'save-application-state'];
|
|
15
|
+
export async function worldAllocationApproval(scope, options, selected, output) {
|
|
16
|
+
const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
17
|
+
if (!tty(options) && !options.usageQuote)
|
|
18
|
+
throw new CliError('billing_quote_required', 'Use sessions quote --file selection.json, then sessions open with its --usage-quote and original --request-key.', 2);
|
|
19
|
+
const quote = options.usageQuote
|
|
20
|
+
? await api(`/api/workspaces/${scope.workspace}/billing/usage/quotes/${identifier(options.usageQuote)}`, auth)
|
|
21
|
+
: await api(`${scope.prefix}/runtime-quotes`, { ...auth, method: 'POST', idempotencyKey: randomUUID(), body: selected });
|
|
22
|
+
if (!worldAllocationMatches(quote, scope.workspace, scope.world, selected))
|
|
23
|
+
throw new CliError('billing_scope_mismatch', 'The quote does not match this world and opening request.', 2);
|
|
24
|
+
output('world_runtime_quote', quote, `${worldCredits(quote, 'estimated')} credits estimated. Maximum budget ${worldCredits(quote)} credits. The estimate is reserved after approval.`);
|
|
25
|
+
if (tty(options) && !await selectOne('Approve runtime credits?', [{ value: false, label: 'Cancel' }, { value: true, label: 'Approve runtime' }], options.signal))
|
|
26
|
+
throw new CliError('canceled', 'No runtime was opened.', 130);
|
|
27
|
+
return quote.id;
|
|
28
|
+
}
|
|
29
|
+
export async function worldLifecycleCommand(args, options, output) {
|
|
30
|
+
const action = args[0], writing = ['schedule', 'timeline-control', 'timeline-apply', 'timeline-withdraw', 'extend', 'save-application-state'].includes(action), paged = ['timelines', 'timeline-events', 'extensions', 'application-checkpoints'].includes(action);
|
|
31
|
+
const expected = ['timeline-events', 'timeline-control', 'timeline-request', 'timeline-apply', 'timeline-withdraw', 'extension-status'].includes(action) ? 3 : action === 'quote' ? 1 : 2;
|
|
32
|
+
if (args.length !== expected || options.page && !paged || options.search && !['timeline-events', 'application-checkpoints'].includes(action)
|
|
33
|
+
|| options.seconds && !['extension-quote', 'extend'].includes(action) || options.file && !['quote', 'schedule', 'timeline-control', 'timeline-withdraw'].includes(action)
|
|
34
|
+
|| options.usageQuote && action !== 'extend' || options.request && !['quote', 'schedule', 'timeline-control', 'extend', 'save-application-state'].includes(action)
|
|
35
|
+
|| options.name !== undefined && action !== 'save-application-state' || options.run !== undefined && !writing
|
|
36
|
+
|| options.page && !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
37
|
+
throw new CliError('arguments_invalid', 'Use sessions quote, timelines, timeline-events, schedule, timeline-control, extensions, extension-status, extension-quote or extend with the selected session.', 2);
|
|
38
|
+
const scope = await selectedWorld({ ...options, interactive: tty(options) }), auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
39
|
+
if (action === 'quote') {
|
|
40
|
+
if (!options.file)
|
|
41
|
+
throw new CliError('arguments_invalid', 'Use sessions quote --file selection.json, including the original launch_request_key.', 2);
|
|
42
|
+
const selected = JSON.parse(await requestInput(options.file, options.signal));
|
|
43
|
+
const value = await api(`${scope.prefix}/runtime-quotes`, { ...auth, method: 'POST', idempotencyKey: options.request ?? randomUUID(), body: selected });
|
|
44
|
+
if (!worldAllocationMatches(value, scope.workspace, scope.world, selected))
|
|
45
|
+
throw new CliError('billing_scope_mismatch', 'The quote does not match this world.', 2);
|
|
46
|
+
output('world_runtime_quote', value, `${worldCredits(value)} credits maximum.\nUse sessions open --usage-quote ${value.id} --request-key ${selected.launch_request_key}.`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (action === 'application-checkpoint') {
|
|
50
|
+
const key = identifier(args[1]), value = await api(`${scope.prefix}/application-checkpoints/${key}`, auth);
|
|
51
|
+
if (!applicationCheckpointMatches(value, scope.world) || value.id !== key && value.request_key !== key)
|
|
52
|
+
throw new CliError('application_checkpoint_response_invalid', 'Inspect the exact original application checkpoint.', 2);
|
|
53
|
+
output('application_checkpoint', value, `${String(value.name)} ${String(value.state)}\n`
|
|
54
|
+
+ (value.available ? `${value.components.map(one => one.name).join(', ')}\nNot included: ${value.excluded.join(', ')}.`
|
|
55
|
+
: value.state === 'pending' ? 'Waiting for the capture to finish.' : 'The saved application state is unavailable.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const session = identifier(args[1]), base = `${scope.prefix}/runtime-sessions/${session}`;
|
|
59
|
+
const context = { id: session, world_id: scope.world, workspace_id: scope.workspace };
|
|
60
|
+
const readTimelineIntent = async (key, method = 'GET', body, selected) => {
|
|
61
|
+
const value = await api(`${base}/timelines/requests/${key}`, { ...auth, method, ...(body !== undefined ? { body } : {}) });
|
|
62
|
+
if (!worldTimelineIntentMatches(value, { ...context, actor: scope.owner }, key, selected))
|
|
63
|
+
throw new CliError('world_timeline_response_invalid', 'The original schedule action could not be confirmed. No replacement was sent.', 2);
|
|
64
|
+
return value;
|
|
65
|
+
};
|
|
66
|
+
const intentText = (value) => `${value.state}\nRequest: ${value.request_key}`
|
|
67
|
+
+ (value.timeline ? `\nSchedule revision ${value.timeline.revision}: ${value.timeline.state}`
|
|
68
|
+
: value.state === 'prepared' ? '\nPrepared only. Use timeline-apply with this session and request ID to approve its application.'
|
|
69
|
+
: value.state === 'not_received' ? '\nNot recorded. A delayed prepare may still arrive. Withdraw the original before making a replacement.' : '\nNot applied. This request cannot be revived.');
|
|
70
|
+
if (['timeline-request', 'timeline-apply', 'timeline-withdraw'].includes(action)) {
|
|
71
|
+
const key = identifier(args[2]);
|
|
72
|
+
let value = await readTimelineIntent(key);
|
|
73
|
+
if (action !== 'timeline-request') {
|
|
74
|
+
if (!options.run && !(tty(options) && await selectOne(action === 'timeline-apply' ? 'Apply the exact original prepared schedule action?' : 'Withdraw the original unapplied schedule action?', [
|
|
75
|
+
{ value: false, label: 'Cancel' }, { value: true, label: action === 'timeline-apply' ? 'Apply original request' : 'Withdraw original request' }
|
|
76
|
+
], options.signal)))
|
|
77
|
+
throw new CliError('world_timeline_consent_required', 'Use --run to approve this original request action.', 2);
|
|
78
|
+
let digest = value.request_digest;
|
|
79
|
+
if (action === 'timeline-withdraw' && options.file) {
|
|
80
|
+
const saved = JSON.parse(await requestInput(options.file, options.signal));
|
|
81
|
+
if (!saved || Object.keys(saved).length !== 1 || typeof saved.request_digest !== 'string' || !/^[a-f0-9]{64}$/.test(saved.request_digest))
|
|
82
|
+
throw new CliError('arguments_invalid', 'The withdrawal file must contain only the original request_digest from the request receipt.', 2);
|
|
83
|
+
if (digest && digest !== saved.request_digest)
|
|
84
|
+
throw new CliError('world_timeline_response_invalid', 'The saved checksum does not match this original request.', 2);
|
|
85
|
+
digest = saved.request_digest;
|
|
86
|
+
}
|
|
87
|
+
if (action === 'timeline-withdraw' && !digest)
|
|
88
|
+
throw new CliError('world_timeline_original_body_required', 'The server has not retained this request checksum. Use --file containing only the original request_digest printed before submission, so a delayed prepare cannot revive it.', 2);
|
|
89
|
+
if (action === 'timeline-apply' && value.state === 'not_received')
|
|
90
|
+
throw new CliError('world_timeline_original_body_required', 'Prepare the exact original JSON file with its original --request-key. Nothing was applied.', 2);
|
|
91
|
+
value = await readTimelineIntent(key, action === 'timeline-apply' ? 'POST' : 'DELETE', action === 'timeline-apply' ? {} : { request_digest: digest });
|
|
92
|
+
}
|
|
93
|
+
output('world_timeline_request', value, intentText(value));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const readSession = async () => {
|
|
97
|
+
const value = await api(base, auth);
|
|
98
|
+
if (!worldSessionMatches(value, { workspace: scope.workspace, world: scope.world, actor: scope.owner }, { id: session }))
|
|
99
|
+
throw new CliError('world_session_receipt_invalid', 'The selected world session could not be confirmed.', 2);
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
if (paged) {
|
|
103
|
+
const route = action === 'timeline-events' ? `timelines/${identifier(args[2])}/events` : action;
|
|
104
|
+
const page = Number(options.page ?? 1);
|
|
105
|
+
const value = await api(`${base}/${route}?page=${page}${options.search ? '&q=' + encodeURIComponent(options.search) : ''}`, auth);
|
|
106
|
+
if (!resourcePageMatches(value, page, one => action === 'timeline-events' ? worldScheduledEventMatches(one, args[2])
|
|
107
|
+
: action === 'timelines' ? worldTimelineMatches(one, context) : action === 'extensions' ? worldExtensionMatches(one, context)
|
|
108
|
+
: applicationCheckpointMatches(one, scope.world, { session })))
|
|
109
|
+
throw new CliError('world_lifecycle_response_invalid', 'The selected lifecycle history could not be confirmed.', 2);
|
|
110
|
+
output('world_' + action.replaceAll('-', '_'), value, value.results.map(one => `${one.id} ${one.state}${'name' in one ? '\n' + one.name : ''}${'due_at' in one ? '\n' + one.due_at : ''}`).join('\n')
|
|
111
|
+
+ `\nPage ${value.page}; ${value.count} records.${value.next ? `\nContinue with --page ${value.page + 1}` : ''}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (action === 'extension-status') {
|
|
115
|
+
const key = identifier(args[2]), value = await api(`${base}/extensions/${key}`, auth);
|
|
116
|
+
if (!worldExtensionMatches(value, context) || value.id !== key && value.request_key !== key)
|
|
117
|
+
throw new CliError('world_extension_response_invalid', 'Inspect the original extension for this session.', 2);
|
|
118
|
+
output('world_extension', value, `${value.state}\nUntil ${value.requested_expires_at}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (action === 'save-application-state') {
|
|
122
|
+
const row = await readSession();
|
|
123
|
+
if (!row.application_checkpoint.available || !row.observation_current)
|
|
124
|
+
throw new CliError('application_checkpoint_unsupported', 'This running agent has no available source-defined checkpoint API.', 2);
|
|
125
|
+
if (!options.name?.trim())
|
|
126
|
+
throw new CliError('arguments_invalid', 'Name the application checkpoint with --name.', 2);
|
|
127
|
+
output('application_checkpoint_review', row.application_checkpoint, `Source-declared components: ${row.application_checkpoint.components.map(one => one.name).join(', ')}\nNot included: ${row.application_checkpoint.excluded.join(', ')}. Controlled service state is separate.`);
|
|
128
|
+
if (!options.run && !(tty(options) && await selectOne('Save the declared application state?', [{ value: false, label: 'Cancel' }, { value: true, label: 'Save application state' }], options.signal)))
|
|
129
|
+
throw new CliError('application_checkpoint_consent_required', 'Use --run to approve this source-defined capture.', 2);
|
|
130
|
+
const key = identifier(options.request ?? randomUUID());
|
|
131
|
+
output('application_checkpoint_intent', { request_key: key }, `Request: ${key}\nRecovery: sessions application-checkpoint ${key}. Do not create another capture to recover an uncertain reply.`);
|
|
132
|
+
const value = await api(`${base}/application-checkpoints`, { ...auth, method: 'POST', idempotencyKey: key,
|
|
133
|
+
body: { name: options.name.trim(), expected_reset_count: row.reset_count, save_consent: true } });
|
|
134
|
+
if (!applicationCheckpointMatches(value, scope.world, { session, key }) || value.name !== options.name.trim())
|
|
135
|
+
throw new CliError('application_checkpoint_response_invalid', 'Inspect the exact original capture request. Nothing was repeated.', 2);
|
|
136
|
+
output('application_checkpoint', value, `${String(value.state)}\nCheckpoint ${String(value.id)}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (action === 'schedule' || action === 'timeline-control') {
|
|
140
|
+
if (!options.file)
|
|
141
|
+
throw new CliError('arguments_invalid', 'Provide the exact reviewed JSON request with --file.', 2);
|
|
142
|
+
const raw = await requestInput(options.file, options.signal);
|
|
143
|
+
if (!options.run && !(tty(options) && await selectOne(action === 'schedule' ? 'Approve this world event schedule?' : 'Change the pending event schedule?', [
|
|
144
|
+
{ value: false, label: 'Cancel' }, { value: true, label: 'Approve schedule', detail: 'Only controlled simulated service operations. Previously sent requests remain in history.' }
|
|
145
|
+
], options.signal)))
|
|
146
|
+
throw new CliError('world_timeline_consent_required', 'Use --run to approve the exact schedule. Nothing was changed.', 2);
|
|
147
|
+
const key = identifier(options.request ?? randomUUID());
|
|
148
|
+
const input = JSON.parse(raw);
|
|
149
|
+
const selected = { action: action === 'schedule' ? 'schedule' : 'control', timeline: action === 'schedule' ? null : identifier(args[2]) };
|
|
150
|
+
const current = await readSession();
|
|
151
|
+
const body = { action: selected.action, timeline_id: selected.timeline,
|
|
152
|
+
expected_reset_count: action === 'schedule' ? String(input.expected_reset_count ?? '') : current.reset_count, input };
|
|
153
|
+
const digest = createHash('sha256').update(worldTimelineCanonical({ world_id: scope.world, session_id: session, ...body })).digest('hex');
|
|
154
|
+
output('world_timeline_intent', { request_key: key, session_id: session, request_digest: digest }, `Request: ${key}\nChecksum: ${digest}\nRecovery: sessions timeline-request ${session} ${key} --world ${scope.world} --workspace ${scope.workspace}. Keep the original JSON file and request ID.`);
|
|
155
|
+
let receipt;
|
|
156
|
+
try {
|
|
157
|
+
receipt = await readTimelineIntent(key, 'PUT', body, selected);
|
|
158
|
+
if (receipt.state === 'prepared')
|
|
159
|
+
receipt = await readTimelineIntent(key, 'POST', {}, selected);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (options.signal.aborted)
|
|
163
|
+
throw error;
|
|
164
|
+
if (!(error instanceof CliError) || !['connection_uncertain', 'response_unavailable', 'world_timeline_response_invalid'].includes(error.code))
|
|
165
|
+
throw error;
|
|
166
|
+
try {
|
|
167
|
+
receipt = await readTimelineIntent(key, 'GET', undefined, selected);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new CliError('world_timeline_acceptance_unconfirmed', `Read sessions timeline-request ${session} ${key} before another request. No replacement was submitted.`, 2);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
output('world_timeline_request', receipt, intentText(receipt));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const row = await readSession();
|
|
177
|
+
if (!row.can_extend)
|
|
178
|
+
throw new CliError('world_extension_not_ready', 'This world cannot extend its runtime now.', 2);
|
|
179
|
+
const maximum = Math.max(0, Math.floor(row.maximum_extension_seconds - Math.max(0, (Date.parse(row.expires_at) - Date.now()) / 1000)));
|
|
180
|
+
if (maximum < 60)
|
|
181
|
+
throw new CliError('world_extension_not_ready', 'There is no additional runtime available in the current finite extension window.', 2);
|
|
182
|
+
const seconds = options.seconds ? Number(options.seconds) : tty(options) ? await selectOne('Additional runtime', [
|
|
183
|
+
{ value: 60, label: '1 minute' }, { value: 3600, label: '1 hour' }, { value: 86400, label: '1 day' }, { value: 604800, label: '7 days' }
|
|
184
|
+
].filter(one => one.value <= maximum), options.signal) : NaN;
|
|
185
|
+
if (!Number.isSafeInteger(seconds) || seconds < 60 || seconds > maximum)
|
|
186
|
+
throw new CliError('arguments_invalid', 'Choose additional runtime within this session’s available extension window.', 2);
|
|
187
|
+
const input = { additional_seconds: seconds, expected_expires_at: row.expires_at, reset_count: row.reset_count };
|
|
188
|
+
if (action === 'extend' && !options.usageQuote && !tty(options))
|
|
189
|
+
throw new CliError('billing_quote_required', 'Use sessions extension-quote first, then --usage-quote with --run.', 2);
|
|
190
|
+
const quote = options.usageQuote ? await api(`/api/workspaces/${scope.workspace}/billing/usage/quotes/${identifier(options.usageQuote)}`, auth)
|
|
191
|
+
: await api(`${base}/extension-quotes`, { ...auth, method: 'POST', idempotencyKey: randomUUID(), body: input });
|
|
192
|
+
if (!worldQuoteMatches(quote, scope.workspace) || quote.scope.schema !== 'firedrill.world-extension-budget.v1'
|
|
193
|
+
|| quote.scope.session_id !== row.id || quote.scope.reset_count !== row.reset_count || quote.scope.expected_expires_at !== row.expires_at
|
|
194
|
+
|| Date.parse(String(quote.scope.requested_expires_at)) !== Date.parse(row.expires_at) + seconds * 1000)
|
|
195
|
+
throw new CliError('billing_scope_mismatch', 'Review a current quote for this exact extension.', 2);
|
|
196
|
+
output('world_extension_quote', quote, `${worldCredits(quote)} credits maximum. Until ${String(quote.scope.requested_expires_at)}`);
|
|
197
|
+
if (action === 'extension-quote')
|
|
198
|
+
return;
|
|
199
|
+
if (!options.run && !(tty(options) && await selectOne('Approve the runtime extension?', [{ value: false, label: 'Cancel' }, { value: true, label: 'Extend runtime' }], options.signal)))
|
|
200
|
+
throw new CliError('world_extension_consent_required', 'Approve the exact extension with --run. Nothing was changed.', 2);
|
|
201
|
+
const key = identifier(options.request ?? randomUUID());
|
|
202
|
+
output('world_extension_intent', { request_key: key }, `Request: ${key}\nRecovery: sessions extension-status ${row.id} ${key}`);
|
|
203
|
+
const value = await api(`${base}/extensions`, { ...auth, method: 'POST', idempotencyKey: key, body: { ...input, extend_consent: true, usage_quote_id: quote.id } });
|
|
204
|
+
if (!worldExtensionMatches(value, row, { key, actor: scope.owner, input: { ...input, usage_quote_id: quote.id } }))
|
|
205
|
+
throw new CliError('world_extension_response_invalid', 'Inspect the exact original extension request. Nothing was repeated.', 2);
|
|
206
|
+
output('world_extension', value, `${value.state}\nInspect the original extension request before another action.`);
|
|
207
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { api } from './auth.js';
|
|
3
|
+
import { line } from './configuration.js';
|
|
4
|
+
import { CliError } from './http.js';
|
|
5
|
+
import { selectOne } from './prompts.js';
|
|
6
|
+
import { requestInput } from './request-input.js';
|
|
7
|
+
import { selectedWorld } from './world-access.js';
|
|
8
|
+
import { identifier } from './world-contracts.js';
|
|
9
|
+
import { worldSessionMatches, worldSessionsPageMatches } from '../../contracts/session-contracts.js';
|
|
10
|
+
import { sessionCommandMatches, sessionCommandsPageMatches, sessionCommandRequestMatches, sessionConnectionsPageMatches, callableOperationMatches, callableOperationsPageMatches } from '../../contracts/session-command-contracts.js';
|
|
11
|
+
export const SESSION_REQUEST_ACTIONS = ['call', 'connections', 'requests', 'request-show', 'request-result'];
|
|
12
|
+
const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
13
|
+
function describe(row) {
|
|
14
|
+
return `${row.operation}: ${row.state}\nRequest ID: ${row.id}`
|
|
15
|
+
+ (row.response ? `\n${row.protocol === 'mcp' ? row.response.mcp_is_error ? 'MCP tool error' : 'MCP response recorded' : `HTTP ${row.response.http_status}`}; ${row.response.body_size} response bytes.` : '')
|
|
16
|
+
+ (row.response?.schema_status ? `\nOutput schema: ${{ matched: 'matches declaration', mismatch: 'does not match declaration', missing_structured: 'structured result missing', not_declared: 'not declared' }[row.response.schema_status] ?? 'unavailable'}.` : '')
|
|
17
|
+
+ (row.evidence_withheld ? '\nSome bytes were withheld because they contained a private value.' : '')
|
|
18
|
+
+ '\nManual service request, not a simulation drill.';
|
|
19
|
+
}
|
|
20
|
+
export async function worldSessionRequests(args, options, output) {
|
|
21
|
+
const action = args[0], tty = !options.json && Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
22
|
+
if (args.length > (action === 'call' ? 4 : ['request-show', 'request-result'].includes(action) ? 3 : 2)
|
|
23
|
+
|| options.file !== undefined && action !== 'call' || options.run && action !== 'call'
|
|
24
|
+
|| options.page !== undefined && !['requests', 'connections'].includes(action)
|
|
25
|
+
|| options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page) || (options.search?.length ?? 0) > 200) {
|
|
26
|
+
throw new CliError('arguments_invalid', 'Use sessions call [session] [connection] [operation], connections [session], requests [session], request-show <session> <id>, or request-result <session> <key>.', 2);
|
|
27
|
+
}
|
|
28
|
+
const scope = await selectedWorld({ ...options, interactive: tty });
|
|
29
|
+
const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
30
|
+
const identity = { actor: scope.owner, workspace: scope.workspace, world: scope.world };
|
|
31
|
+
const prefix = `${scope.prefix}/runtime-sessions`;
|
|
32
|
+
// Every selector reads bounded server pages. Keyboard navigation never
|
|
33
|
+
// requires downloading the whole world, schema array or session history.
|
|
34
|
+
async function choose(title, path, key, text, matches) {
|
|
35
|
+
let page = 1, search = options.search ?? '';
|
|
36
|
+
while (true) {
|
|
37
|
+
const selectingSession = path.split('?')[0] === prefix;
|
|
38
|
+
const data = await api(`${path}${path.includes('?') ? '&' : '?'}page=${page}${selectingSession ? '' : '&q=' + encodeURIComponent(search)}`, auth);
|
|
39
|
+
if (selectingSession && !worldSessionsPageMatches(data, identity, action === 'call' ? 'active' : ''))
|
|
40
|
+
throw new CliError('session_scope_invalid', 'Session history could not be confirmed in this world.', 2);
|
|
41
|
+
if (matches && !matches(data, search))
|
|
42
|
+
throw new CliError('session_request_invalid', 'The selected service records could not be confirmed. Nothing was submitted.');
|
|
43
|
+
const selected = await selectOne(title, [
|
|
44
|
+
...data.results.map(row => ({ value: 'row:' + key(row), ...text(row) })),
|
|
45
|
+
...(page > 1 ? [{ value: '__previous__', label: 'Previous page' }] : []),
|
|
46
|
+
...(data.next ? [{ value: '__next__', label: 'Next page' }] : []),
|
|
47
|
+
...(!title.includes('session') ? [{ value: '__search__', label: 'Search' }] : []),
|
|
48
|
+
{ value: '__cancel__', label: 'Cancel' },
|
|
49
|
+
], options.signal);
|
|
50
|
+
if (selected === '__cancel__')
|
|
51
|
+
throw new CliError('canceled', 'No manual operation was submitted.', 130);
|
|
52
|
+
if (selected === '__next__') {
|
|
53
|
+
page++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (selected === '__previous__') {
|
|
57
|
+
page--;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (selected === '__search__') {
|
|
61
|
+
search = await line('Find an entry (blank for all)', 200, options.signal);
|
|
62
|
+
page = 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const found = data.results.find(row => 'row:' + key(row) === selected);
|
|
66
|
+
if (!found)
|
|
67
|
+
throw new CliError('selection_changed', 'The selection changed. Nothing was submitted.', 2);
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let sessionId = args[1];
|
|
72
|
+
if (!sessionId && tty)
|
|
73
|
+
sessionId = (await choose('World session', prefix + (action === 'call' ? '?state=active' : ''), row => row.id, row => ({ label: row.id, detail: row.state }))).id;
|
|
74
|
+
if (!sessionId)
|
|
75
|
+
throw new CliError('session_required', 'Provide a saved session ID from sessions list.', 2);
|
|
76
|
+
sessionId = identifier(sessionId);
|
|
77
|
+
const session = await api(`${prefix}/${sessionId}`, auth);
|
|
78
|
+
if (!worldSessionMatches(session, identity, { id: sessionId }))
|
|
79
|
+
throw new CliError('session_scope_invalid', 'Choose a session in this world.', 2);
|
|
80
|
+
const base = `${prefix}/${sessionId}`;
|
|
81
|
+
const commandScope = { ...identity, session: sessionId };
|
|
82
|
+
const query = `page=${options.page ?? '1'}&q=${encodeURIComponent(options.search ?? '')}`;
|
|
83
|
+
function emit(row) {
|
|
84
|
+
if (!sessionCommandMatches(row, commandScope))
|
|
85
|
+
throw new CliError('session_request_invalid', 'The original request could not be confirmed. It was not repeated.');
|
|
86
|
+
output('world_session_request', row, describe(row));
|
|
87
|
+
}
|
|
88
|
+
if (action === 'connections') {
|
|
89
|
+
const data = await api(`${base}/connections?${query}`, auth);
|
|
90
|
+
if (!sessionConnectionsPageMatches(data, commandScope, { query: options.search }))
|
|
91
|
+
throw new CliError('session_request_invalid', 'The selected connections could not be confirmed.');
|
|
92
|
+
output('world_session_connections', data, (data.results.map(row => `${row.variable} ${row.protocol} ${row.service_id}`).join('\n') || 'No matching session connections.') + continuation(data));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (action === 'requests') {
|
|
96
|
+
const data = await api(`${base}/requests?${query}`, auth);
|
|
97
|
+
if (!sessionCommandsPageMatches(data, commandScope, options.search))
|
|
98
|
+
throw new CliError('session_request_invalid', 'The request history could not be confirmed in this session.');
|
|
99
|
+
output('world_session_requests', data, (data.results.map(describe).join('\n\n') || 'No matching manual requests.') + continuation(data));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
function continuation(value) {
|
|
103
|
+
const quote = (one) => `'${one.replaceAll("'", "'\\''")}'`;
|
|
104
|
+
return `\nPage ${value.page}, ${value.count} matching records.${value.next
|
|
105
|
+
? `\nContinue: firedrill sessions ${action} ${sessionId} --world ${scope.world} --workspace ${scope.workspace} --page ${value.page + 1}${options.search ? ' --search ' + quote(options.search) : ''}${options.json ? ' --json' : ''}` : ''}`;
|
|
106
|
+
}
|
|
107
|
+
if (action === 'request-show') {
|
|
108
|
+
const id = identifier(args[2] ?? ''), value = await api(`${base}/requests/${id}`, auth);
|
|
109
|
+
if (!sessionCommandMatches(value, commandScope, { id }))
|
|
110
|
+
throw new CliError('session_request_invalid', 'The selected request could not be confirmed.');
|
|
111
|
+
emit(value);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
async function lookup(key, present = true) {
|
|
115
|
+
const data = await api(`${base}/request-intent?request_key=${encodeURIComponent(key)}`, auth);
|
|
116
|
+
if (!sessionCommandRequestMatches(data, commandScope, key)) {
|
|
117
|
+
throw new CliError('session_request_invalid', 'The original request could not be confirmed. Do not repeat it.');
|
|
118
|
+
}
|
|
119
|
+
if (present) {
|
|
120
|
+
output('world_session_request_intent', data, `Request: ${key}\n${data.state === 'not_observed' ? 'Not observed yet. A delayed original request may still arrive. Do not submit a replacement.' : 'Original request retained.'}`);
|
|
121
|
+
if (data.request)
|
|
122
|
+
emit(data.request);
|
|
123
|
+
}
|
|
124
|
+
return data.request;
|
|
125
|
+
}
|
|
126
|
+
if (action === 'request-result') {
|
|
127
|
+
await lookup(identifier(args[2] ?? ''));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!session.observation_current || session.state !== 'active' || session.terminal_at || !session.can_control) {
|
|
131
|
+
throw new CliError('session_not_ready', 'Wait for a current active session you can control. Nothing was submitted.', 2);
|
|
132
|
+
}
|
|
133
|
+
let connection;
|
|
134
|
+
if (args[2]) {
|
|
135
|
+
if (!VARIABLE.test(args[2]))
|
|
136
|
+
throw new CliError('connection_invalid', 'Choose an exact connection name from sessions connections.', 2);
|
|
137
|
+
const data = await api(`${base}/connections?callable_only=true&variable=${encodeURIComponent(args[2])}&page_size=2`, auth);
|
|
138
|
+
if (!sessionConnectionsPageMatches(data, commandScope, { callable: true, variable: args[2] }) || data.count !== 1 || data.results[0]?.variable !== args[2])
|
|
139
|
+
throw new CliError('connection_invalid', 'This session has no unique callable connection with that name.', 2);
|
|
140
|
+
connection = data.results[0];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
if (!tty)
|
|
144
|
+
throw new CliError('connection_required', 'Provide a connection name from sessions connections.', 2);
|
|
145
|
+
connection = await choose('Connection', `${base}/connections?callable_only=true`, row => row.variable, row => ({ label: row.variable, detail: row.protocol }), (value, search) => sessionConnectionsPageMatches(value, commandScope, { callable: true, query: search }));
|
|
146
|
+
}
|
|
147
|
+
const operations = `${scope.prefix}/items/${identifier(connection.service_id)}/operations?callable_only=true&protocol=${connection.protocol}`;
|
|
148
|
+
let operation;
|
|
149
|
+
if (args[3]) {
|
|
150
|
+
if (args[3].length > 255)
|
|
151
|
+
throw new CliError('operation_invalid', 'Choose a declared operation name.', 2);
|
|
152
|
+
const data = await api(`${operations}&name=${encodeURIComponent(args[3])}&page_size=2`, auth);
|
|
153
|
+
if (!callableOperationsPageMatches(data, connection.service_id, connection.protocol) || data.count !== 1 || data.results[0]?.name !== args[3])
|
|
154
|
+
throw new CliError('operation_invalid', 'No unique declared operation matches this name.', 2);
|
|
155
|
+
operation = data.results[0];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (!tty)
|
|
159
|
+
throw new CliError('operation_required', 'Provide a declared operation name.', 2);
|
|
160
|
+
operation = await choose('Operation', operations, row => row.name, row => ({ label: row.name, detail: `${row.protocol === 'mcp' ? 'MCP' : row.method} · ${row.effect}` }), value => callableOperationsPageMatches(value, connection.service_id, connection.protocol));
|
|
161
|
+
}
|
|
162
|
+
if (!callableOperationMatches(operation) || operation.protocol !== connection.protocol)
|
|
163
|
+
throw new CliError('operation_invalid', 'Choose an operation with this connection transport.', 2);
|
|
164
|
+
let file = options.file;
|
|
165
|
+
if (!file && tty)
|
|
166
|
+
file = await line(operation.protocol === 'mcp' ? 'Arguments JSON file (blank for {})' : 'Request JSON file (optional)', 4096, options.signal) || undefined;
|
|
167
|
+
const raw = await requestInput(file, options.signal);
|
|
168
|
+
const selectedRequest = { service_id: connection.service_id, variable: connection.variable, operation: operation.name };
|
|
169
|
+
let body;
|
|
170
|
+
if (operation.protocol === 'mcp')
|
|
171
|
+
body = { ...selectedRequest, arguments_base64: Buffer.from(raw).toString('base64') };
|
|
172
|
+
else {
|
|
173
|
+
const input = JSON.parse(raw);
|
|
174
|
+
if (Object.keys(input).some(key => !['path', 'headers', 'body_base64'].includes(key)))
|
|
175
|
+
throw new CliError('request_file_invalid', 'HTTP request files contain path, headers and body_base64 only.', 2);
|
|
176
|
+
const requestedPath = input.path ?? operation.route;
|
|
177
|
+
if (typeof requestedPath !== 'string')
|
|
178
|
+
throw new CliError('request_path_invalid', 'Use a declared HTTP path.', 2);
|
|
179
|
+
let path = requestedPath;
|
|
180
|
+
if (tty && /[{}]/.test(path))
|
|
181
|
+
path = await line('HTTP path with record values', 8192, options.signal, path);
|
|
182
|
+
if (/[{}]/.test(path))
|
|
183
|
+
throw new CliError('request_path_required', 'Replace route parameters in the request file path. Nothing was submitted.', 2);
|
|
184
|
+
const headers = input.headers ?? {}, bytes = input.body_base64 ?? '';
|
|
185
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers) || Object.values(headers).some(value => typeof value !== 'string') || typeof bytes !== 'string')
|
|
186
|
+
throw new CliError('request_file_invalid', 'Use string header values and a base64 body. Nothing was submitted.', 2);
|
|
187
|
+
body = { ...selectedRequest, method: operation.method, path, headers: headers, body_base64: bytes };
|
|
188
|
+
}
|
|
189
|
+
const approved = options.run || tty && await selectOne('Send this manual service request?', [
|
|
190
|
+
{ value: false, label: 'Cancel' }, { value: true, label: 'Send request', detail: `${operation.name} through ${connection.variable}. Supplied arguments enter this existing world and its private history.` },
|
|
191
|
+
], options.signal);
|
|
192
|
+
if (!approved)
|
|
193
|
+
throw new CliError('session_request_consent_required', 'Nothing was submitted. Use --run to approve this exact request in automation.', 2);
|
|
194
|
+
const key = randomUUID();
|
|
195
|
+
output('world_session_request_intent', { request_key: key, session_id: sessionId }, `Request: ${key}\nIf interrupted, use sessions request-result ${sessionId} ${key}. Do not submit a replacement.`);
|
|
196
|
+
const matchesSelected = (value) => sessionCommandMatches(value, commandScope, { key, service: connection.service_id,
|
|
197
|
+
operation: operation.name, variable: connection.variable, protocol: operation.protocol });
|
|
198
|
+
try {
|
|
199
|
+
const value = await api(`${base}/requests`, { ...auth, method: 'POST', idempotencyKey: key, body });
|
|
200
|
+
if (!matchesSelected(value))
|
|
201
|
+
throw new CliError('session_request_unconfirmed', `Inspect sessions request-result ${sessionId} ${key}. Original acceptance is not confirmed.`, 2);
|
|
202
|
+
const saved = await lookup(key, false);
|
|
203
|
+
if (!saved || saved.id !== value.id || !matchesSelected(saved))
|
|
204
|
+
throw new CliError('session_request_unconfirmed', 'The original accepted request could not be confirmed. Nothing was repeated.', 2);
|
|
205
|
+
emit(saved);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (options.signal.aborted)
|
|
209
|
+
throw error;
|
|
210
|
+
const retained = await lookup(key, false);
|
|
211
|
+
if (!retained || !matchesSelected(retained))
|
|
212
|
+
throw new CliError('session_request_unconfirmed', `Use sessions request-result ${sessionId} ${key}. The original request was not repeated.`, 2);
|
|
213
|
+
emit(retained);
|
|
214
|
+
}
|
|
215
|
+
}
|