firedrill-run 2.2.5 → 3.0.1
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 +176 -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 +506 -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 +1627 -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 +96 -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,197 @@
|
|
|
1
|
+
import { api } from './auth.js';
|
|
2
|
+
import { CliError } from './http.js';
|
|
3
|
+
import { selectOne } from './prompts.js';
|
|
4
|
+
import { identifier } from './world-contracts.js';
|
|
5
|
+
import { observedCellValue, stateChangeMatches } from '../../contracts/trial-state-contracts.js';
|
|
6
|
+
import { checkpointReport, validateCheckpointReport } from './trial-checkpoints.js';
|
|
7
|
+
const labels = { inserted: 'Added', updated: 'Updated', deleted: 'Removed' };
|
|
8
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
const invalid = () => new CliError('trial_state_changed', 'The selected original state comparison could not be confirmed. Reopen the report.');
|
|
10
|
+
export function validateTrialStateOptions(args, options) {
|
|
11
|
+
if (args[0] === 'checkpoints') {
|
|
12
|
+
validateCheckpointReport(args, options);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const [comparison, action = 'changes', selected] = args;
|
|
16
|
+
if (args.length > 3 || comparison && !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(comparison)
|
|
17
|
+
|| !['changes', 'collections', 'events', 'causes'].includes(action) || !comparison && (args.length > 1 || options.search || options.status || options.evidenceKind)
|
|
18
|
+
|| options.checkpoint !== undefined || options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page)
|
|
19
|
+
|| (options.search?.length ?? 0) > 200 || /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(options.search ?? '')
|
|
20
|
+
|| options.status !== undefined && (action !== 'events' || !['completed', 'pending'].includes(options.status))
|
|
21
|
+
|| options.evidenceKind !== undefined && (action !== 'changes' || selected !== undefined || !Object.hasOwn(labels, options.evidenceKind))
|
|
22
|
+
|| selected !== undefined && (action === 'collections' || action === 'events' && !['before', 'after'].includes(selected)
|
|
23
|
+
|| ['changes', 'causes'].includes(action) && !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(selected))
|
|
24
|
+
|| action === 'causes' && (!selected || options.search !== undefined)
|
|
25
|
+
|| action === 'changes' && selected !== undefined && (options.page !== undefined || options.search !== undefined || options.status !== undefined || options.evidenceKind !== undefined))
|
|
26
|
+
throw new CliError('arguments_invalid', 'Use state [comparison], state <comparison> changes [change], causes <change>, collections, events [before|after], or state checkpoints. Search filters lists; --kind filters changes; --status filters events.', 2);
|
|
27
|
+
}
|
|
28
|
+
function boundaryLines(value, name) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return [];
|
|
31
|
+
return [`${name}: ${value.snapshot_at ?? 'Snapshot time not recorded'}`,
|
|
32
|
+
...(value.clock?.status === 'observed' ? [`SQL clock read: ${value.clock.read_at}; ${value.clock.mode === 'controlled'
|
|
33
|
+
? `declared schedule offset ${value.clock.offset_ms}ms; ${value.clock.events_completed} completed and ${value.clock.events_pending} pending events`
|
|
34
|
+
: 'wall clock'}. This is not target time or time saved.`] : [])];
|
|
35
|
+
}
|
|
36
|
+
function summary(value) {
|
|
37
|
+
return [`Attempt ${value.attempt_number}: ${value.interval === 'whole_trial' ? 'Whole drill' : `Boundary ${value.before_sequence} to ${value.after_sequence}`} · ${value.state === 'queued' ? 'Comparison pending' : value.state === 'unavailable' ? 'Comparison unavailable' : 'Retained state comparison'}`,
|
|
38
|
+
...(value.counts ? [`${value.counts.inserted.toLocaleString()} added; ${value.counts.updated.toLocaleString()} updated; ${value.counts.deleted.toLocaleString()} removed rows.`]
|
|
39
|
+
: ['Row changes have not been measured.']),
|
|
40
|
+
...(value.collections_with_gaps ? [`${value.collections_with_gaps.toLocaleString()} collections could not be compared row by row.`] : []),
|
|
41
|
+
...boundaryLines(value.boundaries?.before, 'Before'), ...boundaryLines(value.boundaries?.after, 'After')];
|
|
42
|
+
}
|
|
43
|
+
function shown(cell) {
|
|
44
|
+
const value = observedCellValue(cell);
|
|
45
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
46
|
+
return text.slice(0, 600) + (text.length > 600 || cell && 'truncated' in cell && cell.truncated ? '…' : '');
|
|
47
|
+
}
|
|
48
|
+
async function detail(prefix, change, scope, options, output, original) {
|
|
49
|
+
const parts = prefix.split('/');
|
|
50
|
+
const value = await api(`${prefix}/changes/${identifier(change)}`, { actor: scope.owner, workspace: scope.workspace,
|
|
51
|
+
signal: options.signal, maximumResponseBytes: 16 * 1024 ** 2 });
|
|
52
|
+
if (value.id !== change || original && !stateChangeMatches(value, original))
|
|
53
|
+
throw invalid();
|
|
54
|
+
output(value, [`${value.collection}: ${labels[value.kind]}`, ...value.changed_fields.slice(0, 40).map(name => `${name}\n Before: ${shown(value.before?.[name])}\n After: ${shown(value.after?.[name])}`),
|
|
55
|
+
...(value.changed_fields.length > 40 ? [`Showing 40 of ${value.changed_fields.length} fields. Use --json for complete typed records.`] : []),
|
|
56
|
+
`Read each link's basis: confirmed controlled SQL/scheduled row writes differ from temporal operation associations. Read linked evidence: firedrill report ${parts.at(-5)} drill ${parts.at(-3)} state ${parts.at(-1)} causes ${change} --workspace ${scope.workspace}`].join('\n\n'));
|
|
57
|
+
}
|
|
58
|
+
export async function trialStateReport(args, path, scope, interactive, options, output) {
|
|
59
|
+
validateTrialStateOptions(args, options);
|
|
60
|
+
const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
|
|
61
|
+
const parts = path.split('/');
|
|
62
|
+
const command = `firedrill report ${identifier(parts.at(-3))} drill ${identifier(parts.at(-1))} state`;
|
|
63
|
+
if (args[0] === 'checkpoints') {
|
|
64
|
+
await checkpointReport(args, path, command, scope, interactive, options, output);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let comparison = args[0], page = Number(options.page ?? 1), chosen = false;
|
|
68
|
+
if (!comparison) {
|
|
69
|
+
while (true) {
|
|
70
|
+
const data = await api(`${path}/state-comparisons?page=${page}`, auth);
|
|
71
|
+
output(data, [...data.results.map(row => [...summary(row), row.id,
|
|
72
|
+
`Inspect: ${command} ${row.id} --workspace ${scope.workspace}`].join('\n')),
|
|
73
|
+
...(!data.count ? ['No complete state interval is available. No empty state or zero change count is inferred.'] : []),
|
|
74
|
+
`Task checkpoints: ${command} checkpoints --workspace ${scope.workspace}`,
|
|
75
|
+
`${data.observations_retained.toLocaleString()} state observations retained. Page ${data.page}; ${data.count.toLocaleString()} comparisons.`,
|
|
76
|
+
...(data.next ? [`Continue: ${command} --workspace ${scope.workspace} --page ${page + 1}${options.json ? ' --json' : ''}`] : [])].join('\n\n'));
|
|
77
|
+
if (!interactive)
|
|
78
|
+
return;
|
|
79
|
+
const choices = data.results.map(row => ({ value: row.id, label: `Attempt ${row.attempt_number}`, detail: summary(row).join(' ') }));
|
|
80
|
+
choices.push({ value: 'checkpoints', label: 'Task checkpoints and observed records', detail: 'Inspect actual boundary state without invoking the target' });
|
|
81
|
+
if (page > 1)
|
|
82
|
+
choices.push({ value: 'previous', label: 'Previous page', detail: '' });
|
|
83
|
+
if (data.next)
|
|
84
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
85
|
+
choices.push({ value: 'done', label: 'Done', detail: '' });
|
|
86
|
+
const value = await selectOne('State comparison', choices, options.signal);
|
|
87
|
+
if (value === 'done')
|
|
88
|
+
return;
|
|
89
|
+
if (value === 'checkpoints') {
|
|
90
|
+
await checkpointReport(['checkpoints'], path, command, scope, true, { ...options, page: undefined, search: undefined }, output);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (value === 'previous' || value === 'next') {
|
|
94
|
+
page += value === 'next' ? 1 : -1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
comparison = identifier(value);
|
|
98
|
+
chosen = true;
|
|
99
|
+
page = 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const selectedComparison = identifier(comparison);
|
|
104
|
+
const mode = args[1] ?? (interactive && !options.search && !options.status && !options.evidenceKind ? await selectOne('Inspect state', [
|
|
105
|
+
{ value: 'changes', label: 'Observed row changes' }, { value: 'collections', label: 'Collection coverage and gaps' },
|
|
106
|
+
{ value: 'events', label: 'Scheduled changes at a boundary' }, { value: 'done', label: 'Done' }
|
|
107
|
+
], options.signal) : 'changes');
|
|
108
|
+
if (mode === 'done')
|
|
109
|
+
return;
|
|
110
|
+
const prefix = `${path}/state-comparisons/${selectedComparison}`;
|
|
111
|
+
if (mode === 'causes') {
|
|
112
|
+
const change = identifier(args[2]);
|
|
113
|
+
while (true) {
|
|
114
|
+
const data = await api(`${prefix}/changes/${change}/causes?page=${page}`, auth);
|
|
115
|
+
output(data, [...data.results.map(row => `${row.producer_origin === 'customer_device' ? 'Device reported: ' : ''}${row.kind}: ${row.reference}\n${row.basis === 'committed_tool_transaction' ? 'Confirmed controlled SQL tool write' : row.basis === 'committed_schedule_transaction' ? 'Confirmed scheduled write in this linked interval' : 'Observed interval association, not proof of causality'}${row.transactions.length ? `\nNative transactions: ${row.transactions.join(', ')}` : ''}${row.relationship === 'row_history' ? `\nEarlier row mutation, not proof of contribution to the final value: ${command} ${row.source_comparison_id} --workspace ${scope.workspace}` : ''}${row.action_id ? `\nOperation: firedrill report ${parts.at(-3)} drill ${parts.at(-1)} actions ${row.action_id} --workspace ${scope.workspace}` : ''}`),
|
|
116
|
+
...(!data.count ? ['No individual cause was established for this change.'] : []), `Page ${data.page}; ${data.count.toLocaleString()} linked references.`,
|
|
117
|
+
...(data.next ? [`Continue: ${command} ${selectedComparison} causes ${change} --workspace ${scope.workspace} --page ${page + 1}${options.json ? ' --json' : ''}`] : [])].join('\n\n'));
|
|
118
|
+
if (!interactive)
|
|
119
|
+
return;
|
|
120
|
+
const choices = [{ value: 'done', label: 'Done' }];
|
|
121
|
+
if (page > 1)
|
|
122
|
+
choices.unshift({ value: 'previous', label: 'Previous page' });
|
|
123
|
+
if (data.next)
|
|
124
|
+
choices.unshift({ value: 'next', label: 'Next page' });
|
|
125
|
+
const value = await selectOne('Linked state evidence', choices, options.signal);
|
|
126
|
+
if (value === 'done')
|
|
127
|
+
return;
|
|
128
|
+
page += value === 'next' ? 1 : -1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (mode === 'changes' && args[2]) {
|
|
132
|
+
await detail(prefix, args[2], scope, options, output);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const term = chosen ? '' : options.search ?? '', kind = options.evidenceKind ?? '', status = options.status ?? '', phase = args[2] ?? 'after';
|
|
136
|
+
const continuation = `${command} ${selectedComparison} ${mode}${mode === 'events' ? ' ' + phase : ''} --workspace ${scope.workspace}`
|
|
137
|
+
+ (term ? ' --search ' + quote(term) : '') + (kind ? ' --kind ' + kind : '') + (status ? ' --status ' + status : '') + (options.json ? ' --json' : '');
|
|
138
|
+
while (true) {
|
|
139
|
+
const params = new URLSearchParams({ page: String(page), q: term });
|
|
140
|
+
if (mode === 'events') {
|
|
141
|
+
params.set('phase', phase);
|
|
142
|
+
params.set('status', status);
|
|
143
|
+
}
|
|
144
|
+
if (mode === 'changes')
|
|
145
|
+
params.set('kind', kind);
|
|
146
|
+
const route = mode === 'events' ? 'world-events' : mode;
|
|
147
|
+
let rows = [], count, next;
|
|
148
|
+
if (mode === 'changes') {
|
|
149
|
+
const data = await api(`${prefix}/${route}?${params}`, auth);
|
|
150
|
+
if (data.comparison.id !== selectedComparison)
|
|
151
|
+
throw invalid();
|
|
152
|
+
rows = data.results;
|
|
153
|
+
count = data.count;
|
|
154
|
+
next = data.next;
|
|
155
|
+
output(data, [...summary(data.comparison), ...rows.map(row => `${labels[row.kind]}: ${row.collection}\n${Object.entries(row.identity).map(([key, value]) => `${key}: ${shown(value)}`).join('; ')}\n${row.field_count} changed fields; ${row.id}\nRead: ${command} ${selectedComparison} changes ${row.id} --workspace ${scope.workspace}`),
|
|
156
|
+
...(!count ? [data.comparison.state === 'queued' ? 'Comparing retained observations.' : data.comparison.counts === null
|
|
157
|
+
? 'Row changes could not be measured. Inspect collection coverage or original files.' : 'No matching observed row changes.'] : []),
|
|
158
|
+
`Page ${data.page}; ${count.toLocaleString()} matching changes.`, ...(next ? [`Continue: ${continuation} --page ${page + 1}`] : [])].join('\n\n'));
|
|
159
|
+
}
|
|
160
|
+
else if (mode === 'collections') {
|
|
161
|
+
const data = await api(`${prefix}/${route}?${params}`, auth);
|
|
162
|
+
count = data.count;
|
|
163
|
+
next = data.next;
|
|
164
|
+
output(data, [...data.results.map(row => `${row.name}: ${row.before_rows ?? 'Not present'} rows before; ${row.after_rows ?? 'Not present'} after.\n${row.comparable ? 'Rows compared' : row.gap === 'schema_changed' ? 'Schema changed' : 'Stable row key unavailable'}; collection ${row.collection_change}.`),
|
|
165
|
+
...(!count ? ['No matching observed collections.'] : []), `Page ${data.page}; ${count.toLocaleString()} collections.`,
|
|
166
|
+
...(next ? [`Continue: ${continuation} --page ${page + 1}`] : [])].join('\n\n'));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const data = await api(`${prefix}/${route}?${params}`, auth);
|
|
170
|
+
count = data.count;
|
|
171
|
+
next = data.next;
|
|
172
|
+
output(data, [...boundaryLines(data.boundary, phase === 'before' ? 'Before' : 'After'),
|
|
173
|
+
...data.results.map(row => `${row.id}: ${row.action} ${row.collection}; ${row.offset_ms}ms from declared world start; ${row.status}.`),
|
|
174
|
+
...(!count ? ['No matching scheduled changes in this observation.'] : []), `Page ${data.page}; ${count.toLocaleString()} events.`,
|
|
175
|
+
...(next ? [`Continue: ${continuation} --page ${page + 1}`] : [])].join('\n\n'));
|
|
176
|
+
}
|
|
177
|
+
if (!interactive)
|
|
178
|
+
return;
|
|
179
|
+
const choices = rows.map(row => ({ value: row.id, label: `${labels[row.kind]}: ${row.collection}`, detail: `${row.field_count} changed fields` }));
|
|
180
|
+
if (page > 1)
|
|
181
|
+
choices.push({ value: 'previous', label: 'Previous page', detail: '' });
|
|
182
|
+
if (next)
|
|
183
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
184
|
+
choices.push({ value: 'done', label: 'Done', detail: '' });
|
|
185
|
+
const value = await selectOne('State report', choices, options.signal);
|
|
186
|
+
if (value === 'done')
|
|
187
|
+
return;
|
|
188
|
+
if (value === 'previous' || value === 'next') {
|
|
189
|
+
page += value === 'next' ? 1 : -1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const original = rows.find(row => row.id === value);
|
|
193
|
+
if (!original)
|
|
194
|
+
throw invalid();
|
|
195
|
+
await detail(prefix, value, scope, options, output, original);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { APP_ORIGIN } from './http.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { api, openBrowser } from './auth.js';
|
|
6
|
+
import { privateDirectory } from './credentials.js';
|
|
7
|
+
import { auth, choosePaged, interactive, line, query } from './configuration.js';
|
|
8
|
+
import { readMetadata, record } from './configuration-intents.js';
|
|
9
|
+
import { CliError } from './http.js';
|
|
10
|
+
import { secretLine, selectOne, terminalText } from './prompts.js';
|
|
11
|
+
import { selectedWorkspace } from './world-access.js';
|
|
12
|
+
import { identifier } from './world-contracts.js';
|
|
13
|
+
const invalid = () => new CliError('vault_input_invalid', 'Use the exact Vault metadata and original request. Values belong in hidden input or --value-stdin, never JSON metadata or command arguments.', 2);
|
|
14
|
+
const fields = (value, keys) => Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
15
|
+
const at = (value) => value === null || typeof value === 'string' && value.length <= 40
|
|
16
|
+
&& /(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
17
|
+
function checkedMetadata(value) {
|
|
18
|
+
if (!record(value) || !fields(value, ['name', 'secret', 'phase', 'purpose', 'expires_at', ...(value.replaces_id ? ['replaces_id', 'basis_version'] : [])])
|
|
19
|
+
|| typeof value.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value.name) || typeof value.secret !== 'boolean'
|
|
20
|
+
|| !['build', 'runtime', 'both'].includes(String(value.phase)) || typeof value.purpose !== 'string' || value.purpose.length > 255 || !at(value.expires_at)
|
|
21
|
+
|| value.replaces_id && (typeof value.replaces_id !== 'string' || !Number.isSafeInteger(value.basis_version) || Number(value.basis_version) < 1))
|
|
22
|
+
throw invalid();
|
|
23
|
+
if (value.replaces_id)
|
|
24
|
+
identifier(String(value.replaces_id));
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
const same = (a, b) => ['name', 'secret', 'phase', 'purpose', 'replaces_id', 'basis_version']
|
|
28
|
+
.every(key => a[key] === b[key])
|
|
29
|
+
&& (a.expires_at === null ? b.expires_at === null : b.expires_at !== null && Date.parse(a.expires_at) === Date.parse(b.expires_at));
|
|
30
|
+
const location = async (scope, key) => join(await privateDirectory(), `vault-${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(key)}.json`);
|
|
31
|
+
async function retained(scope, key) {
|
|
32
|
+
const value = await readMetadata(await location(scope, key), true);
|
|
33
|
+
if (!fields(value, ['schema', 'actor', 'workspace', 'key', 'metadata']) || value.schema !== 'firedrill.local-vault-intent.v1'
|
|
34
|
+
|| value.actor !== scope.owner || value.workspace !== scope.workspace || value.key !== identifier(key))
|
|
35
|
+
throw invalid();
|
|
36
|
+
checkedMetadata(value.metadata);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
async function remember(scope, value) {
|
|
40
|
+
checkedMetadata(value.metadata);
|
|
41
|
+
try {
|
|
42
|
+
const file = await open(await location(scope, value.key), 'wx', 0o600);
|
|
43
|
+
try {
|
|
44
|
+
await file.writeFile(JSON.stringify(value));
|
|
45
|
+
await file.sync();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await file.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!record(error) || error.code !== 'EEXIST')
|
|
53
|
+
throw error;
|
|
54
|
+
if (!same((await retained(scope, value.key)).metadata, value.metadata))
|
|
55
|
+
throw invalid();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function checkedReceipt(value, scope, key, metadata) {
|
|
59
|
+
if (!value || value.schema !== 'firedrill.vault-request.v1' || value.actor_id !== scope.owner || value.workspace_id !== scope.workspace
|
|
60
|
+
|| value.request_key !== key || !['accepted', 'not_received'].includes(value.state) || typeof value.requires_value !== 'boolean'
|
|
61
|
+
|| value.state === 'not_received' && (value.entry !== null || value.metadata !== null || value.requires_value)
|
|
62
|
+
|| value.state === 'accepted' && (!value.entry || !value.metadata || metadata && !same(checkedMetadata(value.metadata), metadata)))
|
|
63
|
+
throw invalid();
|
|
64
|
+
if (value.entry)
|
|
65
|
+
identifier(value.entry.id);
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function summary(row) {
|
|
69
|
+
const expired = row.expires_at && Date.parse(row.expires_at) <= Date.now();
|
|
70
|
+
return `${row.name} · v${row.version} · ${row.phase} · ${row.state === 'revoked' ? 'Revoked' : expired ? 'Expired' : row.state === 'active' ? row.secret ? 'Secret saved' : row.empty ? 'Empty configuration' : 'Configuration saved' : 'Save pending'}\n${row.id}${row.expires_at ? '\nExpires: ' + row.expires_at : ''}${row.original_request_key ? '\nContinue: firedrill vault resume ' + identifier(row.original_request_key) : ''}`;
|
|
71
|
+
}
|
|
72
|
+
function show(value, output) {
|
|
73
|
+
output(value, [`Request: ${value.request_key}`, value.entry ? summary(value.entry) : 'Not confirmed. The original request may still arrive.'].map(terminalText).join('\n'));
|
|
74
|
+
}
|
|
75
|
+
async function approve(options, message) {
|
|
76
|
+
if (options.approve)
|
|
77
|
+
return;
|
|
78
|
+
if (!interactive(options) || !await selectOne(message, [{ value: false, label: 'Cancel' }, { value: true, label: 'Confirm change' }], options.signal))
|
|
79
|
+
throw new CliError('approval_required', 'Nothing was submitted. Review this change and pass --approve-change for automation.', 2);
|
|
80
|
+
}
|
|
81
|
+
export async function readPrivateValue(options) {
|
|
82
|
+
if (!options.valueStdin) {
|
|
83
|
+
if (!interactive(options))
|
|
84
|
+
throw new CliError('value_input_required', 'Pipe the exact UTF-8 value with --value-stdin for automation. Do not pass values in command arguments.', 2);
|
|
85
|
+
return secretLine('Value', options.signal);
|
|
86
|
+
}
|
|
87
|
+
if (process.stdin.isTTY)
|
|
88
|
+
throw new CliError('value_input_required', 'Pipe the exact UTF-8 value with --value-stdin, or omit the flag to use hidden input.', 2);
|
|
89
|
+
const chunks = [];
|
|
90
|
+
let count = 0;
|
|
91
|
+
const onAbort = () => process.stdin.destroy(new CliError('canceled', 'Value input canceled. No save was submitted.', 130));
|
|
92
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
93
|
+
try {
|
|
94
|
+
if (options.signal.aborted)
|
|
95
|
+
onAbort();
|
|
96
|
+
for await (const chunk of process.stdin) {
|
|
97
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
98
|
+
count += bytes.length;
|
|
99
|
+
if (count > 65536)
|
|
100
|
+
throw new CliError('value_capacity', 'The value exceeds secure-entry capacity. No save was submitted.', 2);
|
|
101
|
+
chunks.push(bytes);
|
|
102
|
+
}
|
|
103
|
+
const joined = Buffer.concat(chunks);
|
|
104
|
+
try {
|
|
105
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(joined);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw invalid();
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
joined.fill(0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
for (const chunk of chunks)
|
|
116
|
+
chunk.fill(0);
|
|
117
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function expiryChoice(options) {
|
|
121
|
+
if (!await selectOne('Expiry', [{ value: false, label: 'No expiry' }, { value: true, label: 'Set an expiry date' }], options.signal))
|
|
122
|
+
return null;
|
|
123
|
+
const value = await line('Expiry (ISO date with timezone)', 40, options.signal);
|
|
124
|
+
if (!at(value))
|
|
125
|
+
throw invalid();
|
|
126
|
+
return new Date(value).toISOString();
|
|
127
|
+
}
|
|
128
|
+
async function submit(scope, intent, options, output) {
|
|
129
|
+
const base = `/api/workspaces/${scope.workspace}/vault`, access = auth(scope, options);
|
|
130
|
+
let saved = checkedReceipt(await api(`${base}/requests/${intent.key}`, access), scope, intent.key, intent.metadata);
|
|
131
|
+
if (saved.entry && ['active', 'revoked'].includes(saved.entry.state)) {
|
|
132
|
+
show(saved, output);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await approve(options, intent.metadata.replaces_id ? `Replace ${terminalText(intent.metadata.name)} and revoke old grants after storage is confirmed? Review world and CI mappings afterward.` : `Save ${terminalText(intent.metadata.name)} for ${intent.metadata.phase}?`);
|
|
136
|
+
await remember(scope, intent);
|
|
137
|
+
output({ phase: 'request_prepared', request_key: intent.key, workspace_id: scope.workspace }, `Request: ${intent.key}\nRecovery: firedrill vault request ${intent.key} --workspace ${scope.workspace}\nContinue: firedrill vault resume ${intent.key} --workspace ${scope.workspace}`);
|
|
138
|
+
let value;
|
|
139
|
+
try {
|
|
140
|
+
if (saved.state === 'not_received' || saved.requires_value)
|
|
141
|
+
value = await readPrivateValue(options);
|
|
142
|
+
if (intent.metadata.secret && value === '')
|
|
143
|
+
throw new CliError('value_required', 'Enter a nonempty secret, or use a non-secret configuration value.', 2);
|
|
144
|
+
if (saved.state === 'not_received') {
|
|
145
|
+
const { replaces_id, basis_version, ...metadata } = intent.metadata;
|
|
146
|
+
if (value === undefined || replaces_id && basis_version === undefined)
|
|
147
|
+
throw invalid();
|
|
148
|
+
const body = replaces_id
|
|
149
|
+
? { basis_version: basis_version, revoke_previous: true, expires_at: metadata.expires_at, value } : { ...metadata, value };
|
|
150
|
+
await api(replaces_id ? `${base}/${identifier(replaces_id)}/rotate` : base, { ...access, method: 'POST', idempotencyKey: intent.key, body });
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
await api(`${base}/requests/${intent.key}`, { ...access, method: 'POST', body: value === undefined ? {} : { value } });
|
|
154
|
+
saved = checkedReceipt(await api(`${base}/requests/${intent.key}`, access), scope, intent.key, intent.metadata);
|
|
155
|
+
show(saved, output);
|
|
156
|
+
if (!saved.entry || !['active', 'revoked'].includes(saved.entry.state))
|
|
157
|
+
throw new CliError('vault_write_pending', 'The original save is not complete. Use its request ID to inspect or continue it. No second save was created.');
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
value = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function vaultCommand(args, options, output) {
|
|
164
|
+
let action = args[0] ?? (interactive(options) ? '' : 'list');
|
|
165
|
+
if (!action)
|
|
166
|
+
action = await selectOne('Vault', [{ value: 'list', label: 'View saved values' }, { value: 'save', label: 'Save a value' },
|
|
167
|
+
{ value: 'rotate', label: 'Rotate a value' }, { value: 'expiry', label: 'Change expiry' }, { value: 'remove', label: 'Remove a value' }, { value: 'open', label: 'Open Vault in the browser' }], options.signal);
|
|
168
|
+
if (!['list', 'show', 'save', 'rotate', 'expiry', 'remove', 'request', 'resume', 'open'].includes(action) || args.length > (['list', 'save', 'open'].includes(action) ? 1 : 2)
|
|
169
|
+
|| options.file && !['save', 'rotate', 'expiry'].includes(action) || options.request && !['save', 'rotate'].includes(action)
|
|
170
|
+
|| options.valueStdin && !['save', 'rotate', 'resume'].includes(action) || options.approve && !['save', 'rotate', 'expiry', 'remove', 'resume'].includes(action)
|
|
171
|
+
|| (options.page || options.search || options.status) && action !== 'list')
|
|
172
|
+
throw invalid();
|
|
173
|
+
const scope = await selectedWorkspace({ ...options, interactive: interactive(options) }), access = auth(scope, options), base = `/api/workspaces/${scope.workspace}/vault`;
|
|
174
|
+
if (action === 'open') {
|
|
175
|
+
const url = new URL('/vault', APP_ORIGIN);
|
|
176
|
+
url.searchParams.set('workspace', scope.workspace);
|
|
177
|
+
output({ url: url.href, workspace_id: scope.workspace }, url.href);
|
|
178
|
+
if (options.openBrowser && interactive(options))
|
|
179
|
+
await openBrowser(url.href);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (action === 'list') {
|
|
183
|
+
if (options.status && !['current', 'all', 'revoked', 'pending', 'expired'].includes(options.status))
|
|
184
|
+
throw invalid();
|
|
185
|
+
const rows = await api(`${base}?${query(options)}&status=${options.status ?? 'current'}`, access);
|
|
186
|
+
output(rows, [...rows.results.map(summary), ...(!rows.count ? ['No matching values.'] : []), `Page ${rows.page}; ${rows.count.toLocaleString()} values.`,
|
|
187
|
+
...(rows.next ? ['Use --page ' + (rows.page + 1) + ' with the same filters to continue.'] : [])].map(terminalText).join('\n\n'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action === 'request' || action === 'resume') {
|
|
191
|
+
const key = identifier(args[1] ?? ''), value = checkedReceipt(await api(`${base}/requests/${key}`, access), scope, key);
|
|
192
|
+
if (action === 'request') {
|
|
193
|
+
show(value, output);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const intent = value.metadata ? { schema: 'firedrill.local-vault-intent.v1', actor: scope.owner, workspace: scope.workspace, key, metadata: checkedMetadata(value.metadata) } : await retained(scope, key);
|
|
197
|
+
await submit(scope, intent, options, output);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let entry = null;
|
|
201
|
+
if (!['save'].includes(action)) {
|
|
202
|
+
const id = args[1] ?? (interactive(options) ? (await choosePaged('Value', base, scope, options, row => ({ label: row.name, detail: `v${row.version} · ${row.phase} · ${row.state}` })))?.id : undefined);
|
|
203
|
+
if (!id)
|
|
204
|
+
throw new CliError('vault_entry_required', 'Choose an entry with firedrill vault list.', 2);
|
|
205
|
+
entry = await api(`${base}/${identifier(id)}`, access);
|
|
206
|
+
if (!entry || entry.id !== id)
|
|
207
|
+
throw invalid();
|
|
208
|
+
if (action === 'show') {
|
|
209
|
+
output(entry, terminalText(summary(entry)));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (action === 'remove') {
|
|
213
|
+
await approve(options, `Remove ${terminalText(entry.name)}? Existing grants will stop resolving. This does not revoke the original provider credential.`);
|
|
214
|
+
const value = await api(`${base}/${entry.id}`, { ...access, method: 'DELETE' });
|
|
215
|
+
if (!value || value.state !== 'revoked')
|
|
216
|
+
throw invalid();
|
|
217
|
+
output(value, 'Value removed.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (action === 'expiry') {
|
|
222
|
+
const body = options.file ? await readMetadata(options.file) : interactive(options) ? { basis_revision: entry.metadata_revision, expires_at: await expiryChoice(options) } : null;
|
|
223
|
+
if (!body || !fields(body, ['basis_revision', 'expires_at']) || !Number.isSafeInteger(body.basis_revision) || Number(body.basis_revision) < 1 || !at(body.expires_at))
|
|
224
|
+
throw invalid();
|
|
225
|
+
await approve(options, `Change the expiry for ${terminalText(entry.name)}?`);
|
|
226
|
+
const expiresAt = body.expires_at;
|
|
227
|
+
const change = { basis_revision: Number(body.basis_revision), expires_at: expiresAt };
|
|
228
|
+
const value = await api(`${base}/${entry.id}`, { ...access, method: 'PATCH', body: change });
|
|
229
|
+
if (!value || value.id !== entry.id || value.version !== entry.version || value.state !== 'active'
|
|
230
|
+
|| !Number.isSafeInteger(value.metadata_revision) || value.metadata_revision < entry.metadata_revision
|
|
231
|
+
|| (value.expires_at === null) !== (expiresAt === null)
|
|
232
|
+
|| expiresAt !== null && Date.parse(value.expires_at) !== Date.parse(expiresAt))
|
|
233
|
+
throw invalid();
|
|
234
|
+
output(value, terminalText(summary(value)));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let metadata;
|
|
238
|
+
if (options.file) {
|
|
239
|
+
const body = await readMetadata(options.file);
|
|
240
|
+
if (action === 'rotate') {
|
|
241
|
+
if (!fields(body, ['expires_at']) || !at(body.expires_at))
|
|
242
|
+
throw invalid();
|
|
243
|
+
metadata = { name: entry.name, secret: entry.secret, phase: entry.phase, purpose: entry.purpose,
|
|
244
|
+
expires_at: body.expires_at, replaces_id: entry.id, basis_version: entry.version };
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
metadata = checkedMetadata(body);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
if (!interactive(options))
|
|
251
|
+
throw new CliError('vault_metadata_required', 'Pass --file with metadata only. Use hidden input or --value-stdin for the value. Run interactively for guided setup.', 2);
|
|
252
|
+
metadata = action === 'rotate' ? { name: entry.name, secret: entry.secret, phase: entry.phase, purpose: entry.purpose,
|
|
253
|
+
expires_at: await expiryChoice(options), replaces_id: entry.id, basis_version: entry.version } : {
|
|
254
|
+
name: await line('Name', 128, options.signal), secret: await selectOne('Value type', [{ value: true, label: 'Secret' }, { value: false, label: 'Configuration' }], options.signal),
|
|
255
|
+
phase: await selectOne('Available during', [{ value: 'runtime', label: 'Runtime' }, { value: 'build', label: 'Build time' }, { value: 'both', label: 'Build and runtime' }], options.signal),
|
|
256
|
+
purpose: await line('Purpose (optional)', 255, options.signal), expires_at: await expiryChoice(options)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
checkedMetadata(metadata);
|
|
260
|
+
// Normalize request metadata once, before persistence or provider submission.
|
|
261
|
+
metadata.expires_at = metadata.expires_at ? new Date(metadata.expires_at).toISOString() : null;
|
|
262
|
+
if (action === 'save' && metadata.replaces_id)
|
|
263
|
+
throw invalid();
|
|
264
|
+
await submit(scope, { schema: 'firedrill.local-vault-intent.v1', actor: scope.owner, workspace: scope.workspace,
|
|
265
|
+
key: options.request ? identifier(options.request) : randomUUID(), metadata }, options, output);
|
|
266
|
+
}
|