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,331 @@
|
|
|
1
|
+
import { mkdir, lstat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { CliError, API_ORIGIN } from './http.js';
|
|
6
|
+
import { bootstrapProcessLock } from './bootstrap-process-lock.js';
|
|
7
|
+
function validDevice(value) {
|
|
8
|
+
if (!value || typeof value !== 'object')
|
|
9
|
+
return false;
|
|
10
|
+
const one = value;
|
|
11
|
+
if (one.schema !== 'firedrill.device-authorization.v1' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(one.id)
|
|
12
|
+
|| !['waiting', 'polling'].includes(one.state) || !/^client_[A-Za-z0-9]{1,120}$/.test(one.client_id)
|
|
13
|
+
|| one.organization_hint !== null && (typeof one.organization_hint !== 'string' || !/^(?:org_[A-Za-z0-9]{1,120}|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/.test(one.organization_hint))
|
|
14
|
+
|| typeof one.device_code !== 'string' || !one.device_code || one.device_code.length > 16384
|
|
15
|
+
|| !/^[A-Z0-9-]{4,64}$/.test(one.user_code) || typeof one.verification_url !== 'string' || one.verification_url.length > 8192
|
|
16
|
+
|| !Number.isSafeInteger(one.expires_at) || one.expires_at <= 0 || !Number.isSafeInteger(one.next_poll_at) || one.next_poll_at <= 0
|
|
17
|
+
|| !Number.isSafeInteger(one.interval) || one.interval < 1 || one.interval > 3600)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(one.verification_url);
|
|
21
|
+
return url.protocol === 'https:' && !url.username && !url.password && !url.hash && url.pathname === '/device'
|
|
22
|
+
&& [...url.searchParams.keys()].every(key => key === 'user_code')
|
|
23
|
+
&& (!url.search || url.searchParams.get('user_code') === one.user_code);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function valid(value) {
|
|
30
|
+
if (!value || typeof value !== 'object')
|
|
31
|
+
return false;
|
|
32
|
+
const item = value;
|
|
33
|
+
return item.schema === 'firedrill.native-credentials.v1' && ['ready', 'refresh_pending', 'organization_selection_pending'].includes(item.state)
|
|
34
|
+
&& /^client_[A-Za-z0-9]{1,120}$/.test(item.client_id) && /^user_[A-Za-z0-9]{1,120}$/.test(item.subject)
|
|
35
|
+
&& /^session_[A-Za-z0-9]{1,120}$/.test(item.sid) && typeof item.access_token === 'string' && item.access_token.length <= 16384
|
|
36
|
+
&& typeof item.refresh_token === 'string' && item.refresh_token.length > 0 && item.refresh_token.length <= 16384
|
|
37
|
+
&& Number.isSafeInteger(item.expires_at) && item.expires_at > 0;
|
|
38
|
+
}
|
|
39
|
+
const LINUX_STORE = { linux: { store: 'secret-service' } };
|
|
40
|
+
async function entry(service = 'run.firedrill.simulation.native.v1') {
|
|
41
|
+
try {
|
|
42
|
+
const { Entry } = await import('@napi-rs/keyring');
|
|
43
|
+
return new Entry(service, API_ORIGIN, process.platform === 'linux' ? LINUX_STORE : undefined);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new CliError('credential_store_unavailable', 'Unlock or configure your OS credential store, or use --session-only. Credentials will not be saved in a file.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function classifyStoreError(error) {
|
|
50
|
+
// Classified from the binding's error text only. The raw text is never
|
|
51
|
+
// returned or printed because it can carry platform internals.
|
|
52
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
53
|
+
if (/matched by|ambiguous/i.test(text))
|
|
54
|
+
return { ok: false, failure: 'ambiguous_entry' };
|
|
55
|
+
if (/locked|interaction is not allowed|user interaction|-25308|-128\b|cancel/i.test(text))
|
|
56
|
+
return { ok: false, failure: 'store_locked' };
|
|
57
|
+
if (/couldn.t access|no storage access|serviceunknown|org\.freedesktop|d-?bus|secret ?service|connection refused|no such file|not provided by any \.service/i.test(text))
|
|
58
|
+
return { ok: false, failure: 'store_unreachable' };
|
|
59
|
+
return { ok: false, failure: 'store_failure' };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Probe the OS credential store without storing any secret. The read probe looks
|
|
63
|
+
* up a fixed, normally absent entry. The write probe additionally stores a fixed
|
|
64
|
+
* non-secret marker under a separate probe service, reads it back and deletes it,
|
|
65
|
+
* so a store that can be searched but not written (for example a locked Secret
|
|
66
|
+
* Service collection) is not reported as available.
|
|
67
|
+
*/
|
|
68
|
+
export async function probeCredentialStore(options = {}) {
|
|
69
|
+
let Entry;
|
|
70
|
+
try {
|
|
71
|
+
({ Entry } = await import('@napi-rs/keyring'));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { ok: false, failure: 'binding_unavailable' };
|
|
75
|
+
}
|
|
76
|
+
options.signal?.throwIfAborted();
|
|
77
|
+
let probe;
|
|
78
|
+
try {
|
|
79
|
+
probe = new Entry('run.firedrill.simulation.probe.v1', API_ORIGIN, process.platform === 'linux' ? LINUX_STORE : undefined);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return classifyStoreError(error);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
probe.getPassword();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return classifyStoreError(error);
|
|
89
|
+
}
|
|
90
|
+
if (!options.write)
|
|
91
|
+
return { ok: true };
|
|
92
|
+
const marker = 'firedrill-credential-store-probe';
|
|
93
|
+
try {
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
probe.setPassword(marker);
|
|
96
|
+
if (probe.getPassword() !== marker)
|
|
97
|
+
return { ok: false, failure: 'write_unconfirmed' };
|
|
98
|
+
return { ok: true };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (options.signal?.aborted)
|
|
102
|
+
throw error;
|
|
103
|
+
return classifyStoreError(error);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
try {
|
|
107
|
+
probe.deleteCredential();
|
|
108
|
+
}
|
|
109
|
+
catch { /* A leftover marker is not a secret. */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Process credential mode and the volatile session store.
|
|
114
|
+
let selectedMode = null;
|
|
115
|
+
let companionRole = false;
|
|
116
|
+
const volatile = { credentials: null, pending: null, erased: false };
|
|
117
|
+
/** After erasure the volatile session is closed: an in-flight refresh or login cannot write tokens back. */
|
|
118
|
+
function assertVolatileOpen() {
|
|
119
|
+
if (volatile.erased)
|
|
120
|
+
throw new CliError('companion_stopping', 'The session-only companion signed out or is stopping. No credential was kept.', 2);
|
|
121
|
+
}
|
|
122
|
+
/** Select the credential mode once per process, before any credential access. */
|
|
123
|
+
export function setCredentialMode(mode, options = {}) {
|
|
124
|
+
if (mode !== 'persistent' && mode !== 'session')
|
|
125
|
+
throw new CliError('credential_mode_invalid', 'Use persistent or session-only credentials. No credential mode was selected.', 2);
|
|
126
|
+
const companion = options.companion === true;
|
|
127
|
+
if (selectedMode !== null) {
|
|
128
|
+
if (selectedMode === mode && companionRole === companion)
|
|
129
|
+
return;
|
|
130
|
+
throw new CliError('credential_mode_locked', 'This process already selected a credential mode. No credentials were moved between modes.', 2);
|
|
131
|
+
}
|
|
132
|
+
selectedMode = mode;
|
|
133
|
+
companionRole = companion;
|
|
134
|
+
}
|
|
135
|
+
/** Defaults to persistent so existing commands keep their OS-store behavior. */
|
|
136
|
+
export function credentialMode() { return selectedMode ?? 'persistent'; }
|
|
137
|
+
/** True only in the process that holds credentials and serves companion IPC. */
|
|
138
|
+
export function isCredentialCompanion() { return companionRole; }
|
|
139
|
+
function volatileHolder() {
|
|
140
|
+
if (selectedMode !== 'session')
|
|
141
|
+
return false;
|
|
142
|
+
if (!companionRole)
|
|
143
|
+
throw new CliError('session_companion_required', 'No signed-in session-only companion is running. Run `firedrill run` or `firedrill init --session-only` in another terminal, or sign in with an OS credential store.', 2);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/** Non-secret view of the volatile session for companion status. */
|
|
147
|
+
export function volatileCredentialSummary() {
|
|
148
|
+
const value = selectedMode === 'session' && companionRole ? volatile.credentials : null;
|
|
149
|
+
return { signed_in: value?.state === 'ready', state: value?.state ?? null, subject: value?.subject ?? null, expires_at: value?.expires_at ?? null };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Erase the volatile credentials and pending authorization. Called on companion
|
|
153
|
+
* shutdown and process exit. JavaScript strings cannot be zeroed; dropping every
|
|
154
|
+
* reference is the strongest erasure this runtime offers.
|
|
155
|
+
*/
|
|
156
|
+
export function eraseVolatileCredentials() {
|
|
157
|
+
volatile.credentials = null;
|
|
158
|
+
volatile.pending = null;
|
|
159
|
+
volatile.erased = true;
|
|
160
|
+
}
|
|
161
|
+
const DEVICE_SERVICE = 'run.firedrill.simulation.device.v1';
|
|
162
|
+
/** Separate original attempt (OS store, or memory in session mode). Never returned as a CLI/model receipt. */
|
|
163
|
+
export async function loadPendingDevice() {
|
|
164
|
+
if (volatileHolder())
|
|
165
|
+
return volatile.pending ? { ...volatile.pending } : null;
|
|
166
|
+
try {
|
|
167
|
+
const encoded = (await entry(DEVICE_SERVICE)).getPassword();
|
|
168
|
+
if (encoded == null)
|
|
169
|
+
return null;
|
|
170
|
+
if (encoded.length > 30000)
|
|
171
|
+
throw new Error();
|
|
172
|
+
const value = JSON.parse(encoded);
|
|
173
|
+
if (!validDevice(value))
|
|
174
|
+
throw new Error();
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (error instanceof CliError)
|
|
179
|
+
throw error;
|
|
180
|
+
throw new CliError('credential_store_unavailable', 'The pending authorization could not be read from the OS credential store. No replacement attempt was created.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function savePendingDevice(value, signal) {
|
|
184
|
+
if (!validDevice(value))
|
|
185
|
+
throw new CliError('device_authorization_invalid', 'The pending authorization could not be confirmed.');
|
|
186
|
+
if (volatileHolder()) {
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
assertVolatileOpen();
|
|
189
|
+
volatile.pending = { ...value };
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const store = await entry(DEVICE_SERVICE), encoded = JSON.stringify(value);
|
|
194
|
+
signal?.throwIfAborted();
|
|
195
|
+
store.setPassword(encoded);
|
|
196
|
+
if (store.getPassword() !== encoded)
|
|
197
|
+
throw new Error();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
throw new CliError('credential_store_unavailable', 'The original authorization could not be saved securely. No file fallback is used.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function removePendingDevice(signal) {
|
|
204
|
+
if (volatileHolder()) {
|
|
205
|
+
signal?.throwIfAborted();
|
|
206
|
+
volatile.pending = null;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const store = await entry(DEVICE_SERVICE);
|
|
211
|
+
signal?.throwIfAborted();
|
|
212
|
+
if (store.getPassword() != null)
|
|
213
|
+
store.deleteCredential();
|
|
214
|
+
if (store.getPassword() != null)
|
|
215
|
+
throw new Error();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new CliError('credential_removal_unconfirmed', 'The pending authorization could not be removed from the OS credential store. No replacement attempt was created.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export async function loadCredentials() {
|
|
222
|
+
if (volatileHolder())
|
|
223
|
+
return volatile.credentials && !volatile.erased ? { ...volatile.credentials } : null;
|
|
224
|
+
try {
|
|
225
|
+
const value = (await entry()).getPassword();
|
|
226
|
+
if (value == null)
|
|
227
|
+
return null;
|
|
228
|
+
if (value.length > 40000)
|
|
229
|
+
throw new Error();
|
|
230
|
+
const parsed = JSON.parse(value);
|
|
231
|
+
if (!valid(parsed))
|
|
232
|
+
throw new Error();
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error instanceof CliError)
|
|
237
|
+
throw error;
|
|
238
|
+
throw new CliError('credential_store_unavailable', 'The saved sign-in could not be read. Unlock your OS credential store.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export async function saveCredentials(value, signal) {
|
|
242
|
+
if (!valid(value))
|
|
243
|
+
throw new CliError('credentials_invalid', 'The sign-in response is incomplete.');
|
|
244
|
+
if (volatileHolder()) {
|
|
245
|
+
signal?.throwIfAborted();
|
|
246
|
+
assertVolatileOpen();
|
|
247
|
+
volatile.credentials = { ...value };
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const store = await entry();
|
|
252
|
+
const encoded = JSON.stringify(value);
|
|
253
|
+
signal?.throwIfAborted();
|
|
254
|
+
store.setPassword(encoded);
|
|
255
|
+
if (store.getPassword() !== encoded)
|
|
256
|
+
throw new Error();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new CliError('credential_store_unavailable', 'Sign-in could not be saved to the OS credential store. No file fallback is used.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export async function removeCredentials(signal) {
|
|
263
|
+
if (volatileHolder()) {
|
|
264
|
+
signal?.throwIfAborted();
|
|
265
|
+
volatile.credentials = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const store = await entry();
|
|
270
|
+
signal?.throwIfAborted();
|
|
271
|
+
if (store.getPassword() != null)
|
|
272
|
+
store.deleteCredential();
|
|
273
|
+
if (store.getPassword() != null)
|
|
274
|
+
throw new Error();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
throw new CliError('credential_removal_unconfirmed', 'Terminal access was revoked, but local credential removal was not confirmed. Unlock the OS credential store and run logout again.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export async function ensurePrivateDirectory(directory) {
|
|
281
|
+
await mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
282
|
+
throw error; });
|
|
283
|
+
const stat = await lstat(directory);
|
|
284
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid())
|
|
285
|
+
|| process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
|
|
286
|
+
throw new CliError('credential_lock_unsafe', 'Use a private Firedrill state directory owned by this OS user, without symbolic links or shared permissions.');
|
|
287
|
+
}
|
|
288
|
+
return directory;
|
|
289
|
+
}
|
|
290
|
+
export async function privateDirectory() {
|
|
291
|
+
const root = await ensurePrivateDirectory(join(homedir(), '.firedrill-simulation'));
|
|
292
|
+
if (API_ORIGIN === 'https://api.firedrill.run')
|
|
293
|
+
return root;
|
|
294
|
+
// Public history is not moved or searched as a development fallback. Every
|
|
295
|
+
// request journal, local source bundle, runner and helper cache below this
|
|
296
|
+
// directory inherits the same selected non-production API namespace as the keyring.
|
|
297
|
+
const profiles = await ensurePrivateDirectory(join(root, 'profiles'));
|
|
298
|
+
return ensurePrivateDirectory(join(profiles, createHash('sha256').update(API_ORIGIN).digest('hex')));
|
|
299
|
+
}
|
|
300
|
+
let volatileLockTail = Promise.resolve();
|
|
301
|
+
export async function credentialLock(work) {
|
|
302
|
+
if (volatileHolder()) {
|
|
303
|
+
// Session credentials live only in this process, so an in-process queue
|
|
304
|
+
// serializes login/refresh intent. refresh_pending still refuses a second
|
|
305
|
+
// exchange of an uncertain refresh credential.
|
|
306
|
+
const previous = volatileLockTail;
|
|
307
|
+
let release = () => { };
|
|
308
|
+
volatileLockTail = new Promise(resolve => { release = resolve; });
|
|
309
|
+
await previous;
|
|
310
|
+
try {
|
|
311
|
+
return await work(new AbortController().signal);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
release();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// OS-owned coordination releases on process death. The saved refresh_pending
|
|
318
|
+
// state still refuses another exchange of an uncertain refresh credential.
|
|
319
|
+
// A retained directory lock from a prior client is never stolen or removed.
|
|
320
|
+
const directory = await privateDirectory();
|
|
321
|
+
const lock = await bootstrapProcessLock(join(directory, 'auth.lock'), { waitMs: 30000 });
|
|
322
|
+
try {
|
|
323
|
+
lock.assertHeld();
|
|
324
|
+
const value = await work(lock.signal);
|
|
325
|
+
lock.assertHeld();
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await lock.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CliError } from './http.js';
|
|
2
|
+
export function creditAmount(units, scale) {
|
|
3
|
+
if (!Number.isSafeInteger(units) || units < 0 || !Number.isSafeInteger(scale) || scale < 1 || !/^10*$/.test(String(scale)))
|
|
4
|
+
throw new CliError('credit_amount_unconfirmed', 'The frozen credit amount could not be confirmed. No amount was estimated.');
|
|
5
|
+
const value = BigInt(units), divisor = BigInt(scale), digits = String(scale).length - 1;
|
|
6
|
+
const fraction = (value % divisor).toString().padStart(digits, '0').replace(/0+$/, '');
|
|
7
|
+
return new Intl.NumberFormat().format(value / divisor) + (fraction ? '.' + fraction : '');
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../contracts/dataset-contracts.js';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, lstat, open, realpath, unlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { access } from './auth.js';
|
|
6
|
+
import { CliError, endpoint, responseError } from './http.js';
|
|
7
|
+
import { selectOne, terminalText } from './prompts.js';
|
|
8
|
+
import { identifier } from './world-contracts.js';
|
|
9
|
+
import { datasetReads } from './dataset-reads.js';
|
|
10
|
+
const interactive = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
11
|
+
async function question(label, signal) {
|
|
12
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr, terminal: false });
|
|
13
|
+
try {
|
|
14
|
+
return (await prompt.question(label, { signal })).trim();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
prompt.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function chooseDataset(prefix, scope, signal) {
|
|
21
|
+
let page = 1, query = '';
|
|
22
|
+
while (true) {
|
|
23
|
+
const rows = await datasetReads(prefix, scope, signal).page({ page, query, state: 'retained' });
|
|
24
|
+
const choices = rows.results.filter(row => row.available).map(row => ({ value: row.id, label: row.name, detail: `Version ${row.version}` }));
|
|
25
|
+
if (page > 1)
|
|
26
|
+
choices.unshift({ value: 'previous', label: 'Previous page', detail: '' });
|
|
27
|
+
if (rows.next)
|
|
28
|
+
choices.push({ value: 'next', label: 'Next page', detail: '' });
|
|
29
|
+
choices.push({ value: 'search', label: 'Search datasets', detail: query }, { value: 'cancel', label: 'Cancel', detail: '' });
|
|
30
|
+
const choice = await selectOne('Dataset to export', choices, signal);
|
|
31
|
+
if (choice === 'next') {
|
|
32
|
+
page++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (choice === 'previous') {
|
|
36
|
+
page--;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (choice === 'cancel')
|
|
40
|
+
throw new CliError('canceled', 'No dataset was downloaded.', 130);
|
|
41
|
+
if (choice === 'search') {
|
|
42
|
+
query = await question('Find a dataset (blank for all): ', signal);
|
|
43
|
+
if (query.length > 200 || /[\u0000-\u001f\u007f-\u009f]/u.test(query))
|
|
44
|
+
throw new CliError('dataset_search_invalid', 'Use search text of up to 200 characters.', 2);
|
|
45
|
+
page = 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return identifier(choice);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function metadata(row, id) {
|
|
52
|
+
if (!row || row.id !== id || row.available !== true || !row.file || !Number.isSafeInteger(row.file.size)
|
|
53
|
+
|| row.file.size < 1 || row.file.size > 8 * 1024 * 1024 || !/^[a-f0-9]{64}$/.test(row.file.sha256))
|
|
54
|
+
throw new CliError('dataset_export_unavailable', 'The original dataset file is unavailable. Inspect its saved version before downloading.', 2);
|
|
55
|
+
return row.file;
|
|
56
|
+
}
|
|
57
|
+
/** Read-only export of original GCS bytes. No recipe application or target run. */
|
|
58
|
+
export async function exportDataset(prefix, selected, scope, options, output) {
|
|
59
|
+
const uuid = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
|
|
60
|
+
if (!new RegExp(`^/api/workspaces/${identifier(scope.workspace)}/(?:tools|worlds/${uuid})$`).test(prefix))
|
|
61
|
+
throw new CliError('dataset_export_scope_invalid', 'Choose a dataset in the current workspace.', 2);
|
|
62
|
+
const tty = interactive(options);
|
|
63
|
+
if (!selected && !tty)
|
|
64
|
+
throw new CliError('dataset_required', 'Provide a saved dataset version ID.', 2);
|
|
65
|
+
const id = selected ? identifier(selected) : await chooseDataset(prefix, scope, options.signal);
|
|
66
|
+
const reads = datasetReads(prefix, scope, options.signal);
|
|
67
|
+
const row = await reads.detail(id), expected = metadata(row, id);
|
|
68
|
+
const into = options.into ?? (tty ? await question('Save private dataset JSON to a new file: ', options.signal) : '');
|
|
69
|
+
if (!into.trim() || /[\u0000-\u001f\u007f-\u009f]/u.test(into))
|
|
70
|
+
throw new CliError('dataset_export_path_required', 'Use --into <new-file.json>. Existing files are never replaced.', 2);
|
|
71
|
+
output('dataset_export_review', { id, name: row.name, version: row.version, file: expected }, `${terminalText(row.name)} · Version ${row.version}\n${expected.size.toLocaleString()} bytes. This private device copy will not be erased by cloud retention or withdrawal.`);
|
|
72
|
+
if (!options.approveDownload) {
|
|
73
|
+
if (!tty)
|
|
74
|
+
throw new CliError('dataset_export_consent_required', 'Use --approve-download to save this private dataset on the device.', 2);
|
|
75
|
+
if (!await selectOne('Save this private dataset?', [{ value: false, label: 'Cancel' }, { value: true, label: 'Export JSON' }], options.signal))
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const chosen = resolve(into), parent = await realpath(dirname(chosen)), folder = await lstat(parent), destination = join(parent, basename(chosen));
|
|
79
|
+
if (!folder.isDirectory() || process.platform !== 'win32' && (process.getuid && folder.uid !== process.getuid() || !!(folder.mode & 0o022)))
|
|
80
|
+
throw new CliError('dataset_export_directory_invalid', 'Choose a directory owned by this OS user without shared write permission.', 2);
|
|
81
|
+
try {
|
|
82
|
+
await lstat(destination);
|
|
83
|
+
throw new CliError('dataset_export_file_exists', 'The chosen file already exists. Choose a new path.', 2);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const partial = join(parent, `.firedrill-dataset-${randomUUID()}.partial`), file = await open(partial, 'wx', 0o600), held = await file.stat();
|
|
90
|
+
try {
|
|
91
|
+
await access(async (token) => {
|
|
92
|
+
const response = await fetch(endpoint(`${prefix}/datasets/${id}/file`), { redirect: 'error', credentials: 'omit',
|
|
93
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Firedrill-Actor': scope.owner, 'X-Firedrill-Workspace': scope.workspace, 'Accept-Encoding': 'identity' },
|
|
94
|
+
signal: AbortSignal.any([options.signal, AbortSignal.timeout(120000)]) });
|
|
95
|
+
if (response.status !== 200) {
|
|
96
|
+
await response.body?.cancel();
|
|
97
|
+
throw responseError(null, response.status);
|
|
98
|
+
}
|
|
99
|
+
const length = response.headers.get('content-length');
|
|
100
|
+
if (!response.body || length !== null && length !== String(expected.size) || response.headers.get('x-content-sha256') !== expected.sha256) {
|
|
101
|
+
await response.body?.cancel();
|
|
102
|
+
throw new CliError('dataset_export_integrity', 'The response did not match the original dataset file. Nothing was published.');
|
|
103
|
+
}
|
|
104
|
+
const reader = response.body.getReader(), digest = createHash('sha256');
|
|
105
|
+
let size = 0;
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
options.signal.throwIfAborted();
|
|
109
|
+
const part = await reader.read();
|
|
110
|
+
if (part.done)
|
|
111
|
+
break;
|
|
112
|
+
size += part.value.byteLength;
|
|
113
|
+
if (size > expected.size)
|
|
114
|
+
throw new CliError('dataset_export_integrity', 'The file exceeded its recorded size. Nothing was published.');
|
|
115
|
+
digest.update(part.value);
|
|
116
|
+
let offset = 0;
|
|
117
|
+
while (offset < part.value.byteLength) {
|
|
118
|
+
const written = await file.write(part.value, offset, part.value.byteLength - offset);
|
|
119
|
+
if (!written.bytesWritten)
|
|
120
|
+
throw new Error();
|
|
121
|
+
offset += written.bytesWritten;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await reader.cancel().catch(() => undefined);
|
|
127
|
+
reader.releaseLock();
|
|
128
|
+
}
|
|
129
|
+
if (size !== expected.size || digest.digest('hex') !== expected.sha256)
|
|
130
|
+
throw new CliError('dataset_export_integrity', 'The dataset was incomplete or changed. Nothing was published.');
|
|
131
|
+
});
|
|
132
|
+
const fresh = metadata(await reads.detail(id, { digest: row.request_digest }), id);
|
|
133
|
+
if (fresh.size !== expected.size || fresh.sha256 !== expected.sha256)
|
|
134
|
+
throw new CliError('dataset_export_integrity', 'The original dataset changed. Nothing was published.');
|
|
135
|
+
await file.sync();
|
|
136
|
+
options.signal.throwIfAborted();
|
|
137
|
+
const current = await lstat(partial);
|
|
138
|
+
if (!current.isFile() || current.dev !== held.dev || current.ino !== held.ino || current.size !== expected.size)
|
|
139
|
+
throw new Error();
|
|
140
|
+
await link(partial, destination);
|
|
141
|
+
output('dataset_exported', { id, path: destination, ...expected }, `Dataset exported to ${terminalText(destination)}\n${expected.size.toLocaleString()} bytes · SHA-256 ${expected.sha256}`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof CliError)
|
|
145
|
+
throw error;
|
|
146
|
+
throw new CliError('dataset_export_unconfirmed', 'The export did not finish. Inspect the chosen path before retrying; no existing file was replaced.');
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await file.close().catch(() => undefined);
|
|
150
|
+
const current = await lstat(partial).catch(() => null);
|
|
151
|
+
if (current?.isFile() && current.dev === held.dev && current.ino === held.ino)
|
|
152
|
+
await unlink(partial).catch(() => undefined);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { api } from './auth.js';
|
|
2
|
+
import { identifier } from './world-contracts.js';
|
|
3
|
+
import { datasetScope, datasetRead, datasetMatches, datasetPageMatches, datasetDocumentMatches } from './dataset-contracts.js';
|
|
4
|
+
export function datasetReads(prefix, owner, signal) {
|
|
5
|
+
const scope = datasetScope(prefix, owner.owner);
|
|
6
|
+
if (!scope || scope.workspace !== owner.workspace)
|
|
7
|
+
throw new Error('Choose a dataset in the current workspace.');
|
|
8
|
+
const auth = { actor: owner.owner, workspace: owner.workspace, signal };
|
|
9
|
+
const detail = async (id, selected = {}) => {
|
|
10
|
+
const value = await api(`${prefix}/datasets/${identifier(id)}`, auth);
|
|
11
|
+
return datasetRead(value, datasetMatches(value, scope, { ...selected, id }));
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
async page(filters = {}) {
|
|
15
|
+
const params = new URLSearchParams({ page: String(filters.page ?? 1), q: filters.query ?? '', state: filters.state ?? 'all' });
|
|
16
|
+
if (filters.source)
|
|
17
|
+
params.set(scope.kind === 'tool' ? 'checkpoint_id' : 'observation_id', identifier(filters.source));
|
|
18
|
+
if (filters.implementation && scope.kind === 'tool')
|
|
19
|
+
params.set('implementation_id', identifier(filters.implementation));
|
|
20
|
+
if (filters.series)
|
|
21
|
+
params.set('series_id', identifier(filters.series));
|
|
22
|
+
if (scope.kind === 'world')
|
|
23
|
+
params.set('startable', String(filters.startable ?? false));
|
|
24
|
+
const value = await api(`${prefix}/datasets?${params}`, auth);
|
|
25
|
+
return datasetRead(value, datasetPageMatches(value, scope, { ...filters, page: filters.page ?? 1 }));
|
|
26
|
+
},
|
|
27
|
+
detail,
|
|
28
|
+
async original(key, selected = {}) {
|
|
29
|
+
const value = await api(`${prefix}/dataset-requests/${identifier(key)}`, auth);
|
|
30
|
+
return datasetRead(value, datasetMatches(value, scope, { ...selected, key }));
|
|
31
|
+
},
|
|
32
|
+
async document(row) {
|
|
33
|
+
const value = await api(`${prefix}/datasets/${identifier(row.id)}/document`, { ...auth, maximumResponseBytes: 16 * 1024 ** 2 });
|
|
34
|
+
return datasetRead(value, datasetDocumentMatches(value, scope, row));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|