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,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creating a workspace from the terminal (`firedrill workspace create`,
|
|
3
|
+
* `requests`, `request`, `withdraw`).
|
|
4
|
+
*
|
|
5
|
+
* Root (`accounts/native_workspace_requests.py`), all under the terminal's
|
|
6
|
+
* native session:
|
|
7
|
+
* - `GET /api/auth/native/workspace-policy` gives the terms and privacy
|
|
8
|
+
* documents (version and URL) the human must read and accept. 503 until they
|
|
9
|
+
* are configured.
|
|
10
|
+
* - `POST /api/auth/native/workspace-requests` with an original UUID
|
|
11
|
+
* Idempotency-Key and exactly `{name, workspace_consent, terms_version,
|
|
12
|
+
* terms_accepted, privacy_version, privacy_accepted}`. Replaying the same key
|
|
13
|
+
* with the same name and the originally approved versions continues the same
|
|
14
|
+
* request; it never creates a second organization. Every native request row
|
|
15
|
+
* carries the versions the human approved (`terms_version`,
|
|
16
|
+
* `privacy_version`), and a replay uses exactly those, never today's
|
|
17
|
+
* documents.
|
|
18
|
+
* - `GET .../workspace-requests` (paged) and `.../{key}` recover the original
|
|
19
|
+
* (`not_received` when absent). `DELETE .../{key}` with `{withdraw_consent:
|
|
20
|
+
* true}` withdraws it only while it is provably unsent.
|
|
21
|
+
*
|
|
22
|
+
* Consent is the human's: never inferred from sign-in or a task. JSON mode needs
|
|
23
|
+
* --approve-change plus the exact document versions the human accepted.
|
|
24
|
+
*
|
|
25
|
+
* A finished request in a new organization says `reauthorization_required`:
|
|
26
|
+
* this terminal's grant is for another organization, so the human signs in to
|
|
27
|
+
* the new one with a new device authorization. Nothing here changes the
|
|
28
|
+
* current grant's organization or selected workspace on its own.
|
|
29
|
+
*/
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
import { createInterface } from 'node:readline/promises';
|
|
32
|
+
import { api, login } from './auth.js';
|
|
33
|
+
import { APP_ORIGIN, CliError } from './http.js';
|
|
34
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
35
|
+
import { isTransient } from './native/control-plane.js';
|
|
36
|
+
const POLICY = '/api/auth/native/workspace-policy';
|
|
37
|
+
const REQUESTS = '/api/auth/native/workspace-requests';
|
|
38
|
+
const PAGE_SIZE = 25;
|
|
39
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
40
|
+
const ORGANIZATION = /^org_[A-Za-z0-9]{1,120}$/;
|
|
41
|
+
const STATES = ['not_received', 'pending', 'creating', 'uncertain', 'completed', 'withdrawn'];
|
|
42
|
+
/** Sent or being sent: only replaying the same request moves it on. */
|
|
43
|
+
const UNFINISHED = new Set(['pending', 'creating', 'uncertain']);
|
|
44
|
+
/** Root's answers that mean "the same request is still being confirmed; send it again shortly". */
|
|
45
|
+
const STILL_CREATING = new Set(['workspace_creation_pending', 'membership_pending']);
|
|
46
|
+
/** A step the human must take before anything is sent: its own JSON line, or text on stderr. */
|
|
47
|
+
function notice(json, value, text) {
|
|
48
|
+
if (json)
|
|
49
|
+
process.stdout.write(JSON.stringify(value) + '\n');
|
|
50
|
+
else
|
|
51
|
+
process.stderr.write(text + '\n');
|
|
52
|
+
}
|
|
53
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
54
|
+
const lower = (value) => value.toLowerCase();
|
|
55
|
+
/** Root's version check: 1-80 characters, no spaces or controls. */
|
|
56
|
+
const versionText = (value) => typeof value === 'string' && value.length >= 1 && value.length <= 80
|
|
57
|
+
&& ![...value].some((c) => c.charCodeAt(0) < 33);
|
|
58
|
+
function documentMatches(value) {
|
|
59
|
+
if (!object(value) || Object.keys(value).length !== 2 || !versionText(value.version) || typeof value.url !== 'string')
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
const url = new URL(value.url);
|
|
63
|
+
return url.protocol === 'https:' && !!url.hostname && !url.username && !url.password && !url.hash;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function documentsMatch(value) {
|
|
70
|
+
return object(value) && Object.keys(value).length === 2 && documentMatches(value.terms) && documentMatches(value.privacy);
|
|
71
|
+
}
|
|
72
|
+
/** One workspace request as root returns it, for this person and (when given) this exact key. */
|
|
73
|
+
export function workspaceRequestMatches(value, owner, key) {
|
|
74
|
+
if (!object(value))
|
|
75
|
+
return false;
|
|
76
|
+
const v = value;
|
|
77
|
+
if (v.schema !== 'firedrill.workspace-request.v1' || typeof v.actor_id !== 'string' || lower(v.actor_id) !== owner
|
|
78
|
+
|| typeof v.request_key !== 'string' || !UUID.test(v.request_key) || (key !== undefined && v.request_key !== key)
|
|
79
|
+
|| !STATES.includes(v.state) || typeof v.workspace_available !== 'boolean'
|
|
80
|
+
|| typeof v.reauthorization_required !== 'boolean')
|
|
81
|
+
return false;
|
|
82
|
+
if (v.workspace_id !== null && (typeof v.workspace_id !== 'string' || !UUID.test(lower(v.workspace_id))))
|
|
83
|
+
return false;
|
|
84
|
+
if (v.workos_organization_id !== null && (typeof v.workos_organization_id !== 'string' || !ORGANIZATION.test(v.workos_organization_id)))
|
|
85
|
+
return false;
|
|
86
|
+
// Root shows a workspace only while this person can still open it, and its organization only then.
|
|
87
|
+
if (v.workspace_available !== (v.workspace_id !== null))
|
|
88
|
+
return false;
|
|
89
|
+
if (v.workspace_available && v.state !== 'completed')
|
|
90
|
+
return false;
|
|
91
|
+
if (v.workos_organization_id !== null && !v.workspace_available)
|
|
92
|
+
return false;
|
|
93
|
+
if (v.reauthorization_required && v.workos_organization_id === null)
|
|
94
|
+
return false;
|
|
95
|
+
if (v.state === 'not_received')
|
|
96
|
+
return v.name === null && v.created_at === null && !v.workspace_available
|
|
97
|
+
&& v.terms_version === null && v.privacy_version === null;
|
|
98
|
+
return typeof v.name === 'string' && v.name.length >= 1 && v.name.length <= 120 && typeof v.created_at === 'string'
|
|
99
|
+
&& versionText(v.terms_version) && versionText(v.privacy_version);
|
|
100
|
+
}
|
|
101
|
+
function pageMatches(value, owner, page) {
|
|
102
|
+
if (!object(value) || !Array.isArray(value.results) || value.page !== page || value.page_size !== PAGE_SIZE
|
|
103
|
+
|| !Number.isSafeInteger(value.count) || value.count < 0 || value.results.length > PAGE_SIZE
|
|
104
|
+
|| (value.next !== null && typeof value.next !== 'string') || (value.previous !== null && typeof value.previous !== 'string'))
|
|
105
|
+
return false;
|
|
106
|
+
const keys = new Set();
|
|
107
|
+
for (const row of value.results) {
|
|
108
|
+
if (!workspaceRequestMatches(row, owner) || row.state === 'not_received' || keys.has(row.request_key))
|
|
109
|
+
return false;
|
|
110
|
+
keys.add(row.request_key);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const unconfirmed = () => new CliError('native_response_unconfirmed', 'The workspace request answer could not be confirmed. Nothing else was sent.', 2);
|
|
115
|
+
/** The original request key: a lowercase UUID, exactly as root keys it. */
|
|
116
|
+
export function workspaceRequestKey(value) {
|
|
117
|
+
if (!UUID.test(value))
|
|
118
|
+
throw new CliError('native_workspace_request_invalid', 'Use the original workspace request key (a lowercase UUID).', 2);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
/** Root trims the name and refuses controls; the same rule here sends nothing root would refuse. */
|
|
122
|
+
export function workspaceName(value) {
|
|
123
|
+
const name = value.trim();
|
|
124
|
+
if (name.length < 1 || name.length > 120 || /[\u0000-\u001f\u007f]/.test(value))
|
|
125
|
+
throw new CliError('workspace_name_invalid', 'Use a workspace name of 1 to 120 characters without control characters. Nothing was sent.', 2);
|
|
126
|
+
return name;
|
|
127
|
+
}
|
|
128
|
+
const quoted = (value) => /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
129
|
+
/** Continues the original: root's retained versions are used once it has the request; until then it is shown for approval again. */
|
|
130
|
+
function recoveryCommand(name, key) {
|
|
131
|
+
return `firedrill workspace create ${quoted(name)} --request-key ${key}`;
|
|
132
|
+
}
|
|
133
|
+
/** The command a human approves for a new request: the name, the key and the exact document versions they accepted. */
|
|
134
|
+
function approvedCommand(name, key, documents) {
|
|
135
|
+
return `${recoveryCommand(name, key)} --terms-version ${quoted(documents.terms)} --privacy-version ${quoted(documents.privacy)} --approve-change --json`;
|
|
136
|
+
}
|
|
137
|
+
export async function workspaceDocuments(owner, signal) {
|
|
138
|
+
let value;
|
|
139
|
+
try {
|
|
140
|
+
value = await api(POLICY, { actor: owner, signal });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof CliError && error.code === 'native_workspace_terms_unavailable')
|
|
144
|
+
throw new CliError(error.code, `Creating a workspace from the terminal is not available yet. Open ${new URL('/onboarding', APP_ORIGIN).href} to create one in the browser. Nothing was sent.`, 2, error.httpStatus);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
if (!documentsMatch(value))
|
|
148
|
+
throw unconfirmed();
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
export async function recoverWorkspaceRequest(owner, key, signal) {
|
|
152
|
+
const value = await api(`${REQUESTS}/${key}`, { actor: owner, signal });
|
|
153
|
+
if (!workspaceRequestMatches(value, owner, key))
|
|
154
|
+
throw unconfirmed();
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
async function readLine(question, signal) {
|
|
158
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr, terminal: false });
|
|
159
|
+
try {
|
|
160
|
+
return await prompt.question(question, { signal });
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
prompt.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function requestText(row) {
|
|
167
|
+
const name = terminalText(row.name ?? '');
|
|
168
|
+
switch (row.state) {
|
|
169
|
+
case 'completed':
|
|
170
|
+
if (!row.workspace_available)
|
|
171
|
+
return `Workspace ${name} was created, but this account can no longer open it.`;
|
|
172
|
+
return row.reauthorization_required
|
|
173
|
+
? `Workspace ${name} is ready (${row.workspace_id}) in a new organization. This terminal is signed in to a different one.`
|
|
174
|
+
: `Workspace ${name} is ready (${row.workspace_id}).`;
|
|
175
|
+
case 'withdrawn': return `The request for ${name} was withdrawn before it was sent. No workspace was created.`;
|
|
176
|
+
case 'not_received': return 'Firedrill has no workspace request with this key.';
|
|
177
|
+
default: return `Workspace ${name} is still being created.`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** What to run next for a request, in order: empty when nothing is left to do. */
|
|
181
|
+
function nextCommands(row) {
|
|
182
|
+
if (row.state !== 'completed' || !row.workspace_available)
|
|
183
|
+
return [];
|
|
184
|
+
return [...(row.reauthorization_required ? [`firedrill login --organization ${row.workos_organization_id}`] : []),
|
|
185
|
+
`firedrill workspace use ${row.workspace_id}`];
|
|
186
|
+
}
|
|
187
|
+
const nextText = (row) => nextCommands(row).length ? `Next: ${nextCommands(row).join(', then ')}` : '';
|
|
188
|
+
/** Select a ready workspace for this terminal, signing in to its organization first when the human chooses to. */
|
|
189
|
+
async function useWorkspace(row, options) {
|
|
190
|
+
if (row.state !== 'completed' || !row.workspace_available || !row.workspace_id)
|
|
191
|
+
return null;
|
|
192
|
+
const use = await selectOne(row.reauthorization_required ? 'Sign in to its organization' : 'Use this workspace', [
|
|
193
|
+
{ value: true, label: row.reauthorization_required ? 'Sign in to it now' : 'Use it now',
|
|
194
|
+
detail: row.reauthorization_required ? 'A new browser sign-in for the new organization.' : '' },
|
|
195
|
+
{ value: false, label: 'Later', detail: nextCommands(row).join(', then ') },
|
|
196
|
+
], options.signal);
|
|
197
|
+
if (!use)
|
|
198
|
+
return null;
|
|
199
|
+
if (row.reauthorization_required) {
|
|
200
|
+
const session = await login({ json: false, open: options.openBrowser, organization: row.workos_organization_id, signal: options.signal });
|
|
201
|
+
if (session.user.id.toLowerCase() !== options.owner)
|
|
202
|
+
throw new CliError('native_account_changed', 'A different account signed in. The workspace was not selected.', 2);
|
|
203
|
+
}
|
|
204
|
+
return api('/api/auth/native/workspaces', { method: 'POST', body: { workspace_id: row.workspace_id }, actor: options.owner, signal: options.signal });
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Create a workspace, or continue the original request. Returns the request as
|
|
208
|
+
* root last answered it (never a guess).
|
|
209
|
+
*/
|
|
210
|
+
export async function createWorkspace(nameInput, options) {
|
|
211
|
+
const interactive = !options.json && process.stdin.isTTY === true;
|
|
212
|
+
const key = workspaceRequestKey(options.request ?? randomUUID());
|
|
213
|
+
let name = nameInput === undefined ? undefined : workspaceName(nameInput);
|
|
214
|
+
let versions = null;
|
|
215
|
+
if (options.termsVersion !== undefined || options.privacyVersion !== undefined) {
|
|
216
|
+
if (!versionText(options.termsVersion) || !versionText(options.privacyVersion))
|
|
217
|
+
throw new CliError('arguments_invalid', 'Pass both --terms-version and --privacy-version, as shown with the documents. Nothing was sent.', 2);
|
|
218
|
+
versions = { terms: options.termsVersion, privacy: options.privacyVersion };
|
|
219
|
+
}
|
|
220
|
+
// The original first: a finished, withdrawn or unfinished request is never replaced by a new one.
|
|
221
|
+
let replay = false;
|
|
222
|
+
if (options.request) {
|
|
223
|
+
const original = await recoverWorkspaceRequest(options.owner, key, options.signal);
|
|
224
|
+
if (original.state === 'completed')
|
|
225
|
+
return original;
|
|
226
|
+
if (original.state === 'withdrawn')
|
|
227
|
+
throw new CliError('workspace_request_withdrawn', 'This request was withdrawn before it was sent. Run firedrill workspace create without --request-key to start a new one.', 2);
|
|
228
|
+
if (UNFINISHED.has(original.state)) {
|
|
229
|
+
if (name !== undefined && name !== original.name)
|
|
230
|
+
throw new CliError('idempotency_conflict', `Request ${key} is for ${terminalText(original.name)}. Continue it with that name, or start a new request without --request-key.`, 2);
|
|
231
|
+
const retained = { terms: original.terms_version, privacy: original.privacy_version };
|
|
232
|
+
if (versions && (versions.terms !== retained.terms || versions.privacy !== retained.privacy))
|
|
233
|
+
throw new CliError('idempotency_conflict', `Request ${key} was approved with terms ${terminalText(retained.terms)} and privacy ${terminalText(retained.privacy)}. It continues only with those. Nothing was sent.`, 2);
|
|
234
|
+
// The original approval, exactly as root retained it: never today's documents, never a new assent.
|
|
235
|
+
name = original.name;
|
|
236
|
+
versions = retained;
|
|
237
|
+
replay = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (name === undefined) {
|
|
241
|
+
if (!interactive)
|
|
242
|
+
throw new CliError('workspace_name_required', 'Pass the workspace name: firedrill workspace create <name>. Nothing was sent.', 2);
|
|
243
|
+
name = workspaceName(await readLine('Workspace name: ', options.signal));
|
|
244
|
+
}
|
|
245
|
+
if (!replay) {
|
|
246
|
+
// A new request: the human reads and accepts the current documents.
|
|
247
|
+
const documents = await workspaceDocuments(options.owner, options.signal);
|
|
248
|
+
const current = { terms: documents.terms.version, privacy: documents.privacy.version };
|
|
249
|
+
if (versions && (versions.terms !== current.terms || versions.privacy !== current.privacy))
|
|
250
|
+
throw new CliError('native_workspace_terms_changed', `The documents changed since they were shown (terms ${terminalText(current.terms)}, privacy ${terminalText(current.privacy)}). Show the human the current ones before asking again. Nothing was sent.`, 2);
|
|
251
|
+
const approval = { type: 'workspace_approval_required', name, request_key: key, terms: documents.terms, privacy: documents.privacy,
|
|
252
|
+
command: approvedCommand(name, key, current) };
|
|
253
|
+
if (interactive) {
|
|
254
|
+
process.stderr.write(`Workspace: ${terminalText(name)}\nCreating it makes you its owner in a new organization and accepts:\n`
|
|
255
|
+
+ ` Terms of service (${terminalText(current.terms)}): ${terminalText(documents.terms.url)}\n`
|
|
256
|
+
+ ` Privacy policy (${terminalText(current.privacy)}): ${terminalText(documents.privacy.url)}\n`);
|
|
257
|
+
const accepted = await selectOne('Create this workspace?', [
|
|
258
|
+
{ value: true, label: 'Create it and accept both documents' }, { value: false, label: 'Cancel' }
|
|
259
|
+
], options.signal);
|
|
260
|
+
if (!accepted)
|
|
261
|
+
throw new CliError('canceled', 'No workspace was requested.', 130);
|
|
262
|
+
}
|
|
263
|
+
else if (!options.approve || !versions) {
|
|
264
|
+
notice(options.json, approval, `Workspace: ${terminalText(name)}\n Terms of service (${terminalText(current.terms)}): ${terminalText(documents.terms.url)}\n`
|
|
265
|
+
+ ` Privacy policy (${terminalText(current.privacy)}): ${terminalText(documents.privacy.url)}\nAfter the human approves: ${approval.command}`);
|
|
266
|
+
throw new CliError('workspace_approval_required', 'Creating a workspace needs the human to approve its name and accept both documents. Show them the name and both document links, and run the printed command only after they approve. Nothing was sent.', 2);
|
|
267
|
+
}
|
|
268
|
+
versions = current;
|
|
269
|
+
}
|
|
270
|
+
if (!versions)
|
|
271
|
+
throw unconfirmed();
|
|
272
|
+
const recovery = recoveryCommand(name, key);
|
|
273
|
+
notice(options.json, { type: 'request_prepared', request_key: key, recovery }, `Recovery: ${recovery}`);
|
|
274
|
+
const body = { name, workspace_consent: true, terms_version: versions.terms, terms_accepted: true,
|
|
275
|
+
privacy_version: versions.privacy, privacy_accepted: true };
|
|
276
|
+
try {
|
|
277
|
+
const value = await api(REQUESTS, { method: 'POST', idempotencyKey: key, body, actor: options.owner, signal: options.signal });
|
|
278
|
+
if (!workspaceRequestMatches(value, options.owner, key) || value.name !== name
|
|
279
|
+
|| value.terms_version !== versions.terms || value.privacy_version !== versions.privacy)
|
|
280
|
+
throw unconfirmed();
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (!(error instanceof CliError) || options.signal.aborted)
|
|
285
|
+
throw error;
|
|
286
|
+
if (error.code === 'native_workspace_terms_changed')
|
|
287
|
+
throw new CliError(error.code, 'The documents changed while they were being reviewed. Run firedrill workspace create again to show the current ones. Nothing was created.', 2, error.httpStatus);
|
|
288
|
+
const answered = error.httpStatus !== null && !isTransient(error);
|
|
289
|
+
if (answered && !STILL_CREATING.has(error.code))
|
|
290
|
+
throw error;
|
|
291
|
+
// Still being confirmed, or no answer: read the original. Never sent again here.
|
|
292
|
+
const original = await recoverWorkspaceRequest(options.owner, key, options.signal).catch(() => null);
|
|
293
|
+
if (original && original.state !== 'not_received')
|
|
294
|
+
return original;
|
|
295
|
+
throw new CliError('workspace_request_unconfirmed', `The workspace request was not confirmed. Run the same request again: ${recovery}`, 2);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function listRequests(options) {
|
|
299
|
+
const page = Number(options.page ?? '1');
|
|
300
|
+
const value = await api(`${REQUESTS}?page=${page}&page_size=${PAGE_SIZE}`, { actor: options.owner, signal: options.signal });
|
|
301
|
+
if (!pageMatches(value, options.owner, page))
|
|
302
|
+
throw unconfirmed();
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
async function withdraw(key, options) {
|
|
306
|
+
const original = await recoverWorkspaceRequest(options.owner, key, options.signal);
|
|
307
|
+
if (original.state === 'not_received')
|
|
308
|
+
throw new CliError('workspace_request_not_found', 'Firedrill has no workspace request with this key. Nothing was withdrawn.', 2);
|
|
309
|
+
if (original.state !== 'pending')
|
|
310
|
+
return original;
|
|
311
|
+
if (!options.json && process.stdin.isTTY) {
|
|
312
|
+
const yes = await selectOne(`Withdraw the request for ${terminalText(original.name)}?`, [
|
|
313
|
+
{ value: true, label: 'Withdraw it', detail: 'Only possible while it has not been sent.' }, { value: false, label: 'Keep it' }
|
|
314
|
+
], options.signal);
|
|
315
|
+
if (!yes)
|
|
316
|
+
throw new CliError('canceled', 'The request was kept.', 130);
|
|
317
|
+
}
|
|
318
|
+
else if (!options.approve) {
|
|
319
|
+
notice(true, { type: 'workspace_withdrawal_approval_required', request_key: key, name: original.name,
|
|
320
|
+
command: `firedrill workspace withdraw ${key} --approve-change --json` }, '');
|
|
321
|
+
throw new CliError('workspace_approval_required', 'Withdrawing needs the human\'s approval (--approve-change). Nothing was withdrawn.', 2);
|
|
322
|
+
}
|
|
323
|
+
const value = await api(`${REQUESTS}/${key}`, { method: 'DELETE', body: { withdraw_consent: true }, actor: options.owner, signal: options.signal });
|
|
324
|
+
if (!workspaceRequestMatches(value, options.owner, key))
|
|
325
|
+
throw unconfirmed();
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
/** `firedrill workspace create|requests|request|withdraw`. */
|
|
329
|
+
export async function workspaceRegistrationCommand(args, options, output) {
|
|
330
|
+
const [action, target] = args;
|
|
331
|
+
if (action === 'create' && args.length <= 2) {
|
|
332
|
+
const row = await createWorkspace(target, options);
|
|
333
|
+
if (!options.json && process.stdin.isTTY && row.state === 'completed' && row.workspace_available) {
|
|
334
|
+
process.stdout.write(requestText(row) + '\n');
|
|
335
|
+
const session = await useWorkspace(row, options);
|
|
336
|
+
if (session)
|
|
337
|
+
output(session, `Workspace: ${terminalText(session.workspace?.name ?? 'Not selected')}`);
|
|
338
|
+
else
|
|
339
|
+
process.stdout.write(nextText(row) + '\n');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
output({ ...row, next_commands: nextCommands(row) }, [requestText(row),
|
|
343
|
+
UNFINISHED.has(row.state) ? `Check it again with: firedrill workspace request ${row.request_key}` : nextText(row)].filter(Boolean).join('\n'));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (action === 'requests' && args.length === 1) {
|
|
347
|
+
if (options.page !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.page))
|
|
348
|
+
throw new CliError('query_invalid', 'Use a valid page number.', 2);
|
|
349
|
+
const page = await listRequests(options);
|
|
350
|
+
output(page, [page.results.map((row) => `${row.request_key} ${row.state} ${terminalText(row.name ?? '')}${row.workspace_id ? ' ' + row.workspace_id : ''}`).join('\n')
|
|
351
|
+
|| 'No workspace requests from this terminal.', page.next ? `More: firedrill workspace requests --page ${page.page + 1}` : ''].filter(Boolean).join('\n'));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (action === 'request' && args.length === 2) {
|
|
355
|
+
const row = await recoverWorkspaceRequest(options.owner, workspaceRequestKey(target), options.signal);
|
|
356
|
+
output({ ...row, next_commands: nextCommands(row) }, [requestText(row), UNFINISHED.has(row.state)
|
|
357
|
+
? `If it stays like this, continue the same request: firedrill workspace create --request-key ${row.request_key} (it keeps the documents approved for it).`
|
|
358
|
+
: nextText(row)].filter(Boolean).join('\n'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (action === 'withdraw' && args.length === 2) {
|
|
362
|
+
const row = await withdraw(workspaceRequestKey(target), options);
|
|
363
|
+
output(row, row.state === 'withdrawn' ? `Withdrawn. No workspace was created for ${terminalText(row.name ?? '')}.`
|
|
364
|
+
: `It was already sent, so it cannot be withdrawn. ${requestText(row)}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
throw new CliError('arguments_invalid', 'Use workspace create [name], workspace requests, workspace request <key> or workspace withdraw <key>.', 2);
|
|
368
|
+
}
|
|
369
|
+
/** The terminal chooser's "create a workspace here": the new workspace's ID once this grant can use it, otherwise null. */
|
|
370
|
+
export async function createWorkspaceHere(owner, signal, openBrowser) {
|
|
371
|
+
const options = { json: false, owner, openBrowser, signal };
|
|
372
|
+
const row = await createWorkspace(undefined, options);
|
|
373
|
+
process.stderr.write(requestText(row) + '\n');
|
|
374
|
+
if (row.state !== 'completed' || !row.workspace_available || !row.workspace_id) {
|
|
375
|
+
if (UNFINISHED.has(row.state))
|
|
376
|
+
process.stderr.write(`Check it again with: firedrill workspace request ${row.request_key}\n`);
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (!row.reauthorization_required)
|
|
380
|
+
return row.workspace_id;
|
|
381
|
+
const session = await useWorkspace(row, options);
|
|
382
|
+
return session ? row.workspace_id : null;
|
|
383
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/workspace-team-contracts.js';
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { api, login, openBrowser } from './auth.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { APP_ORIGIN, CliError } from './http.js';
|
|
4
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
5
|
+
import { identifier } from './world-contracts.js';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { createWorkspaceHere } from './workspace-registration.js';
|
|
8
|
+
export async function selectedWorkspace(options) {
|
|
9
|
+
let session;
|
|
10
|
+
try {
|
|
11
|
+
session = await api('/api/auth/native/session', { signal: options.signal });
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!(error instanceof CliError) || error.code !== 'authentication_required' || !options.interactive)
|
|
15
|
+
throw error;
|
|
16
|
+
const choice = await selectOne('Authorize Firedrill access', [{ label: 'Sign in using the browser', value: true },
|
|
17
|
+
{ label: 'Cancel', value: false }], options.signal);
|
|
18
|
+
if (!choice)
|
|
19
|
+
throw new CliError('canceled', 'No sign-in was started.', 130);
|
|
20
|
+
session = await login({ json: false, open: options.openBrowser, signal: options.signal });
|
|
21
|
+
}
|
|
22
|
+
const owner = identifier(session.user.id);
|
|
23
|
+
if (options.expectedOwner && owner !== options.expectedOwner)
|
|
24
|
+
throw new CliError('native_account_changed', 'The terminal account changed. Reopen this project before selecting its workspace.', 2);
|
|
25
|
+
let workspace = options.workspace ? identifier(options.workspace) : session.workspace?.id;
|
|
26
|
+
if (!workspace && options.interactive) {
|
|
27
|
+
workspace = await chooseWorkspace(owner, options.signal, options.openBrowser);
|
|
28
|
+
}
|
|
29
|
+
if (!workspace && session.membership_sync.state === 'running')
|
|
30
|
+
throw new CliError('workspace_discovery_pending', 'Workspace access is still being checked. Use firedrill workspace list to see its progress, then select an available workspace.', 2);
|
|
31
|
+
if (!workspace)
|
|
32
|
+
throw new CliError('workspace_setup_required', `Create one with firedrill workspace create <name> after the human approves it and accepts the terms, or open ${new URL('/onboarding', APP_ORIGIN).href} to create or choose one in the browser. Then run firedrill workspace. No agent was started.`, 2);
|
|
33
|
+
workspace = identifier(workspace);
|
|
34
|
+
// The user's explicit choice establishes the same selected scope used by
|
|
35
|
+
// stream revocation checks. The actor pin prevents another process's login
|
|
36
|
+
// from inheriting this conversation's remembered authority.
|
|
37
|
+
if (workspace !== session.workspace?.id)
|
|
38
|
+
await api('/api/auth/native/workspaces', {
|
|
39
|
+
method: 'POST', body: { workspace_id: workspace }, actor: owner, signal: options.signal
|
|
40
|
+
});
|
|
41
|
+
return { owner, workspace };
|
|
42
|
+
}
|
|
43
|
+
export async function selectedWorld(options) {
|
|
44
|
+
const { owner, workspace } = await selectedWorkspace(options);
|
|
45
|
+
const prefix = `/api/workspaces/${workspace}/worlds`;
|
|
46
|
+
let world = options.world ? identifier(options.world) : undefined;
|
|
47
|
+
if (!world) {
|
|
48
|
+
const { savedProject } = await import('./source-connection.js');
|
|
49
|
+
world = (await savedProject({ owner, workspace }, process.cwd()))?.world;
|
|
50
|
+
}
|
|
51
|
+
if (!world && options.interactive) {
|
|
52
|
+
const action = await selectOne('Project', [{ value: 'connect', label: 'Connect this local project', detail: 'Review a private source capture and choose whether to upload it.' },
|
|
53
|
+
{ value: 'connect-many', label: 'Connect multiple local folders', detail: 'Choose the agent components explicitly. One agent and one world are created.' },
|
|
54
|
+
{ value: 'repository', label: 'Connect a GitHub repository', detail: 'Choose repository access in the browser.' },
|
|
55
|
+
{ value: 'upload', label: 'Upload code', detail: 'Choose a source archive in the browser.' },
|
|
56
|
+
{ value: 'new-world', label: 'Create a world for a saved agent' },
|
|
57
|
+
{ value: 'saved', label: 'Open a saved world' }], options.signal);
|
|
58
|
+
if (action === 'repository' || action === 'upload') {
|
|
59
|
+
const url = new URL('/new', APP_ORIGIN);
|
|
60
|
+
url.searchParams.set('workspace', workspace);
|
|
61
|
+
url.searchParams.set('source', action);
|
|
62
|
+
const opened = options.openBrowser ? await openBrowser(url.href) : false;
|
|
63
|
+
process.stdout.write(`${opened ? 'Opened' : 'Open'} agent connection in the browser. Finish the repository connection or upload, then continue here. No source was transferred by this terminal command.\n${url.href}\n`);
|
|
64
|
+
const next = await selectOne('After source connection', [
|
|
65
|
+
{ value: 'agent', label: 'Continue with the connected agent', detail: 'Choose its saved source and create a world here.' },
|
|
66
|
+
{ value: 'world', label: 'Open a world already created in the browser' },
|
|
67
|
+
{ value: 'cancel', label: 'Return later' },
|
|
68
|
+
], options.signal);
|
|
69
|
+
if (next === 'cancel')
|
|
70
|
+
throw new CliError('canceled', 'Browser source setup stays saved. Return with firedrill when ready.', 130);
|
|
71
|
+
if (next === 'agent') {
|
|
72
|
+
const { createWorld } = await import('./world-creation.js');
|
|
73
|
+
const result = await createWorld({ ...options, json: false }, (_data, readable) => {
|
|
74
|
+
process.stdout.write(readable.split('\n').map(terminalText).join('\n') + '\n');
|
|
75
|
+
}, undefined, { owner, workspace });
|
|
76
|
+
if (result.state === 'accepted' && result.resource_available && result.world_id)
|
|
77
|
+
world = identifier(result.world_id);
|
|
78
|
+
else
|
|
79
|
+
throw new CliError('world_creation_unavailable', 'The original request has no available world. Nothing was replaced or started.', 2);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (action === 'connect') {
|
|
83
|
+
const { connectSource } = await import('./source-connection.js');
|
|
84
|
+
const result = await connectSource({ workspace, expectedOwner: owner, upload: false, json: false,
|
|
85
|
+
openBrowser: options.openBrowser, signal: options.signal }, (_type, _data, readable) => {
|
|
86
|
+
process.stdout.write(readable.split('\n').map(terminalText).join('\n') + '\n');
|
|
87
|
+
});
|
|
88
|
+
if (result?.state === 'connected' && result.world_id)
|
|
89
|
+
world = identifier(result.world_id);
|
|
90
|
+
else if (result)
|
|
91
|
+
throw new CliError('source_processing_pending', `Inspect source request ${result.request_key} or use firedrill connect resume ${result.request_key}. No build was started.`);
|
|
92
|
+
else
|
|
93
|
+
throw new CliError('source_kept_local', 'Source kept locally. No world was connected.', 2);
|
|
94
|
+
}
|
|
95
|
+
if (action === 'connect-many') {
|
|
96
|
+
const { connectSources } = await import('./source-batch.js');
|
|
97
|
+
const result = await connectSources({ workspace, expectedOwner: owner, upload: false, json: false,
|
|
98
|
+
openBrowser: options.openBrowser, signal: options.signal }, (_type, _data, readable) => {
|
|
99
|
+
process.stdout.write(readable.split('\n').map(terminalText).join('\n') + '\n');
|
|
100
|
+
});
|
|
101
|
+
if (result)
|
|
102
|
+
world = identifier(result.world_id);
|
|
103
|
+
else
|
|
104
|
+
throw new CliError('source_kept_local', 'Source captures kept locally. No world was connected.', 2);
|
|
105
|
+
}
|
|
106
|
+
if (action === 'new-world') {
|
|
107
|
+
const { createWorld } = await import('./world-creation.js');
|
|
108
|
+
const result = await createWorld({ ...options, json: false }, (_data, readable) => {
|
|
109
|
+
process.stdout.write(readable.split('\n').map(terminalText).join('\n') + '\n');
|
|
110
|
+
}, undefined, { owner, workspace });
|
|
111
|
+
if (result.state === 'accepted' && result.resource_available && result.world_id)
|
|
112
|
+
world = identifier(result.world_id);
|
|
113
|
+
else
|
|
114
|
+
throw new CliError('world_creation_unavailable', 'The original request has no available world. Nothing was replaced or started.', 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!world && options.interactive)
|
|
118
|
+
world = await choosePaged('Saved world', prefix, owner, item => ({ label: item.name, detail: item.status }), options.signal);
|
|
119
|
+
if (!world)
|
|
120
|
+
throw new CliError('world_required', 'Pass --world <id>, use firedrill connect, or create a world with firedrill worlds new <agent>.', 2);
|
|
121
|
+
return { owner, workspace, world, prefix: `${prefix}/${world}` };
|
|
122
|
+
}
|
|
123
|
+
export function chooseWorkspace(owner, signal, allowBrowser = true) {
|
|
124
|
+
return choosePaged('Workspace', '/api/auth/native/workspaces', owner, item => ({ label: item.name, detail: item.role }), signal, allowBrowser);
|
|
125
|
+
}
|
|
126
|
+
export async function refreshWorkspaceAccess(owner, signal, key) {
|
|
127
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(key))
|
|
128
|
+
throw new CliError('membership_discovery_request_invalid', 'Use the original workspace refresh request key.', 2);
|
|
129
|
+
try {
|
|
130
|
+
return await api('/api/auth/native/membership-sync', { method: 'POST', body: {},
|
|
131
|
+
idempotencyKey: key, actor: owner, signal });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof CliError && error.code === 'membership_discovery_running')
|
|
135
|
+
return api('/api/auth/native/membership-sync', { actor: owner, signal });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function choosePaged(title, path, owner, describe, signal, allowBrowser = false) {
|
|
140
|
+
let number = 1, search = '';
|
|
141
|
+
while (true) {
|
|
142
|
+
const page = await api(`${path}?page=${number}&q=${encodeURIComponent(search)}`, { actor: owner, signal });
|
|
143
|
+
const choices = page.results.map(item => ({ value: item.id, ...describe(item) }));
|
|
144
|
+
if (path === '/api/auth/native/workspaces')
|
|
145
|
+
choices.push({ value: 'create-here', label: 'Create a workspace here',
|
|
146
|
+
detail: 'Name it and accept the terms in this terminal.' });
|
|
147
|
+
if (path === '/api/auth/native/workspaces')
|
|
148
|
+
choices.push({ value: 'create-workspace', label: 'Create a workspace in the browser',
|
|
149
|
+
detail: 'Optional profile setup, actual workspace creation and invitations.' });
|
|
150
|
+
if (path === '/api/auth/native/workspaces')
|
|
151
|
+
choices.push({ value: 'switch-organization', label: 'Switch organization',
|
|
152
|
+
detail: 'Authorize a new organization grant in the browser.' });
|
|
153
|
+
if (number > 1)
|
|
154
|
+
choices.unshift({ value: 'previous', label: 'Previous page', detail: '' });
|
|
155
|
+
if (page.next)
|
|
156
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
157
|
+
if (page.membership_sync)
|
|
158
|
+
choices.push({ value: 'refresh-access',
|
|
159
|
+
label: page.membership_sync.state === 'running' ? 'Check workspace access again' : 'Refresh workspace access',
|
|
160
|
+
detail: page.membership_sync.state === 'running' ? 'Access discovery is still running.' :
|
|
161
|
+
page.membership_sync.state === 'attention_required' ? 'The previous access refresh could not finish.' : '' });
|
|
162
|
+
choices.push({ value: 'search', label: search ? 'Change search' : 'Search entries', detail: search || '' }, { value: 'cancel', label: 'Cancel', detail: '' });
|
|
163
|
+
const value = await selectOne(title, choices, signal);
|
|
164
|
+
if (value === 'previous') {
|
|
165
|
+
number -= 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (value === 'next') {
|
|
169
|
+
number += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (value === 'cancel')
|
|
173
|
+
throw new CliError('canceled', 'Selection canceled. No resource was changed.', 130);
|
|
174
|
+
if (value === 'switch-organization' && path === '/api/auth/native/workspaces') {
|
|
175
|
+
const session = await login({ json: false, open: allowBrowser, signal });
|
|
176
|
+
if (session.user.id !== owner)
|
|
177
|
+
throw new CliError('native_account_changed', 'A different account signed in. Reopen the original command before selecting its workspace.', 2);
|
|
178
|
+
number = 1;
|
|
179
|
+
search = '';
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (value === 'create-here' && path === '/api/auth/native/workspaces') {
|
|
183
|
+
const created = await createWorkspaceHere(owner, signal, allowBrowser);
|
|
184
|
+
if (created)
|
|
185
|
+
return identifier(created);
|
|
186
|
+
number = 1;
|
|
187
|
+
search = '';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (value === 'create-workspace') {
|
|
191
|
+
const url = new URL('/onboarding', APP_ORIGIN).href;
|
|
192
|
+
process.stderr.write(`Open ${url} to create a workspace. This terminal does not create an organization or start an agent.\n`);
|
|
193
|
+
if (allowBrowser)
|
|
194
|
+
await openBrowser(url);
|
|
195
|
+
const check = await selectOne('After browser setup', [{ value: 'check', label: 'Check workspace access again' },
|
|
196
|
+
{ value: 'cancel', label: 'Return later', detail: 'Use firedrill workspace when setup is complete.' }], signal);
|
|
197
|
+
if (check === 'cancel')
|
|
198
|
+
throw new CliError('canceled', 'Browser setup stays saved. Return with firedrill workspace when ready.', 130);
|
|
199
|
+
const key = randomUUID();
|
|
200
|
+
process.stderr.write(`Recovery: firedrill workspace refresh --request-key ${key}\n`);
|
|
201
|
+
await refreshWorkspaceAccess(owner, signal, key);
|
|
202
|
+
number = 1;
|
|
203
|
+
search = '';
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (value === 'refresh-access') {
|
|
207
|
+
if (page.membership_sync?.state !== 'running') {
|
|
208
|
+
const key = randomUUID();
|
|
209
|
+
process.stderr.write(`Recovery: firedrill workspace refresh --request-key ${key}\n`);
|
|
210
|
+
await refreshWorkspaceAccess(owner, signal, key);
|
|
211
|
+
}
|
|
212
|
+
number = 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (value === 'search') {
|
|
216
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr, terminal: false });
|
|
217
|
+
try {
|
|
218
|
+
const entered = (await prompt.question('Find entries (blank for all): ', { signal })).trim();
|
|
219
|
+
if (entered.length > 200 || /[\u0000-\u001f\u007f-\u009f]/.test(entered))
|
|
220
|
+
throw new CliError('search_invalid', 'Use search text up to 200 characters without terminal controls.', 2);
|
|
221
|
+
search = entered;
|
|
222
|
+
number = 1;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
prompt.close();
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
return identifier(value);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { api } from './auth.js';
|
|
2
|
+
// Saved world preparation read used by simulation and scenario commands.
|
|
3
|
+
// The former interactive conversation client is not part of this release.
|
|
4
|
+
export const preparation = (context, signal) => api(`${context.prefix}/preparation`, { actor: context.owner, workspace: context.workspace, signal });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Supported native checkpoint formats and their declared fork-reset destination. */
|
|
2
|
+
export const checkpointResetBaselines = {
|
|
3
|
+
owned_postgres_rows_sequences_and_sql_clock: 'prepared_world',
|
|
4
|
+
declared_sqlite_files: 'initial_checkpoint',
|
|
5
|
+
owned_postgres_and_declared_sqlite_files: 'initial_checkpoint',
|
|
6
|
+
};
|
|
7
|
+
export function checkpointContents(scope) {
|
|
8
|
+
if (scope === 'owned_postgres_rows_sequences_and_sql_clock')
|
|
9
|
+
return 'PostgreSQL records, sequence positions and world time settings';
|
|
10
|
+
if (scope === 'declared_sqlite_files')
|
|
11
|
+
return 'Declared SQLite stores';
|
|
12
|
+
if (scope === 'owned_postgres_and_declared_sqlite_files')
|
|
13
|
+
return 'PostgreSQL records and sequence positions plus declared SQLite stores';
|
|
14
|
+
return 'State details not recorded';
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CliError } from './http.js';
|
|
2
|
+
export function identifier(value) {
|
|
3
|
+
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value))
|
|
4
|
+
throw new CliError('identifier_invalid', 'Choose a saved item or request by its full ID.', 2);
|
|
5
|
+
return value.toLowerCase();
|
|
6
|
+
}
|