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,289 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CliError } from './http.js';
|
|
3
|
+
const CHUNK = 16384, HASH = /^[a-f0-9]{64}$/;
|
|
4
|
+
const EVENTS = new Set(['trial.preparing', 'trial.world_unavailable', 'trial.execution_receipt', 'trial.elapsed', 'runner.started',
|
|
5
|
+
'driver.ready', 'driver.ingress_observed', 'driver.action_started', 'driver.action_finished',
|
|
6
|
+
'task.progress', 'conversation.progress', 'voice.progress', 'browser.capture_progress', 'desktop.capture_progress', 'dependency.request', 'dependency.response',
|
|
7
|
+
'world.time.started', 'world.time.advanced', 'world.time.finished', 'world.continuity.started', 'world.agent.observed', 'world.interaction.observed',
|
|
8
|
+
'world.event.intent', 'world.event.observed', 'world.continuity.finished', 'world.control.observed']);
|
|
9
|
+
const fail = () => new CliError('local_trial_evidence_invalid', 'The saved drill evidence could not be confirmed. No target was restarted.');
|
|
10
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const exact = (value, keys) => Object.keys(value).sort().join(',') === keys;
|
|
12
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const offset = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
14
|
+
const originalReports = new WeakMap();
|
|
15
|
+
/** Retain the controller's exact committed JSON bytes. Re-encoding parsed JSON
|
|
16
|
+
* in JS can change integer precision or Python's finite numeric representation
|
|
17
|
+
* and would invalidate an otherwise identical live/final event digest. */
|
|
18
|
+
export function trialReportBytes(value) {
|
|
19
|
+
const body = originalReports.get(value);
|
|
20
|
+
if (!body)
|
|
21
|
+
throw fail();
|
|
22
|
+
return Buffer.from(body);
|
|
23
|
+
}
|
|
24
|
+
export function trialStatus(value) {
|
|
25
|
+
if (value === null)
|
|
26
|
+
return;
|
|
27
|
+
if (!object(value) || (value.state === 'failed' ? !exact(value, 'code,state') || typeof value.code !== 'string'
|
|
28
|
+
|| !/^[a-z][a-z0-9_]{0,127}$/.test(value.code) : !exact(value, 'state') || !['executing', 'finished'].includes(String(value.state))))
|
|
29
|
+
throw fail();
|
|
30
|
+
}
|
|
31
|
+
export function inspectTrialEvent(value, execution, sequence) {
|
|
32
|
+
if (!object(value) || !exact(value, 'at,content,execution_id,kind,sequence') || value.sequence !== sequence
|
|
33
|
+
|| !offset(value.sequence) || value.execution_id !== execution || !EVENTS.has(String(value.kind))
|
|
34
|
+
|| typeof value.at !== 'string' || !Number.isFinite(Date.parse(value.at)) || !object(value.content))
|
|
35
|
+
throw fail();
|
|
36
|
+
if (value.kind === 'task.progress') {
|
|
37
|
+
const content = value.content;
|
|
38
|
+
const counts = ['poll_requests', 'responses', 'retained_responses', 'omitted_responses'];
|
|
39
|
+
if (!exact(content, 'accepted_http_status,completion_status_observed,last_http_status,omitted_responses,poll_requests,reason,responses,retained_responses,schema,state')
|
|
40
|
+
|| content.schema !== 'firedrill.http-task-progress.v1'
|
|
41
|
+
|| !['pending', 'completed', 'failed', 'unconfirmed', 'rejected'].includes(String(content.state))
|
|
42
|
+
|| !['', 'acceptance_unconfirmed', 'identifier_unavailable', 'status_unavailable', 'request_unavailable', 'deadline', 'canceled', 'evidence_unavailable'].includes(String(content.reason))
|
|
43
|
+
|| counts.some(key => !offset(content[key]))
|
|
44
|
+
|| Number(content.responses) !== Number(content.retained_responses) + Number(content.omitted_responses)
|
|
45
|
+
|| Number(content.responses) > Number(content.poll_requests) + 1
|
|
46
|
+
|| [content.accepted_http_status, content.last_http_status].some(status => status !== null && (!Number.isInteger(status) || Number(status) < 200 || Number(status) > 599))
|
|
47
|
+
|| content.accepted_http_status !== null && Number(content.accepted_http_status) > 299
|
|
48
|
+
|| typeof content.completion_status_observed !== 'boolean'
|
|
49
|
+
|| content.completion_status_observed !== ['completed', 'failed'].includes(String(content.state))
|
|
50
|
+
|| ['completed', 'failed', 'pending'].includes(String(content.state)) && content.reason !== ''
|
|
51
|
+
|| ['completed', 'failed'].includes(String(content.state)) && (content.accepted_http_status === null
|
|
52
|
+
|| Number(content.poll_requests) < 1 || Number(content.responses) < 2 || content.last_http_status === null
|
|
53
|
+
|| Number(content.last_http_status) > 299)
|
|
54
|
+
|| content.state === 'pending' && (content.accepted_http_status === null || Number(content.responses) < 1)
|
|
55
|
+
|| content.state === 'rejected' && (content.accepted_http_status !== null || content.poll_requests !== 0
|
|
56
|
+
|| content.responses !== 1 || content.last_http_status === null || Number(content.last_http_status) < 400 || Number(content.last_http_status) > 499))
|
|
57
|
+
throw fail();
|
|
58
|
+
}
|
|
59
|
+
if (value.kind === 'voice.progress') {
|
|
60
|
+
const content = value.content, stream = content.message_stream;
|
|
61
|
+
if (!exact(content, 'media_observed,message_stream,messages,omitted_messages,schema,state')
|
|
62
|
+
|| content.schema !== 'firedrill.voice-progress.v1' || !['active', 'closed', 'interrupted'].includes(String(content.state))
|
|
63
|
+
|| typeof content.media_observed !== 'boolean' || !offset(content.messages) || !offset(content.omitted_messages)
|
|
64
|
+
|| Number(content.messages) + Number(content.omitted_messages) > 100000 || !object(stream)
|
|
65
|
+
|| !exact(stream, 'attempt_number,id') || typeof stream.id !== 'string'
|
|
66
|
+
|| !/^[a-f0-9]{8}-[a-f0-9]{4}-5[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(stream.id)
|
|
67
|
+
|| !Number.isSafeInteger(stream.attempt_number) || Number(stream.attempt_number) < 1 || Number(stream.attempt_number) > 2147483647)
|
|
68
|
+
throw fail();
|
|
69
|
+
// Cloud validates the execution-derived UUID and current exact attempt.
|
|
70
|
+
// This transport parser alone never establishes invocation or compliance.
|
|
71
|
+
}
|
|
72
|
+
if (value.kind === 'world.interaction.observed') {
|
|
73
|
+
const content = value.content;
|
|
74
|
+
const uuid = (one) => typeof one === 'string' && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(one);
|
|
75
|
+
if (!exact(content, 'basis,cause_event_key,inputs,message_stream_id,plan_sha256,request_id,responses,sequence,state')
|
|
76
|
+
|| content.basis !== 'original_conversation_input_coordinator' || typeof content.plan_sha256 !== 'string' || !HASH.test(content.plan_sha256)
|
|
77
|
+
|| !['active', 'waiting_for_input', 'closed'].includes(String(content.state))
|
|
78
|
+
|| !Number.isSafeInteger(content.sequence) || Number(content.sequence) < 1 || Number(content.sequence) > 200001
|
|
79
|
+
|| [content.inputs, content.responses].some(one => !offset(one) || Number(one) > 100000)
|
|
80
|
+
|| Number(content.responses) > Number(content.inputs) || Number(content.inputs) > Number(content.responses) + 1
|
|
81
|
+
|| content.state === 'active' && Number(content.inputs) !== Number(content.responses) + 1
|
|
82
|
+
|| content.state === 'waiting_for_input' && (content.inputs === 0 || content.inputs !== content.responses)
|
|
83
|
+
|| !uuid(content.message_stream_id) || !(uuid(content.request_id) || content.request_id === null && content.inputs === 0 && content.state === 'closed')
|
|
84
|
+
|| content.cause_event_key !== null && (typeof content.cause_event_key !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(content.cause_event_key)))
|
|
85
|
+
throw fail();
|
|
86
|
+
// Central admission checks exact attempt-derived stream/cause identities,
|
|
87
|
+
// original accepted plan, retained progress and phase transition ordering.
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
/** Committed controller records only. Model frames may be interleaved. Files
|
|
92
|
+
* stream to a private disk sink, never to an unbounded in-memory accumulator. */
|
|
93
|
+
export function liveEventReader(operation, execution, scenario, retain, retainArtifact) {
|
|
94
|
+
let pending = null;
|
|
95
|
+
let file = null;
|
|
96
|
+
let after = 0, artifactAfter = 0;
|
|
97
|
+
return { get pending() { return pending !== null || file !== null; }, close: async () => {
|
|
98
|
+
if (file) {
|
|
99
|
+
await file.writer.abort();
|
|
100
|
+
file = null;
|
|
101
|
+
}
|
|
102
|
+
pending = null;
|
|
103
|
+
}, accept: async (frame) => {
|
|
104
|
+
if (frame.schema !== 'firedrill.local-execution-frame.v1' || frame.id !== operation)
|
|
105
|
+
throw fail();
|
|
106
|
+
if (frame.kind === 'live_artifact') {
|
|
107
|
+
if (pending || file || !exact(frame, 'artifact,id,kind,schema'))
|
|
108
|
+
throw fail();
|
|
109
|
+
artifact(frame.artifact, execution, scenario);
|
|
110
|
+
const metadata = { ...frame.artifact };
|
|
111
|
+
if (metadata.id !== artifactAfter + '.bin' || metadata.kind === 'result')
|
|
112
|
+
throw fail();
|
|
113
|
+
file = { metadata, bytes: 0, sequence: 0, digest: createHash('sha256'), writer: await retainArtifact(metadata) };
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (file) {
|
|
117
|
+
if (frame.kind === 'live_artifact_chunk') {
|
|
118
|
+
if (!exact(frame, 'data,id,kind,schema,sequence') || frame.sequence !== file.sequence
|
|
119
|
+
|| typeof frame.data !== 'string' || !frame.data.length || frame.data.length > 4 * Math.ceil(CHUNK / 3))
|
|
120
|
+
throw fail();
|
|
121
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
122
|
+
if (!bytes.length || bytes.length > CHUNK || bytes.toString('base64') !== frame.data
|
|
123
|
+
|| file.bytes + bytes.length > file.metadata.size)
|
|
124
|
+
throw fail();
|
|
125
|
+
await file.writer.write(bytes);
|
|
126
|
+
file.digest.update(bytes);
|
|
127
|
+
file.bytes += bytes.length;
|
|
128
|
+
file.sequence++;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (frame.kind !== 'live_artifact_end' || !exact(frame, 'id,kind,schema,sequence')
|
|
132
|
+
|| frame.sequence !== file.sequence || file.bytes !== file.metadata.size
|
|
133
|
+
|| file.digest.digest('hex') !== file.metadata.sha256)
|
|
134
|
+
throw fail();
|
|
135
|
+
await file.writer.commit();
|
|
136
|
+
file = null;
|
|
137
|
+
artifactAfter++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (frame.kind === 'live_event') {
|
|
141
|
+
if (pending || !exact(frame, 'id,kind,schema,sha256,size') || !offset(frame.size) || !frame.size
|
|
142
|
+
|| frame.size > 2 * 1024 ** 2 || typeof frame.sha256 !== 'string' || !HASH.test(frame.sha256))
|
|
143
|
+
throw fail();
|
|
144
|
+
pending = { size: frame.size, sha256: frame.sha256, bytes: 0, sequence: 0, chunks: [] };
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!pending)
|
|
148
|
+
throw fail();
|
|
149
|
+
if (frame.kind === 'live_event_chunk') {
|
|
150
|
+
if (!exact(frame, 'data,id,kind,schema,sequence') || frame.sequence !== pending.sequence
|
|
151
|
+
|| typeof frame.data !== 'string' || !frame.data.length || frame.data.length > 4 * Math.ceil(CHUNK / 3))
|
|
152
|
+
throw fail();
|
|
153
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
154
|
+
if (!bytes.length || bytes.length > CHUNK || bytes.toString('base64') !== frame.data || pending.bytes + bytes.length > pending.size)
|
|
155
|
+
throw fail();
|
|
156
|
+
pending.chunks.push(bytes);
|
|
157
|
+
pending.bytes += bytes.length;
|
|
158
|
+
pending.sequence++;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (frame.kind !== 'live_event_end' || !exact(frame, 'id,kind,schema,sequence')
|
|
162
|
+
|| frame.sequence !== pending.sequence || pending.bytes !== pending.size)
|
|
163
|
+
throw fail();
|
|
164
|
+
const bytes = Buffer.concat(pending.chunks, pending.size);
|
|
165
|
+
if (hash(bytes) !== pending.sha256)
|
|
166
|
+
throw fail();
|
|
167
|
+
let value;
|
|
168
|
+
try {
|
|
169
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw fail();
|
|
173
|
+
}
|
|
174
|
+
const event = inspectTrialEvent(value, execution, after);
|
|
175
|
+
if (event.kind === 'trial.execution_receipt')
|
|
176
|
+
throw fail();
|
|
177
|
+
await retain(bytes, event);
|
|
178
|
+
pending = null;
|
|
179
|
+
after++;
|
|
180
|
+
} };
|
|
181
|
+
}
|
|
182
|
+
function artifact(value, execution, scenario) {
|
|
183
|
+
if (!object(value) || !exact(value, 'execution_id,id,kind,scenario_key,sha256,size')
|
|
184
|
+
|| typeof value.id !== 'string' || !/^(0|[1-9][0-9]{0,9})\.bin$/.test(value.id)
|
|
185
|
+
|| !['transcript', 'tool_trace', 'result', 'state_snapshot', 'browser_capture', 'desktop_capture', 'trace', 'dataset_application', 'recording_combined', 'recording_stereo', 'recording_customer', 'recording_assistant', 'recording_segment'].includes(String(value.kind))
|
|
186
|
+
|| !offset(value.size) || value.size > 512 * 1024 ** 2 || typeof value.sha256 !== 'string' || !HASH.test(value.sha256)
|
|
187
|
+
|| value.execution_id !== execution || value.scenario_key !== scenario)
|
|
188
|
+
throw fail();
|
|
189
|
+
}
|
|
190
|
+
export { artifact as inspectTrialArtifact };
|
|
191
|
+
export function inspectTrialReport(value, operation, execution, scenario, launch, after, artifactAfter) {
|
|
192
|
+
if (!object(value) || !exact(value, 'artifact_offset,artifacts,artifacts_more,events,events_more,execution_id,launch,offset,schema,status')
|
|
193
|
+
|| value.schema !== 'firedrill.trial-progress.v1' || value.execution_id !== execution
|
|
194
|
+
|| !object(value.launch) || value.launch.launch_id !== operation || value.launch.state !== launch.state
|
|
195
|
+
|| !exact(value.launch, launch.state === 'exited' ? 'exit_code,launch_id,state' : 'launch_id,state')
|
|
196
|
+
|| value.launch.exit_code !== launch.exit_code || !Array.isArray(value.events) || value.events.length > 1
|
|
197
|
+
|| !Array.isArray(value.artifacts) || value.artifacts.length > 1 || value.offset !== after + value.events.length
|
|
198
|
+
|| value.artifact_offset !== artifactAfter + value.artifacts.length
|
|
199
|
+
|| typeof value.events_more !== 'boolean' || typeof value.artifacts_more !== 'boolean'
|
|
200
|
+
|| value.events_more && !value.events.length || value.artifacts_more && !value.artifacts.length)
|
|
201
|
+
throw fail();
|
|
202
|
+
trialStatus(value.status);
|
|
203
|
+
for (const item of value.events)
|
|
204
|
+
inspectTrialEvent(item, execution, after);
|
|
205
|
+
for (const item of value.artifacts) {
|
|
206
|
+
artifact(item, execution, scenario);
|
|
207
|
+
if (item.id !== artifactAfter + '.bin')
|
|
208
|
+
throw fail();
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
/** Fixed post-launch journal reader. No arbitrary commands, filenames or URLs. */
|
|
213
|
+
export function trialReader(operation, execution, scenario, launch, emit, reply) {
|
|
214
|
+
let reading = false, epoch = 0;
|
|
215
|
+
const exclusive = async (work) => {
|
|
216
|
+
if (reading)
|
|
217
|
+
throw fail();
|
|
218
|
+
reading = true;
|
|
219
|
+
try {
|
|
220
|
+
return await work();
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
reading = false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
report: (after, artifactAfter) => exclusive(async () => {
|
|
228
|
+
if (!offset(after) || !offset(artifactAfter))
|
|
229
|
+
throw fail();
|
|
230
|
+
await emit('trial_report', { after, artifact_after: artifactAfter });
|
|
231
|
+
const header = await reply('trial_report');
|
|
232
|
+
if (!exact(header, 'id,kind,schema,sha256,size') || !offset(header.size) || !header.size || header.size > 4 * 1024 ** 2
|
|
233
|
+
|| typeof header.sha256 !== 'string' || !HASH.test(header.sha256))
|
|
234
|
+
throw fail();
|
|
235
|
+
const parts = [];
|
|
236
|
+
let length = 0, sequence = 0;
|
|
237
|
+
while (length < header.size) {
|
|
238
|
+
const frame = await reply('report_chunk');
|
|
239
|
+
if (!exact(frame, 'data,id,kind,schema,sequence') || frame.sequence !== sequence++ || typeof frame.data !== 'string'
|
|
240
|
+
|| frame.data.length > 4 * Math.ceil(CHUNK / 3))
|
|
241
|
+
throw fail();
|
|
242
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
243
|
+
if (!bytes.length || bytes.length > CHUNK || bytes.toString('base64') !== frame.data || length + bytes.length > header.size)
|
|
244
|
+
throw fail();
|
|
245
|
+
parts.push(bytes);
|
|
246
|
+
length += bytes.length;
|
|
247
|
+
}
|
|
248
|
+
const end = await reply('report_end'), body = Buffer.concat(parts);
|
|
249
|
+
if (!exact(end, 'id,kind,schema,sequence') || end.sequence !== sequence || hash(body) !== header.sha256)
|
|
250
|
+
throw fail();
|
|
251
|
+
let value;
|
|
252
|
+
try {
|
|
253
|
+
value = JSON.parse(body.toString('utf8'));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
throw fail();
|
|
257
|
+
}
|
|
258
|
+
const report = inspectTrialReport(value, operation, execution, scenario, launch, after, artifactAfter);
|
|
259
|
+
originalReports.set(report, body);
|
|
260
|
+
return report;
|
|
261
|
+
}),
|
|
262
|
+
artifact: metadata => exclusive(async () => {
|
|
263
|
+
artifact(metadata, execution, scenario);
|
|
264
|
+
metadata = { ...metadata };
|
|
265
|
+
const current = ++epoch; // Invalidate earlier readers before selection.
|
|
266
|
+
await emit('trial_artifact', { artifact: metadata.id });
|
|
267
|
+
const frame = await reply('trial_artifact');
|
|
268
|
+
if (!exact(frame, 'artifact,id,kind,schema'))
|
|
269
|
+
throw fail();
|
|
270
|
+
artifact(frame.artifact, execution, scenario);
|
|
271
|
+
const actual = frame.artifact;
|
|
272
|
+
if (Object.keys(metadata).some(key => metadata[key] !== actual[key]))
|
|
273
|
+
throw fail();
|
|
274
|
+
return { read: (position, length) => exclusive(async () => {
|
|
275
|
+
if (current !== epoch || !offset(position) || !offset(length) || length < 1 || length > CHUNK || position + length > metadata.size)
|
|
276
|
+
throw fail();
|
|
277
|
+
await emit('export_chunk', { offset: position, length });
|
|
278
|
+
const row = await reply('export_chunk');
|
|
279
|
+
if (!exact(row, 'data,id,kind,length,offset,schema,sha256') || row.offset !== position || row.length !== length
|
|
280
|
+
|| typeof row.data !== 'string' || row.data.length > 4 * Math.ceil(CHUNK / 3))
|
|
281
|
+
throw fail();
|
|
282
|
+
const bytes = Buffer.from(row.data, 'base64');
|
|
283
|
+
if (bytes.length !== length || bytes.toString('base64') !== row.data || hash(bytes) !== row.sha256)
|
|
284
|
+
throw fail();
|
|
285
|
+
return bytes;
|
|
286
|
+
}) };
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, rename } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { privateDirectory } from './credentials.js';
|
|
6
|
+
import { CliError } from './http.js';
|
|
7
|
+
import { identifier } from './world-contracts.js';
|
|
8
|
+
import { localTrialCollection } from './runner-trial-journal.js';
|
|
9
|
+
import { synchronizeTrialEvidence as synchronize, trialTransferKey as transferKey } from './runner-trial-transfer.js';
|
|
10
|
+
import { acquireLocalEvidence, assertEvidenceRetentionCurrent, evidenceRetentionSignal } from './runner-evidence-retention.js';
|
|
11
|
+
/** Explicit transfer-only recovery of the exact journal. No Docker/target/model
|
|
12
|
+
* invocation, lease renewal, new transfer identity or campaign outcome inference.
|
|
13
|
+
*/
|
|
14
|
+
export async function syncLocalTrialEvidence(scope, operation, signal, output) {
|
|
15
|
+
operation = identifier(operation);
|
|
16
|
+
const evidence = await acquireLocalEvidence(scope, operation);
|
|
17
|
+
try {
|
|
18
|
+
return await synchronizeRetainedTrial(scope, operation, AbortSignal.any([signal, evidence.signal]), evidence.retention, output);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await evidence.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function synchronizeRetainedTrial(scope, operation, signal, retention, output) {
|
|
25
|
+
signal = evidenceRetentionSignal(retention, signal);
|
|
26
|
+
const collection = await localTrialCollection(scope, operation, retention), retained = collection.journal, owner = retained.owner;
|
|
27
|
+
if (!owner.evidenceUpload)
|
|
28
|
+
return { state: 'local_only', provenance: 'customer_device', cloud_completion_recorded: false };
|
|
29
|
+
const directory = join(await privateDirectory(), `trial-${identifier(scope.owner)}-${identifier(scope.workspace)}-${operation}`);
|
|
30
|
+
const lock = join(directory, 'cloud-sync.lock');
|
|
31
|
+
// All new sync ownership is the outer kernel lock. Preserve ambiguous old
|
|
32
|
+
// directory locks; no PID/time-based takeover or silent removal is allowed.
|
|
33
|
+
if (await lstat(lock).then(() => true).catch(error => { if (error.code === 'ENOENT')
|
|
34
|
+
return false; throw error; }))
|
|
35
|
+
throw new CliError('local_evidence_legacy_lock', 'This older drill transfer has an ambiguous cloud-sync.lock. Inspect that exact legacy lock before continuing; it was not removed.');
|
|
36
|
+
// This is one reconciliation pass, not a catalogue of all prior receipts.
|
|
37
|
+
// Every original deterministic transfer is still read back and every local
|
|
38
|
+
// byte is checked. Restarting synchronization never restarts the target.
|
|
39
|
+
const journal = { schema: 'firedrill.local-trial-sync.v2', operation_id: operation, job_id: owner.job,
|
|
40
|
+
manifest_sha256: retained.manifest_sha256, state: 'synchronizing', reports_retained: 0, artifacts_retained: 0,
|
|
41
|
+
last_transfer: null, pending: null, cloud_completion_recorded: false };
|
|
42
|
+
const persist = async () => {
|
|
43
|
+
assertEvidenceRetentionCurrent(retention);
|
|
44
|
+
const target = join(directory, 'cloud-receipts.json'), temporary = target + '.' + randomUUID() + '.tmp', file = await open(temporary, 'wx', 0o600);
|
|
45
|
+
try {
|
|
46
|
+
await file.writeFile(JSON.stringify(journal));
|
|
47
|
+
await file.sync();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await file.close();
|
|
51
|
+
}
|
|
52
|
+
await rename(temporary, target);
|
|
53
|
+
const parent = await open(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
54
|
+
try {
|
|
55
|
+
await parent.sync();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await parent.close();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
{
|
|
62
|
+
await persist();
|
|
63
|
+
const active = AbortSignal.any([signal, AbortSignal.timeout(30 * 60 * 1000)]);
|
|
64
|
+
let after = 0, artifactAfter = 0, unavailable = 0;
|
|
65
|
+
let notified = 0;
|
|
66
|
+
const progress = (force = false) => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (!force && now - notified < 1000)
|
|
69
|
+
return;
|
|
70
|
+
notified = now;
|
|
71
|
+
output?.('local_trial_sync_progress', { operation_id: operation, job_id: owner.job,
|
|
72
|
+
reports_retained: journal.reports_retained, artifacts_retained: journal.artifacts_retained,
|
|
73
|
+
unavailable_artifacts: unavailable, state: journal.state, provenance: 'customer_device', cloud_completion_recorded: false }, `Cloud receipts checked: ${journal.reports_retained} report pages, ${journal.artifacts_retained} evidence files.`);
|
|
74
|
+
};
|
|
75
|
+
progress(true);
|
|
76
|
+
const pending = async (value) => { journal.pending = value; await persist(); };
|
|
77
|
+
for await (const page of collection.pages(active)) {
|
|
78
|
+
const document = { kind: 'trial_report', receipt: { size: page.size, sha256: page.sha256 },
|
|
79
|
+
position: page.position, after, artifact_after: artifactAfter, launch: retained.observation.launch, status: retained.observation.status };
|
|
80
|
+
const key = transferKey(operation, 'report:' + page.position);
|
|
81
|
+
const artifact = await synchronize(owner, retained.manifest_sha256, key, document, async (offset, length) => { assertEvidenceRetentionCurrent(retention); return page.bytes.subarray(offset, offset + length); }, active, pending);
|
|
82
|
+
journal.last_transfer = { key, artifact };
|
|
83
|
+
journal.reports_retained++;
|
|
84
|
+
await persist();
|
|
85
|
+
progress();
|
|
86
|
+
after = page.report.offset;
|
|
87
|
+
artifactAfter = page.report.artifact_offset;
|
|
88
|
+
for (const metadata of page.report.artifacts) {
|
|
89
|
+
if (Number(metadata.id.split('.')[0]) >= retained.artifacts_retained) {
|
|
90
|
+
unavailable++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
await collection.artifact(metadata, async (read) => {
|
|
94
|
+
const key = transferKey(operation, 'artifact:' + metadata.id);
|
|
95
|
+
const artifact = await synchronize(owner, retained.manifest_sha256, key, { kind: 'trial_artifact', receipt: { size: metadata.size, sha256: metadata.sha256 }, metadata,
|
|
96
|
+
launch: retained.observation.launch, status: retained.observation.status }, read, active, pending);
|
|
97
|
+
journal.last_transfer = { key, artifact };
|
|
98
|
+
journal.artifacts_retained++;
|
|
99
|
+
await persist();
|
|
100
|
+
progress();
|
|
101
|
+
}, active);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
journal.state = retained.stage === 'local_collected' && unavailable === 0 ? 'cloud_staged' : 'cloud_staged_partial';
|
|
105
|
+
await persist();
|
|
106
|
+
progress(true);
|
|
107
|
+
return { state: journal.state, transfers: journal.reports_retained + journal.artifacts_retained, unavailable_artifacts: unavailable,
|
|
108
|
+
provenance: 'customer_device', cloud_completion_recorded: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } 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 { historicalRunnerSigner } from './runner-identity.js';
|
|
6
|
+
import { identifier } from './world-contracts.js';
|
|
7
|
+
import { localEvidenceReceiptMatches } from '../../contracts/local-evidence-contracts.js';
|
|
8
|
+
const CHUNK = 1024 ** 2;
|
|
9
|
+
const hash = (body) => createHash('sha256').update(body).digest('hex');
|
|
10
|
+
const fail = () => new CliError('local_trial_sync_unconfirmed', 'Drill evidence is not confirmed in the cloud. Its exact private journal remains; no target was run again.');
|
|
11
|
+
function canonical(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return '[' + value.map(canonical).join(',') + ']';
|
|
14
|
+
if (value && typeof value === 'object')
|
|
15
|
+
return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}';
|
|
16
|
+
const body = JSON.stringify(value);
|
|
17
|
+
if (body === undefined)
|
|
18
|
+
throw fail();
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
export function trialTransferKey(operation, name) {
|
|
22
|
+
const bytes = createHash('sha256').update('firedrill.local-trial-transfer.v1\0' + operation + '\0' + name).digest().subarray(0, 16);
|
|
23
|
+
bytes[6] = (bytes[6] & 15) | 128;
|
|
24
|
+
bytes[8] = (bytes[8] & 63) | 128;
|
|
25
|
+
const hex = bytes.toString('hex');
|
|
26
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
27
|
+
}
|
|
28
|
+
function match(receipt, owner, key, document, manifest) {
|
|
29
|
+
if (!localEvidenceReceiptMatches(receipt, { ...owner, manifestSha256: manifest }, key)
|
|
30
|
+
|| receipt.schema !== 'firedrill.local-trial-evidence-receipt.v1' || receipt.job_id !== owner.job
|
|
31
|
+
|| receipt.transfer_key !== key || receipt.lease_id !== owner.lease || receipt.provenance !== 'customer_device'
|
|
32
|
+
|| receipt.cloud_completion_recorded !== false || !Array.isArray(receipt.chunks)
|
|
33
|
+
|| !['not_retained', 'receiving', 'verifying', 'retained'].includes(receipt.state)
|
|
34
|
+
|| receipt.state !== 'not_retained' && (receipt.request_digest !== hash(canonical(document)) || canonical(receipt.document) !== canonical(document)))
|
|
35
|
+
throw fail();
|
|
36
|
+
if (receipt.state !== 'retained' && (receipt.upload_available !== true || !Number.isFinite(Date.parse(receipt.server_time))
|
|
37
|
+
|| !Number.isFinite(Date.parse(receipt.upload_until)) || Date.parse(receipt.upload_until) <= Date.parse(receipt.server_time)))
|
|
38
|
+
throw fail();
|
|
39
|
+
}
|
|
40
|
+
/** One exact resumable file intent. Live retention is not a closing declaration.
|
|
41
|
+
* An uncertain write is reconciled by GET only. The caller owns durable bytes,
|
|
42
|
+
* current consent, bounded cancellation and queue state, never a target retry. */
|
|
43
|
+
export async function synchronizeTrialEvidence(owner, manifest, key, document, read, signal, pending) {
|
|
44
|
+
const path = `/api/workspaces/${identifier(owner.workspace)}/local-runners/${identifier(owner.device)}/jobs/${identifier(owner.job)}/claims/${identifier(owner.request)}/trial-evidence/${identifier(key)}`;
|
|
45
|
+
const auth = { actor: owner.owner, workspace: owner.workspace, signal };
|
|
46
|
+
let receipt = await api(path, auth);
|
|
47
|
+
match(receipt, owner, key, document, manifest);
|
|
48
|
+
const signer = await historicalRunnerSigner(owner, owner.device, signal);
|
|
49
|
+
const send = async (action, fields) => {
|
|
50
|
+
const sent = Date.parse(receipt.server_time);
|
|
51
|
+
if (!Number.isSafeInteger(sent))
|
|
52
|
+
throw fail();
|
|
53
|
+
await pending({ key, action, ...(action === 'chunk' ? { index: Number(fields.index) } : {}) });
|
|
54
|
+
const proof = await signer.sign({ schema: 'firedrill.local-trial-evidence-write.v1', job_id: owner.job,
|
|
55
|
+
attempt_id: owner.attempt, lease_id: owner.lease, request_key: owner.request, device_key: owner.device,
|
|
56
|
+
native_session_id: signer.nativeSession, transfer_key: key, manifest_sha256: manifest, sent_at_ms: sent, action, ...fields });
|
|
57
|
+
try {
|
|
58
|
+
receipt = await api(path, { ...auth, method: 'POST', body: proof });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!(error instanceof CliError) || !['connection_uncertain', 'response_unavailable'].includes(error.code))
|
|
62
|
+
throw error;
|
|
63
|
+
receipt = await api(path, auth);
|
|
64
|
+
}
|
|
65
|
+
match(receipt, owner, key, document, manifest);
|
|
66
|
+
};
|
|
67
|
+
if (receipt.state === 'not_retained') {
|
|
68
|
+
await send('register', { document });
|
|
69
|
+
if (receipt.state === 'not_retained')
|
|
70
|
+
throw fail();
|
|
71
|
+
}
|
|
72
|
+
const digest = createHash('sha256'), count = Math.ceil(document.receipt.size / CHUNK);
|
|
73
|
+
for (let index = 0; index < count; index++) {
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
const size = Math.min(CHUNK, document.receipt.size - index * CHUNK), bytes = await read(index * CHUNK, size);
|
|
76
|
+
if (bytes.length !== size)
|
|
77
|
+
throw fail();
|
|
78
|
+
digest.update(bytes);
|
|
79
|
+
const sha256 = hash(bytes), previous = receipt.chunks.find(value => value.index === index);
|
|
80
|
+
if (previous && (previous.size !== size || previous.sha256 !== sha256))
|
|
81
|
+
throw fail();
|
|
82
|
+
if (receipt.state === 'receiving' && !previous?.retained)
|
|
83
|
+
await send('chunk', { index, sha256, data: bytes.toString('base64') });
|
|
84
|
+
// A previously published identical live artifact may be linked without
|
|
85
|
+
// another upload. Still hash EVERY local byte before accepting its receipt.
|
|
86
|
+
if (receipt.state !== 'retained' && !receipt.chunks.some(value => value.index === index
|
|
87
|
+
&& value.size === size && value.sha256 === sha256 && value.retained))
|
|
88
|
+
throw fail();
|
|
89
|
+
}
|
|
90
|
+
if (digest.digest('hex') !== document.receipt.sha256)
|
|
91
|
+
throw fail();
|
|
92
|
+
if (receipt.state === 'receiving' || receipt.state === 'verifying')
|
|
93
|
+
await send('commit', {});
|
|
94
|
+
while (receipt.state === 'verifying') {
|
|
95
|
+
if (!receipt.authorization_current)
|
|
96
|
+
throw new CliError('local_trial_sync_authorization_required', 'Authorize this saved transfer from a current terminal session. Execution will not be restarted.');
|
|
97
|
+
await sleep(1000, undefined, { signal });
|
|
98
|
+
receipt = await api(path, auth);
|
|
99
|
+
match(receipt, owner, key, document, manifest);
|
|
100
|
+
}
|
|
101
|
+
const artifact = receipt.artifact;
|
|
102
|
+
if (receipt.state !== 'retained' || !artifact || identifier(artifact.id) !== artifact.id || !/^[1-9][0-9]*$/.test(artifact.generation)
|
|
103
|
+
|| artifact.size !== document.receipt.size || artifact.sha256 !== document.receipt.sha256)
|
|
104
|
+
throw fail();
|
|
105
|
+
await pending(null);
|
|
106
|
+
return artifact;
|
|
107
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { API_ORIGIN, CliError } from './http.js';
|
|
2
|
+
import { identifier } from './world-contracts.js';
|
|
3
|
+
export const voiceOperations = { voice_llm: 'chat/completions', voice_stt: 'audio/transcriptions', voice_tts: 'audio/speech' };
|
|
4
|
+
const invalid = () => new CliError('local_voice_configuration_invalid', 'The saved speech connection does not match this drill. No agent was started.');
|
|
5
|
+
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
export function voiceSession(value) {
|
|
7
|
+
if (!record(value) || Object.keys(value).sort().join(',') !== 'agent_name,agent_name_variable,key_variable,kind,mode,secret_variable,source_process,url_variable'
|
|
8
|
+
|| value.kind !== 'livekit' || value.mode !== 'isolated' || typeof value.source_process !== 'string'
|
|
9
|
+
|| !/^[a-z0-9][a-z0-9_-]{0,127}$/.test(value.source_process) || typeof value.agent_name !== 'string'
|
|
10
|
+
|| value.agent_name.length > 128 || /[^\x20-\x7e]/.test(value.agent_name))
|
|
11
|
+
throw invalid();
|
|
12
|
+
const names = [value.url_variable, value.key_variable, value.secret_variable];
|
|
13
|
+
if (value.agent_name_variable !== null)
|
|
14
|
+
names.push(value.agent_name_variable);
|
|
15
|
+
if (names.some(name => typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name)
|
|
16
|
+
|| ['PATH', 'HOME', 'TMPDIR', 'USER', 'LOGNAME', 'SHELL', 'PORT'].includes(name.toUpperCase())
|
|
17
|
+
|| /^(LD_|PYTHON|HARNESS_|ALK_|FD_|PORT_)/.test(name.toUpperCase())) || new Set(names).size !== names.length)
|
|
18
|
+
throw invalid();
|
|
19
|
+
}
|
|
20
|
+
export function voiceChannels(value, enabled) {
|
|
21
|
+
if (!Array.isArray(value) || value.length !== (enabled ? 3 : 0))
|
|
22
|
+
throw invalid();
|
|
23
|
+
const roles = new Set(), ids = new Set();
|
|
24
|
+
for (const row of value) {
|
|
25
|
+
if (!record(row) || Object.keys(row).sort().join(',') !== 'endpoint,model,role,token'
|
|
26
|
+
|| typeof row.role !== 'string' || !Object.hasOwn(voiceOperations, row.role) || roles.has(row.role)
|
|
27
|
+
|| typeof row.model !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(row.model)
|
|
28
|
+
|| typeof row.endpoint !== 'string' || typeof row.token !== 'string' || !/^[A-Za-z0-9_-]{32,256}$/.test(row.token))
|
|
29
|
+
throw invalid();
|
|
30
|
+
const endpoint = new URL(row.endpoint);
|
|
31
|
+
if (endpoint.origin !== API_ORIGIN || endpoint.username || endpoint.password || endpoint.search || endpoint.hash
|
|
32
|
+
|| !/^\/api\/internal\/model\/[a-f0-9-]{36}$/.test(endpoint.pathname))
|
|
33
|
+
throw invalid();
|
|
34
|
+
const id = identifier(endpoint.pathname.split('/').at(-1));
|
|
35
|
+
if (ids.has(id))
|
|
36
|
+
throw invalid();
|
|
37
|
+
ids.add(id);
|
|
38
|
+
roles.add(row.role);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readLocalClaim } from './runner-claim-reads.js';
|
|
2
|
+
import { CliError } from './http.js';
|
|
3
|
+
export const localWorldClaimPath = (owner) => `/api/workspaces/${owner.workspace}/local-runners/${owner.device}/jobs/${owner.job}/claims/${owner.request}`;
|
|
4
|
+
/** Read current original authority. Neither this read nor a signed command
|
|
5
|
+
* extends presence, a lease, target lifetime or evidence-retention authority. */
|
|
6
|
+
export async function localWorldClaimTime(owner, signal) {
|
|
7
|
+
signal.throwIfAborted();
|
|
8
|
+
const current = await readLocalClaim(owner, owner.device, owner.job, owner.request, signal);
|
|
9
|
+
const claim = current?.claim, identity = current?.world_session, sent = Date.parse(current?.server_time);
|
|
10
|
+
if (!current || current.job_id !== owner.job || current.grant_id !== owner.grant || current.device_key !== owner.device
|
|
11
|
+
|| current.payload_digest !== owner.payloadDigest || current.purpose !== 'world_session'
|
|
12
|
+
|| !identity || identity.session_id !== owner.session.session_id || identity.input_sha256 !== owner.session.input_sha256
|
|
13
|
+
|| identity.configuration_id !== owner.session.configuration_id || identity.expires_at !== owner.session.expires_at
|
|
14
|
+
|| !claim || claim.id !== owner.lease || claim.request_key !== owner.request || claim.attempt_id !== owner.attempt
|
|
15
|
+
|| claim.state !== 'claimed' || !Number.isSafeInteger(sent) || !Number.isSafeInteger(Date.parse(claim.expires_at))
|
|
16
|
+
|| !Number.isSafeInteger(Date.parse(owner.session.expires_at))
|
|
17
|
+
|| Date.parse(claim.expires_at) <= sent || Date.parse(claim.expires_at) > Date.parse(owner.session.expires_at)) {
|
|
18
|
+
throw new CliError('local_world_claim_changed', 'This local world no longer has its original execution authority. No command was sent.');
|
|
19
|
+
}
|
|
20
|
+
return sent;
|
|
21
|
+
}
|