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,159 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { CliError } from './http.js';
|
|
4
|
+
import { executeLocalWorldSession } from './runner-container.js';
|
|
5
|
+
import { withLocalJobInputs } from './runner-ownership.js';
|
|
6
|
+
import { LocalWorldCommandDelivery } from './runner-world-commands.js';
|
|
7
|
+
import { LocalWorldJournal } from './runner-world-journal.js';
|
|
8
|
+
const invalid = () => new CliError('local_world_collection_unconfirmed', 'The original local world journal is incomplete. No target was restarted or cloud completion claimed.');
|
|
9
|
+
async function retain(journal, label, body, signal) {
|
|
10
|
+
if (!body.length || body.length > 12 * 1024 ** 2)
|
|
11
|
+
throw invalid();
|
|
12
|
+
const parts = [];
|
|
13
|
+
for (let offset = 0, index = 0; offset < body.length; offset += 1024 ** 2, index++) {
|
|
14
|
+
signal.throwIfAborted();
|
|
15
|
+
const part = body.subarray(offset, offset + 1024 ** 2);
|
|
16
|
+
await journal.put(label + '-part-' + index, part);
|
|
17
|
+
parts.push({ size: part.length, sha256: createHash('sha256').update(part).digest('hex') });
|
|
18
|
+
}
|
|
19
|
+
const metadata = Buffer.from(JSON.stringify({ schema: 'firedrill.local-world-journal-file.v1',
|
|
20
|
+
size: body.length, sha256: createHash('sha256').update(body).digest('hex'), parts }));
|
|
21
|
+
try {
|
|
22
|
+
await journal.put(label + '-metadata', metadata);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
metadata.fill(0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function collector(configuration) {
|
|
29
|
+
let commandEvidencePending = false;
|
|
30
|
+
return {
|
|
31
|
+
async run(owner, actual, channel, signal) {
|
|
32
|
+
if (actual !== configuration || actual.control.schema !== 'firedrill.world-session.v1')
|
|
33
|
+
throw invalid();
|
|
34
|
+
const delivery = new LocalWorldCommandDelivery(owner, configuration, channel, signal);
|
|
35
|
+
try {
|
|
36
|
+
while (!channel.isClosing) {
|
|
37
|
+
try {
|
|
38
|
+
const command = await delivery.next();
|
|
39
|
+
if (command) {
|
|
40
|
+
commandEvidencePending = true;
|
|
41
|
+
const result = command.publication_key === null ? await delivery.deliver(command) : await delivery.recover(command);
|
|
42
|
+
commandEvidencePending = !['observed', 'not_dispatched', 'unknown', 'stop_requested'].includes(result.state);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!channel.isClosing || !(error instanceof CliError) || !['local_world_claim_changed', 'local_world_command_unavailable',
|
|
47
|
+
'local_world_evidence_unavailable', 'local_world_file_unavailable', 'local_session_observation_unavailable'].includes(error.code))
|
|
48
|
+
throw error;
|
|
49
|
+
// Closing can revoke the execution claim while an original file
|
|
50
|
+
// transfer is pending. Keep the actual closed journal, not an
|
|
51
|
+
// execution retry, extended lease or invented cloud receipt.
|
|
52
|
+
commandEvidencePending = true;
|
|
53
|
+
}
|
|
54
|
+
if (!channel.isClosing)
|
|
55
|
+
await Promise.race([sleep(500, undefined, { signal }), channel.waitClosing()]);
|
|
56
|
+
}
|
|
57
|
+
await channel.waitClosing();
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await delivery.close();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async collect(owner, observation, final, signal) {
|
|
64
|
+
if (configuration.control.schema !== 'firedrill.world-session.v1' || observation.operation_id !== owner.lease
|
|
65
|
+
|| final.observation.operation_id !== owner.lease || final.observation.session_id !== owner.session.session_id)
|
|
66
|
+
throw invalid();
|
|
67
|
+
const journal = await LocalWorldJournal.acquire(owner, configuration.manifest_sha256, { existingOnly: true });
|
|
68
|
+
signal = AbortSignal.any([signal, journal.signal]);
|
|
69
|
+
try {
|
|
70
|
+
const context = Buffer.from(configuration.control.evidence_context_sha256);
|
|
71
|
+
try {
|
|
72
|
+
if (context.length !== 64 || !/^[a-f0-9]{64}$/.test(context.toString('ascii')))
|
|
73
|
+
throw invalid();
|
|
74
|
+
await journal.put('evidence-context', context);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
context.fill(0);
|
|
78
|
+
}
|
|
79
|
+
if (final.reference) {
|
|
80
|
+
if (!final.statusBytes || final.statusBytes.length !== final.reference.size
|
|
81
|
+
|| createHash('sha256').update(final.statusBytes).digest('hex') !== final.reference.sha256)
|
|
82
|
+
throw invalid();
|
|
83
|
+
await retain(journal, 'final-native-status', final.statusBytes, signal);
|
|
84
|
+
}
|
|
85
|
+
else if (final.statusBytes !== null)
|
|
86
|
+
throw invalid();
|
|
87
|
+
const body = Buffer.from(JSON.stringify(observation));
|
|
88
|
+
try {
|
|
89
|
+
await retain(journal, 'final-container-observation', body, signal);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
body.fill(0);
|
|
93
|
+
}
|
|
94
|
+
const result = { schema: 'firedrill.local-world-collection.v1', session_id: owner.session.session_id,
|
|
95
|
+
operation_id: owner.lease, manifest_sha256: configuration.manifest_sha256, stage: 'local_collected',
|
|
96
|
+
native_status_retained: final.reference !== null, command_evidence_pending: commandEvidencePending,
|
|
97
|
+
cloud_completion_recorded: false, provenance: 'customer_device' };
|
|
98
|
+
const receipt = Buffer.from(JSON.stringify(result));
|
|
99
|
+
try {
|
|
100
|
+
await journal.put('local-collection', receipt);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
receipt.fill(0);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await journal.close();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Original current-claim manual-world consumer, shared by session opening and
|
|
114
|
+
* explicit execution of a web-admitted assignment. Finite closing/evidence
|
|
115
|
+
* recovery remains separate. This never claims a target trial or assessment.
|
|
116
|
+
*/
|
|
117
|
+
export async function retainLocalWorldSession(context, device, job, request, signal) {
|
|
118
|
+
let operation, result, executionError;
|
|
119
|
+
try {
|
|
120
|
+
result = await withLocalJobInputs(context, device, job, request, 'world_session', signal, async (owner, inputs, configuration, active) => {
|
|
121
|
+
if (owner.purpose !== 'world_session' || !owner.evidenceUpload)
|
|
122
|
+
throw invalid();
|
|
123
|
+
operation = owner.lease;
|
|
124
|
+
return executeLocalWorldSession(owner, inputs, configuration, collector(configuration), active);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
executionError = error;
|
|
129
|
+
}
|
|
130
|
+
const { syncLocalWorldCompletion } = await import('./runner-world-completion.js');
|
|
131
|
+
let closing;
|
|
132
|
+
let evidenceRecovery;
|
|
133
|
+
if (operation) {
|
|
134
|
+
try {
|
|
135
|
+
closing = await syncLocalWorldCompletion(context, operation, AbortSignal.timeout(30000));
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!executionError)
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (operation && closing?.received && !closing.settled) {
|
|
143
|
+
try {
|
|
144
|
+
const { syncLocalWorldEvidence } = await import('./runner-world-evidence-sync.js');
|
|
145
|
+
await syncLocalWorldEvidence(context, operation, AbortSignal.timeout(30000));
|
|
146
|
+
closing = await syncLocalWorldCompletion(context, operation, AbortSignal.timeout(30000));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// The original cleanup receipt is already accepted. Keep it visible when
|
|
150
|
+
// optional private-file recovery is interrupted or no longer authorized.
|
|
151
|
+
evidenceRecovery = { state: 'unconfirmed', code: error instanceof CliError ? error.code : 'local_world_evidence_unconfirmed' };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (executionError)
|
|
155
|
+
throw executionError;
|
|
156
|
+
if (!result || !closing)
|
|
157
|
+
throw invalid();
|
|
158
|
+
return { ...result, closing, ...(evidenceRecovery ? { evidence_recovery: evidenceRecovery } : {}) };
|
|
159
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { api } from './auth.js';
|
|
4
|
+
import { CliError } from './http.js';
|
|
5
|
+
import { identifier } from './world-contracts.js';
|
|
6
|
+
import { historicalRunnerSigner, signRunnerRequest } from './runner-identity.js';
|
|
7
|
+
import { localWorldClaimPath, localWorldClaimTime } from './runner-world-claim.js';
|
|
8
|
+
import { LocalWorldJournal } from './runner-world-journal.js';
|
|
9
|
+
import { localClosingReceiptMatches } from '../../contracts/local-evidence-contracts.js';
|
|
10
|
+
import { localWorldIdentityMatches, LOCAL_WORLD_IDENTITY_KEYS } from '../../contracts/local-world-native-contracts.js';
|
|
11
|
+
const PART = 1024 ** 2, CHUNK = 16384, MAX = 544 * 1024 ** 2, HASH = /^[a-f0-9]{64}$/;
|
|
12
|
+
const FINISHED = ['observed', 'not_dispatched', 'unknown', 'stop_requested'];
|
|
13
|
+
const KINDS = ['receipt', 'response', 'state', 'checkpoint'];
|
|
14
|
+
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
const sha = (body) => createHash('sha256').update(body).digest('hex');
|
|
16
|
+
const invalid = () => new CliError('local_world_evidence_changed', 'The original command evidence could not be confirmed. No agent action was repeated.');
|
|
17
|
+
const exact = (value, keys) => Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
18
|
+
function canonical(value) {
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return '[' + value.map(canonical).join(',') + ']';
|
|
21
|
+
if (record(value))
|
|
22
|
+
return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}';
|
|
23
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
24
|
+
throw invalid();
|
|
25
|
+
const encoded = JSON.stringify(value);
|
|
26
|
+
if (encoded === undefined)
|
|
27
|
+
throw invalid();
|
|
28
|
+
return encoded;
|
|
29
|
+
}
|
|
30
|
+
const digest = (value) => sha(Buffer.from(canonical(value)));
|
|
31
|
+
const name = (command, suffix) => 'command-' + identifier(command.id) + '-' + suffix;
|
|
32
|
+
/** Original native observations/files only. Neither recovery nor uploading can
|
|
33
|
+
* publish a command or recreate a target. Disk holds encrypted chunks; the
|
|
34
|
+
* server separately validates the whole file and both private matcher sets.
|
|
35
|
+
*/
|
|
36
|
+
export class LocalWorldCommandEvidence {
|
|
37
|
+
owner;
|
|
38
|
+
manifest;
|
|
39
|
+
context;
|
|
40
|
+
channel;
|
|
41
|
+
journal;
|
|
42
|
+
signal;
|
|
43
|
+
closing;
|
|
44
|
+
constructor(owner, manifest, context, channel, journal, signal, closing = false) {
|
|
45
|
+
this.owner = owner;
|
|
46
|
+
this.manifest = manifest;
|
|
47
|
+
this.context = context;
|
|
48
|
+
this.channel = channel;
|
|
49
|
+
this.journal = journal;
|
|
50
|
+
this.signal = AbortSignal.any([signal, journal.signal]);
|
|
51
|
+
this.closing = closing;
|
|
52
|
+
}
|
|
53
|
+
static async open(owner, configuration, channel, signal) {
|
|
54
|
+
if (configuration.control.schema !== 'firedrill.world-session.v1')
|
|
55
|
+
throw invalid();
|
|
56
|
+
const journal = await LocalWorldJournal.acquire(owner, configuration.manifest_sha256, { existingOnly: true });
|
|
57
|
+
const context = configuration.control.evidence_context_sha256;
|
|
58
|
+
if (!HASH.test(context)) {
|
|
59
|
+
await journal.close();
|
|
60
|
+
throw invalid();
|
|
61
|
+
}
|
|
62
|
+
const raw = Buffer.from(context);
|
|
63
|
+
try {
|
|
64
|
+
await journal.put('evidence-context', raw);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await journal.close();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
raw.fill(0);
|
|
72
|
+
}
|
|
73
|
+
return new LocalWorldCommandEvidence(owner, configuration.manifest_sha256, context, channel, journal, signal);
|
|
74
|
+
}
|
|
75
|
+
static async recover(owner, manifest, signal) {
|
|
76
|
+
const journal = await LocalWorldJournal.acquire(owner, manifest, { existingOnly: true });
|
|
77
|
+
let raw = null;
|
|
78
|
+
try {
|
|
79
|
+
raw = await journal.get('evidence-context');
|
|
80
|
+
const context = raw?.toString('ascii');
|
|
81
|
+
if (!raw || raw.length !== 64 || !context || !HASH.test(context))
|
|
82
|
+
throw invalid();
|
|
83
|
+
return new LocalWorldCommandEvidence(owner, manifest, context, null, journal, signal, true);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await journal.close();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
raw?.fill(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async *commands() {
|
|
94
|
+
// Historical evidence is corroborated per original command, so publication
|
|
95
|
+
// need not sort or load the complete session before retaining its first file.
|
|
96
|
+
for await (const id of this.journal.commandIdentities(this.signal)) {
|
|
97
|
+
const raw = await this.journal.get('command-' + id + '-identity');
|
|
98
|
+
try {
|
|
99
|
+
if (!raw)
|
|
100
|
+
throw invalid();
|
|
101
|
+
const value = JSON.parse(raw.toString('utf8'));
|
|
102
|
+
if (!record(value) || !exact(value, ['id', 'sequence', 'kind', 'sha256', 'not_after']) || value.id !== id
|
|
103
|
+
|| typeof value.sequence !== 'string' || !/^[1-9][0-9]{0,15}$/.test(value.sequence) || !Number.isSafeInteger(Number(value.sequence))
|
|
104
|
+
|| typeof value.kind !== 'string' || !/^[a-z_]{1,32}$/.test(value.kind) || typeof value.sha256 !== 'string' || !HASH.test(value.sha256)
|
|
105
|
+
|| typeof value.not_after !== 'string' || !Number.isFinite(Date.parse(value.not_after)))
|
|
106
|
+
throw invalid();
|
|
107
|
+
yield value;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
raw?.fill(0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async retainFinalNative(reference) {
|
|
115
|
+
if (!this.closing || !Number.isSafeInteger(reference.size) || reference.size < 1 || reference.size > 8 * PART
|
|
116
|
+
|| !HASH.test(reference.sha256))
|
|
117
|
+
throw invalid();
|
|
118
|
+
const owner = this.owner, auth = { actor: owner.owner, workspace: owner.workspace, signal: this.signal };
|
|
119
|
+
const path = localWorldClaimPath(owner) + '/world-final-status';
|
|
120
|
+
const closing = await api(localWorldClaimPath(owner) + '/completion', auth);
|
|
121
|
+
if (!localClosingReceiptMatches(closing, { ...owner, manifestSha256: this.manifest })
|
|
122
|
+
|| closing.schema !== 'firedrill.local-completion-receipt.v1' || closing.job_id !== owner.job || closing.attempt_id !== owner.attempt
|
|
123
|
+
|| closing.lease_id !== owner.lease || closing.request_key !== owner.request || closing.manifest_sha256 !== this.manifest
|
|
124
|
+
|| closing.received !== true || closing.cleanup_recorded !== true || !HASH.test(closing.digest ?? '')
|
|
125
|
+
|| closing.document?.schema !== 'firedrill.local-world-container-receipt.v1'
|
|
126
|
+
|| closing.document.world_session.session_id !== owner.session.session_id
|
|
127
|
+
|| closing.document.world_session.input_sha256 !== owner.session.input_sha256
|
|
128
|
+
|| canonical(closing.document.evidence.native_status) !== canonical(reference))
|
|
129
|
+
throw invalid();
|
|
130
|
+
const check = (raw) => {
|
|
131
|
+
if (!record(raw) || !localWorldIdentityMatches(raw, owner, this.manifest)
|
|
132
|
+
|| !exact(raw, [...LOCAL_WORLD_IDENTITY_KEYS.filter(key => key !== 'manifest_sha256'), 'schema', 'session_id', 'lease_id', 'manifest_sha256', 'input_sha256', 'closing_sha256',
|
|
133
|
+
'state', 'artifact', 'provenance', 'independently_attested', 'server_time']) || raw.schema !== 'firedrill.local-world-final-receipt.v1'
|
|
134
|
+
|| raw.session_id !== owner.session.session_id || raw.lease_id !== owner.lease || raw.manifest_sha256 !== this.manifest
|
|
135
|
+
|| raw.input_sha256 !== owner.session.input_sha256 || raw.closing_sha256 !== closing.digest
|
|
136
|
+
|| !['pending', 'retained', 'withheld', 'invalid', 'unavailable'].includes(String(raw.state))
|
|
137
|
+
|| raw.provenance !== 'customer_device' || raw.independently_attested !== false
|
|
138
|
+
|| typeof raw.server_time !== 'string' || !Number.isFinite(Date.parse(raw.server_time)))
|
|
139
|
+
throw invalid();
|
|
140
|
+
if (raw.state === 'retained') {
|
|
141
|
+
const item = raw.artifact;
|
|
142
|
+
if (!record(item) || !exact(item, ['id', 'size', 'sha256', 'generation']) || typeof item.id !== 'string'
|
|
143
|
+
|| identifier(item.id) !== item.id || item.size !== reference.size || item.sha256 !== reference.sha256
|
|
144
|
+
|| typeof item.generation !== 'string' || !/^[1-9][0-9]*$/.test(item.generation))
|
|
145
|
+
throw invalid();
|
|
146
|
+
}
|
|
147
|
+
else if (raw.artifact !== null)
|
|
148
|
+
throw invalid();
|
|
149
|
+
return raw;
|
|
150
|
+
};
|
|
151
|
+
let receipt = check(await api(path, auth));
|
|
152
|
+
if (receipt.state !== 'pending')
|
|
153
|
+
return receipt;
|
|
154
|
+
if (closing.settled !== false)
|
|
155
|
+
throw invalid();
|
|
156
|
+
const metadata = await this.journal.get('final-native-status-metadata');
|
|
157
|
+
let body = null;
|
|
158
|
+
try {
|
|
159
|
+
if (!metadata)
|
|
160
|
+
throw new CliError('local_world_final_unavailable', 'The original final status was not saved locally. No execution was repeated.');
|
|
161
|
+
const value = JSON.parse(metadata.toString('utf8'));
|
|
162
|
+
if (!record(value) || !exact(value, ['schema', 'size', 'sha256', 'parts'])
|
|
163
|
+
|| value.schema !== 'firedrill.local-world-journal-file.v1')
|
|
164
|
+
throw invalid();
|
|
165
|
+
const plan = { size: value.size, sha256: value.sha256, parts: value.parts };
|
|
166
|
+
this.checkMeta(plan, { ...reference, maximum: 8 * PART });
|
|
167
|
+
body = Buffer.alloc(reference.size);
|
|
168
|
+
let offset = 0;
|
|
169
|
+
for (let index = 0; index < plan.parts.length; index++) {
|
|
170
|
+
this.signal.throwIfAborted();
|
|
171
|
+
const part = await this.journal.get('final-native-status-part-' + index), expected = plan.parts[index];
|
|
172
|
+
try {
|
|
173
|
+
if (!part || part.length !== expected.size || sha(part) !== expected.sha256)
|
|
174
|
+
throw invalid();
|
|
175
|
+
part.copy(body, offset);
|
|
176
|
+
offset += part.length;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
part?.fill(0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (offset !== body.length || sha(body) !== reference.sha256)
|
|
183
|
+
throw invalid();
|
|
184
|
+
const signer = await historicalRunnerSigner(owner, owner.device, this.signal);
|
|
185
|
+
const payload = { schema: 'firedrill.local-world-final-write.v1', job_id: owner.job, attempt_id: owner.attempt,
|
|
186
|
+
lease_id: owner.lease, request_key: owner.request, device_key: owner.device, native_session_id: signer.nativeSession,
|
|
187
|
+
manifest_sha256: this.manifest, session_id: owner.session.session_id, input_sha256: owner.session.input_sha256,
|
|
188
|
+
context_sha256: this.context, closing_sha256: closing.digest, write_key: randomUUID(), sent_at_ms: Date.parse(receipt.server_time),
|
|
189
|
+
size: body.length, sha256: reference.sha256, content_base64: body.toString('base64') };
|
|
190
|
+
const proof = await signer.sign(payload);
|
|
191
|
+
try {
|
|
192
|
+
receipt = check(await api(path, { ...auth, method: 'POST', body: proof }));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const observed = check(await api(path, auth));
|
|
196
|
+
if (observed.state === 'pending')
|
|
197
|
+
throw error;
|
|
198
|
+
receipt = observed; // Read original write intent, never automatic resubmission.
|
|
199
|
+
}
|
|
200
|
+
return receipt;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
metadata?.fill(0);
|
|
204
|
+
body?.fill(0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async remember(command) {
|
|
208
|
+
const body = Buffer.from(canonical(Object.fromEntries(['id', 'sequence', 'kind', 'sha256', 'not_after'].map(key => [key, command[key]]))));
|
|
209
|
+
try {
|
|
210
|
+
await this.journal.put(name(command, 'identity'), body);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
body.fill(0);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
inspectObservation(command, value) {
|
|
217
|
+
if (!record(value) || value.schema !== 'firedrill.world-session-command-observation.v1' || value.session_id !== this.owner.session.session_id
|
|
218
|
+
|| value.operation_id !== this.owner.lease || value.command_id !== command.id || value.command_sha256 !== command.sha256
|
|
219
|
+
|| value.sequence !== Number(command.sequence) || !['queued', 'accepted', ...FINISHED].includes(String(value.state)))
|
|
220
|
+
throw invalid();
|
|
221
|
+
const finished = FINISHED.includes(String(value.state));
|
|
222
|
+
if (!exact(value, ['schema', 'session_id', 'operation_id', 'command_id', 'sequence', 'command_sha256', 'state', 'accepted_at', 'finished_at',
|
|
223
|
+
...(finished ? ['code', 'response'] : [])])
|
|
224
|
+
|| (value.state === 'queued' ? value.accepted_at !== null
|
|
225
|
+
: typeof value.accepted_at !== 'string' || !Number.isFinite(Date.parse(value.accepted_at)))
|
|
226
|
+
|| (finished ? typeof value.finished_at !== 'string' || !Number.isFinite(Date.parse(value.finished_at)) : value.finished_at !== null)
|
|
227
|
+
|| finished && (typeof value.code !== 'string' || !/^([a-z][a-z0-9_]{0,127})?$/.test(value.code)
|
|
228
|
+
|| (value.state === 'observed' ? !record(value.response) : value.response !== null))
|
|
229
|
+
|| finished && typeof value.accepted_at === 'string' && typeof value.finished_at === 'string'
|
|
230
|
+
&& Date.parse(value.finished_at) < Date.parse(value.accepted_at))
|
|
231
|
+
throw invalid();
|
|
232
|
+
// Exact kind-specific native response validation remains server-owned. This
|
|
233
|
+
// declaration is not a successful outcome until that original API accepts it.
|
|
234
|
+
}
|
|
235
|
+
async saved(command) {
|
|
236
|
+
await this.remember(command);
|
|
237
|
+
const raw = await this.journal.get(name(command, 'terminal'));
|
|
238
|
+
if (!raw)
|
|
239
|
+
return null;
|
|
240
|
+
try {
|
|
241
|
+
if (raw.length > (['app_save', 'paired_save'].includes(command.kind) ? 2 * PART : 65536))
|
|
242
|
+
throw invalid();
|
|
243
|
+
const value = JSON.parse(raw.toString('utf8'));
|
|
244
|
+
this.inspectObservation(command, value);
|
|
245
|
+
if (!FINISHED.includes(value.state))
|
|
246
|
+
throw invalid();
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
raw.fill(0);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async request(command, input) {
|
|
254
|
+
this.signal.throwIfAborted();
|
|
255
|
+
const owner = this.owner;
|
|
256
|
+
let sent, nativeSession = owner.nativeSession, signer = null;
|
|
257
|
+
if (this.closing) {
|
|
258
|
+
const receipt = await api(localWorldClaimPath(owner) + '/completion', { actor: owner.owner, workspace: owner.workspace, signal: this.signal });
|
|
259
|
+
if (!localClosingReceiptMatches(receipt, { ...owner, manifestSha256: this.manifest })
|
|
260
|
+
|| receipt.schema !== 'firedrill.local-completion-receipt.v1' || receipt.job_id !== owner.job || receipt.attempt_id !== owner.attempt
|
|
261
|
+
|| receipt.lease_id !== owner.lease || receipt.request_key !== owner.request || receipt.manifest_sha256 !== this.manifest
|
|
262
|
+
|| receipt.received !== true || receipt.cleanup_recorded !== true || receipt.settled !== false
|
|
263
|
+
|| receipt.document?.schema !== 'firedrill.local-world-container-receipt.v1'
|
|
264
|
+
|| receipt.document.world_session.session_id !== owner.session.session_id
|
|
265
|
+
|| receipt.document.world_session.input_sha256 !== owner.session.input_sha256 || !Number.isFinite(Date.parse(receipt.server_time)))
|
|
266
|
+
throw invalid();
|
|
267
|
+
sent = Date.parse(receipt.server_time);
|
|
268
|
+
signer = await historicalRunnerSigner(owner, owner.device, this.signal);
|
|
269
|
+
nativeSession = signer.nativeSession;
|
|
270
|
+
}
|
|
271
|
+
else
|
|
272
|
+
sent = await localWorldClaimTime(owner, this.signal);
|
|
273
|
+
const base = { job_id: owner.job, attempt_id: owner.attempt, lease_id: owner.lease,
|
|
274
|
+
request_key: owner.request, device_key: owner.device, native_session_id: nativeSession, manifest_sha256: this.manifest,
|
|
275
|
+
session_id: owner.session.session_id, input_sha256: owner.session.input_sha256, command_id: command.id, command_sha256: command.sha256,
|
|
276
|
+
context_sha256: this.context, write_key: randomUUID(), sent_at_ms: sent };
|
|
277
|
+
let payload;
|
|
278
|
+
if (input.kind === 'observation') {
|
|
279
|
+
payload = this.closing ? { ...base, schema: 'firedrill.local-world-closing-command-result.v1', observation: input.observation }
|
|
280
|
+
: { ...base, schema: 'firedrill.local-world-command-result.v1', observation: input.observation };
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const file = { ...base, kind: input.fileKind, evidence_id: input.evidenceId, ...input.action };
|
|
284
|
+
payload = this.closing ? { ...file, schema: 'firedrill.local-world-closing-file-write.v1' }
|
|
285
|
+
: { ...file, schema: 'firedrill.local-world-file-write.v1' };
|
|
286
|
+
}
|
|
287
|
+
const suffix = input.kind === 'file' ? '/files/' + input.fileKind : '/evidence';
|
|
288
|
+
const proof = signer ? await signer.sign(payload) : await signRunnerRequest(owner, owner.device, payload, this.signal);
|
|
289
|
+
return api(localWorldClaimPath(owner) + (this.closing ? '/world-command-history/' : '/world-commands/') + identifier(command.id) + suffix, { actor: owner.owner, workspace: owner.workspace, signal: this.signal, method: 'POST', body: proof });
|
|
290
|
+
}
|
|
291
|
+
inspectReceipt(command, observation, raw) {
|
|
292
|
+
if (!record(raw) || !localWorldIdentityMatches(raw, this.owner, this.manifest)
|
|
293
|
+
|| !exact(raw, [...LOCAL_WORLD_IDENTITY_KEYS, 'schema', 'evidence_id', 'command_id', 'session_id', 'lease_id', 'command_sha256', 'observation_sha256',
|
|
294
|
+
'native_state', 'state', 'required_files', 'files', 'server_time', 'provenance'])
|
|
295
|
+
|| raw.schema !== 'firedrill.local-world-command-evidence-receipt.v1' || typeof raw.evidence_id !== 'string'
|
|
296
|
+
|| identifier(raw.evidence_id) !== raw.evidence_id || raw.command_id !== command.id || raw.session_id !== this.owner.session.session_id
|
|
297
|
+
|| raw.lease_id !== this.owner.lease || raw.command_sha256 !== command.sha256 || raw.observation_sha256 !== digest(observation)
|
|
298
|
+
|| raw.native_state !== observation.state || !['observing', 'awaiting_files', 'finalized', 'unavailable'].includes(String(raw.state))
|
|
299
|
+
|| !record(raw.required_files) || !Array.isArray(raw.files) || raw.files.length > 4 || typeof raw.server_time !== 'string'
|
|
300
|
+
|| !Number.isFinite(Date.parse(raw.server_time)) || raw.provenance !== 'customer_device')
|
|
301
|
+
throw invalid();
|
|
302
|
+
const required = Object.keys(raw.required_files);
|
|
303
|
+
if (FINISHED.includes(observation.state) ? !required.includes('receipt') : required.length !== 0)
|
|
304
|
+
throw invalid();
|
|
305
|
+
if (required.some(kind => !KINDS.includes(kind)))
|
|
306
|
+
throw invalid();
|
|
307
|
+
for (const [kind, plan] of Object.entries(raw.required_files)) {
|
|
308
|
+
if (!record(plan) || !exact(plan, ['size', 'sha256', 'maximum']) || !Number.isSafeInteger(plan.maximum)
|
|
309
|
+
|| Number(plan.maximum) < 1 || Number(plan.maximum) > MAX || kind === 'receipt' && Number(plan.maximum) > 2 * PART
|
|
310
|
+
|| plan.size !== null && (!Number.isSafeInteger(plan.size) || Number(plan.size) < 1 || Number(plan.size) > Number(plan.maximum))
|
|
311
|
+
|| plan.sha256 !== null && (typeof plan.sha256 !== 'string' || !HASH.test(plan.sha256))
|
|
312
|
+
|| (plan.size === null) !== (plan.sha256 === null) || plan.size === null && kind !== 'receipt')
|
|
313
|
+
throw invalid();
|
|
314
|
+
}
|
|
315
|
+
if (raw.state === 'unavailable')
|
|
316
|
+
throw new CliError('local_world_evidence_unavailable', 'The original action has unavailable evidence. Inspect its saved report; do not repeat the action.');
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
for (const file of raw.files) {
|
|
319
|
+
if (!record(file) || !exact(file, ['id', 'kind', 'size', 'sha256', 'state', 'received_parts', 'expires_at'])
|
|
320
|
+
|| typeof file.id !== 'string' || identifier(file.id) !== file.id || !KINDS.includes(file.kind) || seen.has(file.kind)
|
|
321
|
+
|| !raw.required_files[file.kind] || !Number.isSafeInteger(file.size) || file.size < 1 || file.size > MAX
|
|
322
|
+
|| !HASH.test(file.sha256) || !['receiving', 'verifying', 'retained', 'withheld', 'refused', 'expired'].includes(file.state)
|
|
323
|
+
|| !Array.isArray(file.received_parts) || new Set(file.received_parts).size !== file.received_parts.length
|
|
324
|
+
|| file.received_parts.some(index => !Number.isSafeInteger(index) || index < 0 || index >= Math.ceil(file.size / PART))
|
|
325
|
+
|| typeof file.expires_at !== 'string' || !Number.isFinite(Date.parse(file.expires_at)))
|
|
326
|
+
throw invalid();
|
|
327
|
+
const plan = raw.required_files[file.kind];
|
|
328
|
+
if (file.size > plan.maximum || plan.size !== null && file.size !== plan.size
|
|
329
|
+
|| plan.sha256 !== null && file.sha256 !== plan.sha256)
|
|
330
|
+
throw invalid();
|
|
331
|
+
seen.add(file.kind);
|
|
332
|
+
}
|
|
333
|
+
return raw;
|
|
334
|
+
}
|
|
335
|
+
inspectFile(command, evidence, kind, metadata, raw) {
|
|
336
|
+
const count = metadata.parts.length;
|
|
337
|
+
if (!record(raw) || !localWorldIdentityMatches(raw, this.owner, this.manifest)
|
|
338
|
+
|| !exact(raw, [...LOCAL_WORLD_IDENTITY_KEYS, 'schema', 'file_id', 'evidence_id', 'command_id', 'lease_id', 'kind', 'size', 'sha256', 'state',
|
|
339
|
+
'part_bytes', 'received_parts', 'expires_at', 'server_time', 'provenance']) || raw.schema !== 'firedrill.local-world-file-receipt.v1'
|
|
340
|
+
|| typeof raw.file_id !== 'string' || identifier(raw.file_id) !== raw.file_id || raw.evidence_id !== evidence.evidence_id
|
|
341
|
+
|| raw.command_id !== command.id || raw.lease_id !== this.owner.lease || raw.kind !== kind || raw.size !== metadata.size
|
|
342
|
+
|| raw.sha256 !== metadata.sha256 || raw.part_bytes !== PART || !Array.isArray(raw.received_parts)
|
|
343
|
+
|| new Set(raw.received_parts).size !== raw.received_parts.length || raw.received_parts.some(index => !Number.isSafeInteger(index) || index < 0 || index >= count)
|
|
344
|
+
|| !['receiving', 'verifying', 'retained', 'withheld', 'refused', 'expired'].includes(String(raw.state)) || raw.provenance !== 'customer_device'
|
|
345
|
+
|| typeof raw.server_time !== 'string' || !Number.isFinite(Date.parse(raw.server_time)) || typeof raw.expires_at !== 'string'
|
|
346
|
+
|| !Number.isFinite(Date.parse(raw.expires_at)) || ['verifying', 'retained', 'withheld'].includes(String(raw.state)) && raw.received_parts.length !== count)
|
|
347
|
+
throw invalid();
|
|
348
|
+
if (['refused', 'expired'].includes(String(raw.state)))
|
|
349
|
+
throw new CliError('local_world_file_unavailable', 'The original command file is unavailable. No action was repeated.');
|
|
350
|
+
return raw;
|
|
351
|
+
}
|
|
352
|
+
checkMeta(metadata, plan) {
|
|
353
|
+
if (!record(metadata) || !exact(metadata, ['size', 'sha256', 'parts']) || !Number.isSafeInteger(metadata.size)
|
|
354
|
+
|| Number(metadata.size) < 1 || Number(metadata.size) > plan.maximum || typeof metadata.sha256 !== 'string' || !HASH.test(metadata.sha256)
|
|
355
|
+
|| plan.size !== null && metadata.size !== plan.size || plan.sha256 !== null && metadata.sha256 !== plan.sha256
|
|
356
|
+
|| !Array.isArray(metadata.parts) || metadata.parts.length !== Math.ceil(Number(metadata.size) / PART))
|
|
357
|
+
throw invalid();
|
|
358
|
+
for (let index = 0; index < metadata.parts.length; index++) {
|
|
359
|
+
const part = metadata.parts[index];
|
|
360
|
+
if (!record(part) || !exact(part, ['size', 'sha256']) || part.size !== Math.min(PART, Number(metadata.size) - index * PART)
|
|
361
|
+
|| typeof part.sha256 !== 'string' || !HASH.test(part.sha256))
|
|
362
|
+
throw invalid();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async collect(command, kind, plan) {
|
|
366
|
+
const prefix = name(command, kind), cached = await this.journal.get(prefix + '-metadata');
|
|
367
|
+
if (cached) {
|
|
368
|
+
try {
|
|
369
|
+
const metadata = JSON.parse(cached.toString('utf8'));
|
|
370
|
+
this.checkMeta(metadata, plan);
|
|
371
|
+
return metadata;
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
cached.fill(0);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (this.closing || !this.channel)
|
|
378
|
+
throw new CliError('local_world_file_unavailable', 'These original command bytes are not in the saved local journal. No agent or native command was restarted.');
|
|
379
|
+
const file = await this.channel.openFile(command.id, kind), hash = createHash('sha256'), parts = [];
|
|
380
|
+
try {
|
|
381
|
+
if (file.size > plan.maximum || plan.size !== null && file.size !== plan.size || plan.sha256 !== null && file.sha256 !== plan.sha256)
|
|
382
|
+
throw invalid();
|
|
383
|
+
for (let offset = 0, index = 0; offset < file.size; offset += PART, index++) {
|
|
384
|
+
const body = Buffer.alloc(Math.min(PART, file.size - offset));
|
|
385
|
+
try {
|
|
386
|
+
for (let position = 0; position < body.length; position += CHUNK) {
|
|
387
|
+
this.signal.throwIfAborted();
|
|
388
|
+
const chunk = await file.read(offset + position, Math.min(CHUNK, body.length - position));
|
|
389
|
+
try {
|
|
390
|
+
chunk.copy(body, position);
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
chunk.fill(0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
hash.update(body);
|
|
397
|
+
parts.push({ size: body.length, sha256: sha(body) });
|
|
398
|
+
await this.journal.put(prefix + '-part-' + index, body);
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
body.fill(0);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (hash.digest('hex') !== file.sha256)
|
|
405
|
+
throw invalid();
|
|
406
|
+
const metadata = { size: file.size, sha256: file.sha256, parts };
|
|
407
|
+
this.checkMeta(metadata, plan);
|
|
408
|
+
const raw = Buffer.from(JSON.stringify(metadata));
|
|
409
|
+
try {
|
|
410
|
+
await this.journal.put(prefix + '-metadata', raw);
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
raw.fill(0);
|
|
414
|
+
}
|
|
415
|
+
return metadata;
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
await file.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async transfer(command, evidence, kind, plan) {
|
|
422
|
+
const metadata = await this.collect(command, kind, plan);
|
|
423
|
+
const request = async (action) => this.inspectFile(command, evidence, kind, metadata, await this.request(command, { kind: 'file', fileKind: kind, evidenceId: evidence.evidence_id, action }));
|
|
424
|
+
let file = await request({ action: 'register', size: metadata.size, sha256: metadata.sha256 });
|
|
425
|
+
if (file.state === 'receiving') {
|
|
426
|
+
for (let index = 0; index < metadata.parts.length; index++) {
|
|
427
|
+
if (file.received_parts.includes(index))
|
|
428
|
+
continue;
|
|
429
|
+
const body = await this.journal.get(name(command, kind) + '-part-' + index), part = metadata.parts[index];
|
|
430
|
+
if (!body)
|
|
431
|
+
throw invalid();
|
|
432
|
+
try {
|
|
433
|
+
if (body.length !== part.size || sha(body) !== part.sha256)
|
|
434
|
+
throw invalid();
|
|
435
|
+
file = await request({ action: 'chunk', index, size: body.length, sha256: part.sha256, content_base64: body.toString('base64') });
|
|
436
|
+
if (!file.received_parts.includes(index))
|
|
437
|
+
throw invalid();
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
body.fill(0);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
file = await request({ action: 'commit' });
|
|
444
|
+
}
|
|
445
|
+
while (file.state === 'verifying') {
|
|
446
|
+
await sleep(1000, undefined, { signal: this.signal }); // Real worker retention, never simulation progress.
|
|
447
|
+
file = await request({ action: 'status' });
|
|
448
|
+
}
|
|
449
|
+
if (!['retained', 'withheld'].includes(file.state))
|
|
450
|
+
throw invalid();
|
|
451
|
+
}
|
|
452
|
+
async retain(command, supplied) {
|
|
453
|
+
await this.remember(command);
|
|
454
|
+
this.inspectObservation(command, supplied);
|
|
455
|
+
const saved = await this.saved(command), observation = saved ?? supplied;
|
|
456
|
+
if (saved && FINISHED.includes(supplied.state) && digest(saved) !== digest(supplied))
|
|
457
|
+
throw invalid();
|
|
458
|
+
const raw = Buffer.from(JSON.stringify(observation));
|
|
459
|
+
try {
|
|
460
|
+
if (raw.length > (['app_save', 'paired_save'].includes(command.kind) ? 2 * PART : 65536))
|
|
461
|
+
throw invalid();
|
|
462
|
+
await this.journal.put(name(command, FINISHED.includes(observation.state) ? 'terminal' : 'observation'), raw, !FINISHED.includes(observation.state));
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
raw.fill(0);
|
|
466
|
+
}
|
|
467
|
+
const publish = async () => this.inspectReceipt(command, observation, await this.request(command, { kind: 'observation', observation }));
|
|
468
|
+
let evidence = await publish();
|
|
469
|
+
if (evidence.state === 'awaiting_files') {
|
|
470
|
+
for (const kind of KINDS)
|
|
471
|
+
if (evidence.required_files[kind])
|
|
472
|
+
await this.transfer(command, evidence, kind, evidence.required_files[kind]);
|
|
473
|
+
evidence = await publish();
|
|
474
|
+
if (evidence.state !== 'finalized')
|
|
475
|
+
throw invalid();
|
|
476
|
+
}
|
|
477
|
+
const receipt = Buffer.from(JSON.stringify(evidence));
|
|
478
|
+
try {
|
|
479
|
+
await this.journal.put(name(command, 'cloud-receipt'), receipt, true);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
receipt.fill(0);
|
|
483
|
+
}
|
|
484
|
+
return observation;
|
|
485
|
+
}
|
|
486
|
+
close() { return this.journal.close(); }
|
|
487
|
+
}
|