firedrill-run 2.2.4 → 3.0.0
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 +178 -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 +465 -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 +1622 -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 +61 -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,794 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, open, opendir, rename } from 'node:fs/promises';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
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 { docker, command, dockerProcess, hostArch, imagePresent } from './runner-runtime.js';
|
|
9
|
+
import { containerPolicy } from './local-container-policy.js';
|
|
10
|
+
import { boundaryArguments, requireContainerBoundary } from './local-container-boundary.js';
|
|
11
|
+
import { LocalModelRelay, modelFrameWriter, readModelFrames } from './runner-model-transport.js';
|
|
12
|
+
import { withLocalJobInputs } from './runner-ownership.js';
|
|
13
|
+
import { liveEventReader, trialReader, trialStatus } from './runner-trial-protocol.js';
|
|
14
|
+
import { LocalSessionChannel, SESSION_FRAME_SCHEMA } from './runner-session-channel.js';
|
|
15
|
+
import { localWorldObservation, localWorldStatus } from './runner-world-session-protocol.js';
|
|
16
|
+
import { protectLocalWorldSession } from './runner-world-native-filters.js';
|
|
17
|
+
import { LocalWorldJournal } from './runner-world-journal.js';
|
|
18
|
+
import { processLifetimeLock } from './runner-process-lock.js';
|
|
19
|
+
import { localExecutionTimeout } from './runner-execution-deadline.js';
|
|
20
|
+
import { startCampaignWorldControls } from './runner-campaign-controls.js';
|
|
21
|
+
const SCHEMA = 'firedrill.local-execution-frame.v1', CHUNK = 16384;
|
|
22
|
+
const HASH = /^[a-f0-9]{64}$/, IMAGE = /^[a-z][a-z0-9-]{0,62}-docker\.pkg\.dev\/[a-z][a-z0-9-]{4,62}\/[a-z][a-z0-9-]{0,62}\/[a-z][a-z0-9-]{0,62}@sha256:[a-f0-9]{64}$/;
|
|
23
|
+
const invalid = () => new CliError('local_container_identity_changed', 'The container does not match the exact saved execution. It was not started or replaced.');
|
|
24
|
+
const failed = () => new CliError('local_container_unconfirmed', 'Local execution did not finish with a confirmed receipt. Its exact container journal was retained. No start was repeated.');
|
|
25
|
+
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
26
|
+
const journalWrites = new Map();
|
|
27
|
+
async function directory() {
|
|
28
|
+
const path = join(await privateDirectory(), 'containers');
|
|
29
|
+
await mkdir(path, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
30
|
+
throw error; });
|
|
31
|
+
const row = await lstat(path);
|
|
32
|
+
if (!row.isDirectory() || row.isSymbolicLink() || row.mode & 0o077 || process.getuid && row.uid !== process.getuid())
|
|
33
|
+
throw invalid();
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
const journalPath = async (scope, lease) => join(await directory(), `${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(lease)}.json`);
|
|
37
|
+
export async function hasLocalContainerJournal(scope, lease) {
|
|
38
|
+
return lstat(await journalPath(scope, lease)).then(() => true, error => {
|
|
39
|
+
if (error.code === 'ENOENT')
|
|
40
|
+
return false;
|
|
41
|
+
throw error;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function retain(value, initial = false) {
|
|
45
|
+
// Native release acknowledgement and error teardown can overlap. Preserve
|
|
46
|
+
// invocation order and immutable stage bytes so an older write cannot replace
|
|
47
|
+
// the later exact-resource cleanup receipt.
|
|
48
|
+
const key = [value.owner, value.workspace, value.lease].join(':'), body = JSON.stringify(value);
|
|
49
|
+
const writing = (journalWrites.get(key) ?? Promise.resolve()).then(async () => {
|
|
50
|
+
const path = await journalPath(value, value.lease), temporary = path + '.' + randomUUID() + '.tmp';
|
|
51
|
+
const file = await open(initial ? path : temporary, 'wx', 0o600);
|
|
52
|
+
try {
|
|
53
|
+
await file.writeFile(body);
|
|
54
|
+
await file.sync();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await file.close();
|
|
58
|
+
}
|
|
59
|
+
if (!initial)
|
|
60
|
+
await rename(temporary, path);
|
|
61
|
+
const parent = await open(await directory(), constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
62
|
+
try {
|
|
63
|
+
await parent.sync();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await parent.close();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
journalWrites.set(key, writing);
|
|
70
|
+
try {
|
|
71
|
+
await writing;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (journalWrites.get(key) === writing)
|
|
75
|
+
journalWrites.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function load(scope, lease) {
|
|
79
|
+
const path = await journalPath(scope, lease), before = await lstat(path);
|
|
80
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.mode & 0o077 || before.size > 8 * 1024 ** 2 + 16384
|
|
81
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
82
|
+
throw invalid();
|
|
83
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
84
|
+
try {
|
|
85
|
+
const actual = await file.stat();
|
|
86
|
+
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size)
|
|
87
|
+
throw invalid();
|
|
88
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
89
|
+
if (value.schema !== 'firedrill.local-container.v1' || !['validation', 'trial', 'world_session'].includes(value.purpose)
|
|
90
|
+
|| value.owner !== scope.owner || value.workspace !== scope.workspace
|
|
91
|
+
|| value.lease !== lease || value.name !== 'fd-job-' + lease || !IMAGE.test(value.image)
|
|
92
|
+
|| !/^[a-f0-9]{40}$/.test(value.source_revision) || !HASH.test(value.manifest_sha256) || !HASH.test(value.configuration_digest)
|
|
93
|
+
|| value.container_id !== null && !HASH.test(value.container_id)
|
|
94
|
+
|| !['preflight', 'no_resource_dispatched', 'create_dispatched', 'created', 'start_dispatched', 'bootstrapped', 'guest_exited', 'container_exited', 'cleanup_confirmed'].includes(value.stage))
|
|
95
|
+
throw invalid();
|
|
96
|
+
for (const id of [value.world, value.device, value.job, value.attempt, value.request])
|
|
97
|
+
identifier(id);
|
|
98
|
+
if (value.purpose === 'world_session') {
|
|
99
|
+
const session = value.world_session;
|
|
100
|
+
if (!session || Object.keys(session).sort().join(',') !== 'configuration_id,expires_at,input_sha256,session_id'
|
|
101
|
+
|| identifier(session.session_id) !== session.session_id || identifier(session.configuration_id) !== session.configuration_id
|
|
102
|
+
|| !HASH.test(session.input_sha256) || !Number.isFinite(Date.parse(session.expires_at))
|
|
103
|
+
|| typeof value.session_released !== 'boolean')
|
|
104
|
+
throw invalid();
|
|
105
|
+
const identity = value.world_journal;
|
|
106
|
+
if (!identity || Object.keys(identity).sort().join(',') !== 'attempt,device,evidenceUpload,grant,job,lease,nativeSession,owner,payloadDigest,request,session,workspace'
|
|
107
|
+
|| identity.owner !== value.owner || identity.workspace !== value.workspace || identity.device !== value.device || identity.lease !== value.lease
|
|
108
|
+
|| identity.job !== value.job || identity.request !== value.request || identity.attempt !== value.attempt || identity.evidenceUpload !== true
|
|
109
|
+
|| JSON.stringify(identity.session) !== JSON.stringify(session) || !HASH.test(identity.payloadDigest))
|
|
110
|
+
throw invalid();
|
|
111
|
+
identifier(identity.grant);
|
|
112
|
+
identifier(identity.nativeSession);
|
|
113
|
+
const reference = value.world_final_status;
|
|
114
|
+
if (reference !== null && (!reference || Object.keys(reference).sort().join(',') !== 'schema,sha256,size'
|
|
115
|
+
|| reference.schema !== 'firedrill.local-world-final-reference.v1' || !Number.isSafeInteger(reference.size)
|
|
116
|
+
|| reference.size < 1 || reference.size > 8 * 1024 ** 2 || !HASH.test(reference.sha256)))
|
|
117
|
+
throw invalid();
|
|
118
|
+
}
|
|
119
|
+
else if (value.world_session !== undefined || value.session_released !== undefined || value.world_journal !== undefined
|
|
120
|
+
|| value.world_final_status !== undefined || before.size > 16384)
|
|
121
|
+
throw invalid();
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw invalid();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await file.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function locked(scope, lease, work) {
|
|
132
|
+
const path = (await journalPath(scope, lease)) + '.lock';
|
|
133
|
+
const lock = await processLifetimeLock(path);
|
|
134
|
+
try {
|
|
135
|
+
lock.assertHeld();
|
|
136
|
+
const value = await work(lock);
|
|
137
|
+
lock.assertHeld();
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await lock.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function labels(value) {
|
|
145
|
+
return { 'io.firedrill.local-execution': value.lease, 'io.firedrill.actor': value.owner, 'io.firedrill.workspace': value.workspace,
|
|
146
|
+
'io.firedrill.job': value.job, 'io.firedrill.attempt': value.attempt, 'io.firedrill.device': value.device,
|
|
147
|
+
'io.firedrill.manifest': value.manifest_sha256 };
|
|
148
|
+
}
|
|
149
|
+
async function inspected(context, value, signal) {
|
|
150
|
+
const reply = await command(context, ['container', 'inspect', value.container_id ?? value.name], { signal });
|
|
151
|
+
if (reply.code !== 0)
|
|
152
|
+
return null; // Never absence proof for a delayed create.
|
|
153
|
+
let rows;
|
|
154
|
+
try {
|
|
155
|
+
rows = JSON.parse(reply.output);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw invalid();
|
|
159
|
+
}
|
|
160
|
+
const row = Array.isArray(rows) && rows.length === 1 ? rows[0] : null;
|
|
161
|
+
if (!row || !HASH.test(row.Id) || value.container_id && row.Id !== value.container_id || row.Name !== '/' + value.name
|
|
162
|
+
|| row.Config?.Image !== value.image || Object.entries(labels(value)).some(([key, expected]) => row.Config?.Labels?.[key] !== expected))
|
|
163
|
+
throw invalid();
|
|
164
|
+
return row;
|
|
165
|
+
}
|
|
166
|
+
async function cleanup(context, value, signal) {
|
|
167
|
+
if (value.stage === 'cleanup_confirmed' || value.stage === 'no_resource_dispatched')
|
|
168
|
+
return true;
|
|
169
|
+
if (value.stage === 'preflight') {
|
|
170
|
+
// This persisted stage precedes create intent. It is not a failed inspect
|
|
171
|
+
// being mistaken for deletion of an uncertain resource.
|
|
172
|
+
value.stage = 'no_resource_dispatched';
|
|
173
|
+
await retain(value);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (!context)
|
|
177
|
+
return false;
|
|
178
|
+
const row = await inspected(context, value, signal);
|
|
179
|
+
if (!row)
|
|
180
|
+
return false;
|
|
181
|
+
value.container_id = row.Id;
|
|
182
|
+
await retain(value);
|
|
183
|
+
const result = await command(context, ['container', 'rm', '--force', row.Id], { signal });
|
|
184
|
+
if (result.code !== 0)
|
|
185
|
+
return false;
|
|
186
|
+
// Successful deletion is exact-resource proof. A failed/not-found command or
|
|
187
|
+
// an absent pending create is deliberately NOT promoted to cleanup proof.
|
|
188
|
+
value.stage = 'cleanup_confirmed';
|
|
189
|
+
await retain(value);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/** Exact local cleanup only. Does not replay bootstrap/start or settle cloud state. */
|
|
193
|
+
export async function recoverLocalContainer(scope, lease, signal) {
|
|
194
|
+
lease = identifier(lease);
|
|
195
|
+
return locked(scope, lease, async (lock) => {
|
|
196
|
+
signal = AbortSignal.any([signal, lock.signal]);
|
|
197
|
+
const value = await load(scope, lease);
|
|
198
|
+
const confirmed = value.stage === 'cleanup_confirmed' || value.stage === 'no_resource_dispatched'
|
|
199
|
+
|| await cleanup(value.stage === 'preflight' ? undefined : await docker(), value, signal);
|
|
200
|
+
return { schema: 'firedrill.local-container-recovery.v1', operation_id: lease,
|
|
201
|
+
cleanup_confirmed: confirmed, resource_state: value.stage === 'no_resource_dispatched' ? 'not_dispatched' : confirmed ? 'removed' : 'unconfirmed',
|
|
202
|
+
cloud_completion_recorded: false };
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Read the exact private journal only. No Docker lookup, start or deletion. */
|
|
206
|
+
export async function localContainerPurpose(scope, operation) {
|
|
207
|
+
return (await load(scope, identifier(operation))).purpose;
|
|
208
|
+
}
|
|
209
|
+
/** OS-owned metadata discovery for OFFLINE evidence erasure only. This neither
|
|
210
|
+
* authenticates cloud access nor opens evidence, starts or removes a container.
|
|
211
|
+
*/
|
|
212
|
+
export async function* localWorldEvidenceContexts(operation, workspace, onProblem, signal) {
|
|
213
|
+
for await (const row of localEvidenceContexts(operation, workspace, onProblem, signal)) {
|
|
214
|
+
if (row.purpose === 'world_session')
|
|
215
|
+
yield row;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export async function* localEvidenceContexts(operation, workspace, onProblem, signal) {
|
|
219
|
+
if (operation)
|
|
220
|
+
identifier(operation);
|
|
221
|
+
if (workspace)
|
|
222
|
+
identifier(workspace);
|
|
223
|
+
const uuid = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
|
|
224
|
+
const format = new RegExp('^(' + uuid + ')-(' + uuid + ')-(' + uuid + ')\\.json$');
|
|
225
|
+
// OS directory iteration is bounded and closes on exhaustion, cancellation or
|
|
226
|
+
// an early consumer return. No sort/whole-history array is needed for erasure.
|
|
227
|
+
for await (const item of await opendir(await directory(), { bufferSize: 32 })) {
|
|
228
|
+
signal?.throwIfAborted();
|
|
229
|
+
const found = format.exec(item.name);
|
|
230
|
+
if (!found || operation && found[3] !== operation || workspace && found[2] !== workspace)
|
|
231
|
+
continue;
|
|
232
|
+
const scope = { owner: identifier(found[1]), workspace: identifier(found[2]) }, id = identifier(found[3]);
|
|
233
|
+
try {
|
|
234
|
+
const record = await load(scope, id);
|
|
235
|
+
yield { ...scope, operation: id, purpose: record.purpose };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (!onProblem)
|
|
239
|
+
throw error;
|
|
240
|
+
onProblem(id, error instanceof CliError ? error.code : 'local_container_identity_changed');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Offline evidence removal holds the ORIGINAL execution lock and requires its
|
|
245
|
+
* saved no-dispatch or exact cleanup receipt. No Docker/provider call occurs. */
|
|
246
|
+
export async function withLocalContainerCompletion(scope, operation, purpose, work) {
|
|
247
|
+
operation = identifier(operation);
|
|
248
|
+
return locked(scope, operation, async (lock) => work(await localContainerCompletion(scope, operation, purpose), lock.signal));
|
|
249
|
+
}
|
|
250
|
+
export async function localContainerCompletion(scope, operation, purpose = 'validation') {
|
|
251
|
+
const value = await load(scope, identifier(operation));
|
|
252
|
+
if (value.purpose !== purpose)
|
|
253
|
+
throw new CliError('local_completion_purpose_changed', 'Use the closing receipt for this exact saved execution purpose.');
|
|
254
|
+
const absent = value.stage === 'no_resource_dispatched';
|
|
255
|
+
if (!absent && (value.stage !== 'cleanup_confirmed' || !value.container_id)) {
|
|
256
|
+
throw new CliError('local_container_cleanup_pending', 'Confirm cleanup of the exact saved container before synchronizing its closing receipt.');
|
|
257
|
+
}
|
|
258
|
+
return { owner: value.owner, workspace: value.workspace, device: value.device, job: value.job, request: value.request,
|
|
259
|
+
lease: value.lease, attempt: value.attempt, manifestSha256: value.manifest_sha256,
|
|
260
|
+
worldEvidenceOwner: purpose === 'world_session' ? value.world_journal : undefined,
|
|
261
|
+
worldFinalStatus: purpose === 'world_session' ? value.world_final_status : undefined,
|
|
262
|
+
document: { schema: purpose === 'world_session' ? 'firedrill.local-world-container-receipt.v1'
|
|
263
|
+
: purpose === 'trial' ? 'firedrill.local-trial-container-receipt.v1' : 'firedrill.local-container-receipt.v1',
|
|
264
|
+
...(purpose === 'world_session' ? { world_session: value.world_session, session_released: value.session_released } : {}),
|
|
265
|
+
operation_id: value.lease, container_id: value.container_id,
|
|
266
|
+
name: value.name, image: value.image, resource_state: absent ? 'not_dispatched' : 'removed', removed: !absent,
|
|
267
|
+
launch: value.launch, exit_code: value.exit_code, status: value.status ?? null } };
|
|
268
|
+
}
|
|
269
|
+
function boundary(owner, policy, inputs) {
|
|
270
|
+
const memory = owner.limits.memory_mb * 1024 ** 2, cpus = owner.limits.cpu_units * 10 ** 9;
|
|
271
|
+
if (!Number.isSafeInteger(memory) || memory < 1024 ** 3 || !Number.isSafeInteger(cpus) || cpus < 10 ** 9)
|
|
272
|
+
throw invalid();
|
|
273
|
+
const workBytes = Math.floor(memory * 0.6), privateBytes = Math.floor(memory * 0.05);
|
|
274
|
+
const workInput = inputs.manifest.entries.filter(item => !['scenarios', 'dataset', 'checkpoint', 'application_checkpoint'].includes(item.part)).reduce((sum, item) => sum + item.size, 0);
|
|
275
|
+
const scenarioSize = owner.purpose === 'world_session' ? 0 : inputs.manifest.entries.find(item => item.part === 'scenarios')?.size ?? Infinity;
|
|
276
|
+
const datasetSize = inputs.manifest.entries.find(item => item.part === 'dataset')?.size ?? 0;
|
|
277
|
+
// The fixed guest bootstrap places initial checkpoints in /run/simulation,
|
|
278
|
+
// beside scenarios and datasets, not in the separate /work archive store.
|
|
279
|
+
const checkpointSize = inputs.manifest.entries.find(item => item.part === 'checkpoint')?.size ?? 0;
|
|
280
|
+
const applicationSize = inputs.manifest.entries.find(item => item.part === 'application_checkpoint')?.size ?? 0;
|
|
281
|
+
if (workInput * 2 > workBytes || scenarioSize + datasetSize + checkpointSize + applicationSize + 4 * 1024 ** 2 > privateBytes) {
|
|
282
|
+
throw new CliError('local_storage_capacity_required', 'The approved memory-backed execution storage cannot hold these inputs safely. No image, memory limit or execution location was changed.', 2);
|
|
283
|
+
}
|
|
284
|
+
return { image: owner.runtime.image, arguments: ['-m', 'runtime_support.guest_local_execution', '--operation-id', owner.lease,
|
|
285
|
+
...(policy.requiredApparmor ? ['--apparmor-profile', policy.apparmor.name] : [])], memoryBytes: memory, nanoCpus: cpus,
|
|
286
|
+
interactive: true, tmpfs: {
|
|
287
|
+
'/work': `rw,nosuid,nodev,size=${workBytes},uid=2000,gid=2000,mode=0755`,
|
|
288
|
+
'/run/simulation': `rw,nosuid,nodev,noexec,size=${privateBytes},uid=2000,gid=2000,mode=0700`,
|
|
289
|
+
'/tmp': `rw,nosuid,nodev,noexec,size=${privateBytes},uid=2000,gid=2000,mode=1777`
|
|
290
|
+
} };
|
|
291
|
+
}
|
|
292
|
+
async function bootstrap(owner, inputs, configuration, write, signal) {
|
|
293
|
+
const packet = Buffer.from(JSON.stringify(configuration));
|
|
294
|
+
if (packet.length > 2 * 1024 ** 2)
|
|
295
|
+
throw invalid();
|
|
296
|
+
const parts = [...(owner.purpose === 'world_session' ? ['source', 'compiled', 'packages']
|
|
297
|
+
: ['source', owner.purpose === 'trial' ? 'compiled' : 'candidate', 'packages', 'scenarios']),
|
|
298
|
+
...(['trial', 'world_session'].includes(owner.purpose) && inputs.manifest.entries.some(one => one.part === 'dataset') ? ['dataset'] : []),
|
|
299
|
+
...(['world_session', 'trial'].includes(owner.purpose) && inputs.manifest.entries.some(one => one.part === 'checkpoint') ? ['checkpoint'] : []),
|
|
300
|
+
...(['world_session', 'trial'].includes(owner.purpose) && inputs.manifest.entries.some(one => one.part === 'application_checkpoint') ? ['application_checkpoint'] : [])];
|
|
301
|
+
const entries = parts.map(part => {
|
|
302
|
+
const entry = inputs.manifest.entries.find(item => item.part === part);
|
|
303
|
+
if (!entry)
|
|
304
|
+
throw invalid();
|
|
305
|
+
return entry;
|
|
306
|
+
});
|
|
307
|
+
const configurationEntry = { part: 'configuration', size: packet.length, sha256: sha(packet) };
|
|
308
|
+
const emit = (kind, fields = {}) => write({ schema: SCHEMA, kind, id: owner.lease, ...fields });
|
|
309
|
+
try {
|
|
310
|
+
await emit('bootstrap', { job_id: owner.job, attempt_id: owner.attempt, manifest_sha256: inputs.manifestSha256,
|
|
311
|
+
purpose: owner.purpose, entries: [...entries, configurationEntry] });
|
|
312
|
+
for (const entry of [...entries, configurationEntry]) {
|
|
313
|
+
signal.throwIfAborted();
|
|
314
|
+
let size = 0, sequence = 0;
|
|
315
|
+
const hash = createHash('sha256');
|
|
316
|
+
const chunk = async (body) => {
|
|
317
|
+
signal.throwIfAborted();
|
|
318
|
+
hash.update(body);
|
|
319
|
+
size += body.length;
|
|
320
|
+
await emit('input_chunk', { part: entry.part, sequence: sequence++, data: body.toString('base64') });
|
|
321
|
+
};
|
|
322
|
+
if (entry.part === 'configuration') {
|
|
323
|
+
for (let offset = 0; offset < packet.length; offset += CHUNK)
|
|
324
|
+
await chunk(packet.subarray(offset, offset + CHUNK));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const path = inputs.files[entry.part];
|
|
328
|
+
if (typeof path !== 'string')
|
|
329
|
+
throw invalid();
|
|
330
|
+
const before = await lstat(path);
|
|
331
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.mode & 0o077 || before.size !== entry.size
|
|
332
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
333
|
+
throw invalid();
|
|
334
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
335
|
+
try {
|
|
336
|
+
const actual = await file.stat();
|
|
337
|
+
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size)
|
|
338
|
+
throw invalid();
|
|
339
|
+
const bytes = Buffer.alloc(CHUNK);
|
|
340
|
+
while (size < entry.size) {
|
|
341
|
+
const result = await file.read(bytes, 0, Math.min(CHUNK, entry.size - size), size);
|
|
342
|
+
if (!result.bytesRead)
|
|
343
|
+
throw invalid();
|
|
344
|
+
await chunk(bytes.subarray(0, result.bytesRead));
|
|
345
|
+
}
|
|
346
|
+
const after = await file.stat();
|
|
347
|
+
if (after.size !== actual.size || after.mtimeMs !== actual.mtimeMs || after.ctimeMs !== actual.ctimeMs)
|
|
348
|
+
throw invalid();
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
await file.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (size !== entry.size || hash.digest('hex') !== entry.sha256)
|
|
355
|
+
throw invalid();
|
|
356
|
+
await emit('input_end', { part: entry.part, sequence });
|
|
357
|
+
}
|
|
358
|
+
await emit('commit');
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
packet.fill(0);
|
|
362
|
+
} // Best-effort buffer erasure, not JS-string zeroization.
|
|
363
|
+
}
|
|
364
|
+
async function connected(context, value, owner, inputs, configuration, policy, collector, signal) {
|
|
365
|
+
const selected = value.boundary;
|
|
366
|
+
if (!selected || !value.security)
|
|
367
|
+
throw invalid();
|
|
368
|
+
value.stage = 'start_dispatched';
|
|
369
|
+
await retain(value);
|
|
370
|
+
const child = dockerProcess(context, ['container', 'start', '--attach', '--interactive', value.container_id], signal);
|
|
371
|
+
child.stderr.resume(); // No daemon/target output or frame body goes to a terminal.
|
|
372
|
+
const closed = new Promise((resolve, reject) => {
|
|
373
|
+
child.once('error', () => reject(failed()));
|
|
374
|
+
child.once('close', code => resolve(code));
|
|
375
|
+
});
|
|
376
|
+
void closed.catch(() => undefined);
|
|
377
|
+
const stopping = new AbortController(), combined = AbortSignal.any([signal, stopping.signal]);
|
|
378
|
+
const write = modelFrameWriter(child.stdin, combined);
|
|
379
|
+
let relay;
|
|
380
|
+
let session, sessionWork;
|
|
381
|
+
let campaignControls;
|
|
382
|
+
let finalSession;
|
|
383
|
+
let installed = false, finished = false, result;
|
|
384
|
+
const live = collector.purpose === 'trial' && configuration.control.schema === 'firedrill.trial.v1'
|
|
385
|
+
? liveEventReader(owner.lease, configuration.control.execution_id, configuration.control.scenario_key, collector.live, collector.liveArtifact) : null;
|
|
386
|
+
const incoming = readModelFrames(child.stdout, combined);
|
|
387
|
+
const reading = (async () => {
|
|
388
|
+
for await (const frame of incoming) {
|
|
389
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame))
|
|
390
|
+
throw failed();
|
|
391
|
+
const item = frame;
|
|
392
|
+
if (item.schema === 'firedrill.local-runtime-frame.v2') {
|
|
393
|
+
if (!installed || finished || !relay)
|
|
394
|
+
throw failed();
|
|
395
|
+
await relay.accept(item);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (item.schema === SESSION_FRAME_SCHEMA) {
|
|
399
|
+
if (!installed || finished || collector.purpose !== 'world_session' || !session)
|
|
400
|
+
throw failed();
|
|
401
|
+
await session.accept(item);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (item.schema !== SCHEMA || item.id !== owner.lease)
|
|
405
|
+
throw failed();
|
|
406
|
+
if (typeof item.kind === 'string' && ['live_event', 'live_event_chunk', 'live_event_end',
|
|
407
|
+
'live_artifact', 'live_artifact_chunk', 'live_artifact_end'].includes(item.kind)) {
|
|
408
|
+
if (!installed || finished || !live)
|
|
409
|
+
throw failed();
|
|
410
|
+
await live.accept(item);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (item.kind === 'bootstrapped' && !installed && !finished
|
|
414
|
+
&& Object.keys(item).sort().join(',') === 'id,kind,manifest_sha256,schema' && item.manifest_sha256 === inputs.manifestSha256) {
|
|
415
|
+
const row = await inspected(context, value, combined);
|
|
416
|
+
if (!row || row.State?.Running !== true || row.State?.Restarting)
|
|
417
|
+
throw failed();
|
|
418
|
+
requireContainerBoundary(row, policy, selected);
|
|
419
|
+
value.stage = 'bootstrapped';
|
|
420
|
+
await retain(value);
|
|
421
|
+
installed = true;
|
|
422
|
+
if (collector.purpose === 'world_session') {
|
|
423
|
+
if (owner.purpose !== 'world_session' || configuration.control.schema !== 'firedrill.world-session.v1')
|
|
424
|
+
throw failed();
|
|
425
|
+
const sessionOwner = owner, control = configuration.control;
|
|
426
|
+
session = new LocalSessionChannel(owner.lease, owner.session.session_id, write, combined, observation => localWorldObservation(observation, sessionOwner, control));
|
|
427
|
+
}
|
|
428
|
+
// Bootstrap may outlive the window checked before container start.
|
|
429
|
+
// Refuse the target handoff now; closing grace never authorizes work.
|
|
430
|
+
localExecutionTimeout(configuration);
|
|
431
|
+
await write({ schema: SCHEMA, kind: 'execute', id: owner.lease });
|
|
432
|
+
if (collector.purpose === 'trial' && owner.purpose === 'trial' && owner.evidenceUpload
|
|
433
|
+
&& configuration.control.schema === 'firedrill.trial.v1' && configuration.control.world_continuity === 'continuous') {
|
|
434
|
+
// This is a separate bounded control poll, not another model reader
|
|
435
|
+
// or local runner. Every native call rechecks the ORIGINAL container
|
|
436
|
+
// identity and unchanged isolation before its fixed image entry.
|
|
437
|
+
const current = async (active) => {
|
|
438
|
+
const row = await inspected(context, value, active);
|
|
439
|
+
if (!row || row.State?.Running !== true || row.State?.Restarting || value.stage !== 'bootstrapped')
|
|
440
|
+
throw failed();
|
|
441
|
+
requireContainerBoundary(row, policy, selected);
|
|
442
|
+
};
|
|
443
|
+
campaignControls = startCampaignWorldControls(owner, configuration, {
|
|
444
|
+
stage: async (id, packet, digest, active) => {
|
|
445
|
+
await current(active);
|
|
446
|
+
const bounded = AbortSignal.any([active, AbortSignal.timeout(20000)]);
|
|
447
|
+
const staging = dockerProcess(context, ['container', 'exec', '--interactive', '--user', '2000:2000', value.container_id,
|
|
448
|
+
'/opt/firedrill/bin/python', '-P', '-B', '-m', 'runtime_support.guest_campaign_input',
|
|
449
|
+
'--operation-id', owner.lease, '--command-id', identifier(id)], bounded);
|
|
450
|
+
staging.stderr.resume();
|
|
451
|
+
const chunks = [];
|
|
452
|
+
let length = 0, overflow = false;
|
|
453
|
+
const done = new Promise((resolve, reject) => {
|
|
454
|
+
staging.stdout.on('data', (chunk) => {
|
|
455
|
+
length += chunk.length;
|
|
456
|
+
if (length > 8192) {
|
|
457
|
+
overflow = true;
|
|
458
|
+
staging.kill('SIGTERM');
|
|
459
|
+
}
|
|
460
|
+
else
|
|
461
|
+
chunks.push(chunk);
|
|
462
|
+
});
|
|
463
|
+
staging.once('error', () => reject(failed()));
|
|
464
|
+
staging.once('close', code => overflow || bounded.aborted ? reject(failed()) : resolve({ code, body: Buffer.concat(chunks) }));
|
|
465
|
+
});
|
|
466
|
+
staging.stdin.on('error', () => undefined);
|
|
467
|
+
staging.stdin.end(packet);
|
|
468
|
+
const result = await done;
|
|
469
|
+
try {
|
|
470
|
+
const row = JSON.parse(result.body.toString('utf8'));
|
|
471
|
+
if (result.code !== 0 && row && typeof row === 'object' && !Array.isArray(row)
|
|
472
|
+
&& Object.keys(row).sort().join(',') === 'code,schema'
|
|
473
|
+
&& row.schema === 'firedrill.campaign-world-control-error.v1'
|
|
474
|
+
&& row.code === 'campaign_world_control_capacity')
|
|
475
|
+
throw new CliError('campaign_world_control_capacity', 'This runtime has no room to retain another world control. No command was published.');
|
|
476
|
+
if (result.code !== 0 || !row || typeof row !== 'object' || Array.isArray(row)
|
|
477
|
+
|| Object.keys(row).sort().join(',') !== 'command_id,operation_id,schema,sha256,staged'
|
|
478
|
+
|| row.schema !== 'firedrill.campaign-world-control-input.v1'
|
|
479
|
+
|| row.command_id !== id || row.operation_id !== owner.lease
|
|
480
|
+
|| row.sha256 !== digest || row.staged !== true)
|
|
481
|
+
throw failed();
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
result.body.fill(0);
|
|
485
|
+
for (const bytes of chunks)
|
|
486
|
+
bytes.fill(0);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
read: async (id, mode, active) => {
|
|
490
|
+
await current(active);
|
|
491
|
+
const result = await command(context, ['container', 'exec', '--user', '2000:2000', value.container_id,
|
|
492
|
+
'/opt/firedrill/bin/python', '-P', '-B', '-m', 'runtime_support.guest_campaign_bridge',
|
|
493
|
+
'--operation-id', owner.lease, '--command-id', identifier(id), '--mode', mode], { signal: active, seconds: 15, maximum: 32768 });
|
|
494
|
+
let resultValue;
|
|
495
|
+
try {
|
|
496
|
+
resultValue = JSON.parse(result.output);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
throw failed();
|
|
500
|
+
}
|
|
501
|
+
if (result.code !== 0) {
|
|
502
|
+
if (resultValue && typeof resultValue === 'object' && !Array.isArray(resultValue)
|
|
503
|
+
&& Object.keys(resultValue).sort().join(',') === 'code,schema'
|
|
504
|
+
&& resultValue.schema === 'firedrill.campaign-world-control-error.v1'
|
|
505
|
+
&& resultValue.code === 'campaign_world_control_capacity')
|
|
506
|
+
throw new CliError('campaign_world_control_capacity', 'This runtime has no room to retain another world control. No command was published.');
|
|
507
|
+
throw failed();
|
|
508
|
+
}
|
|
509
|
+
return resultValue;
|
|
510
|
+
},
|
|
511
|
+
}, combined);
|
|
512
|
+
}
|
|
513
|
+
if (collector.purpose === 'world_session') {
|
|
514
|
+
if (owner.purpose !== 'world_session' || !session)
|
|
515
|
+
throw failed();
|
|
516
|
+
const channel = session, sessionOwner = owner;
|
|
517
|
+
// Never await this loop inside the sole pipe reader. Model replies,
|
|
518
|
+
// command acknowledgements and evidence chunks all need that reader.
|
|
519
|
+
sessionWork = Promise.resolve().then(async () => {
|
|
520
|
+
if (await protectLocalWorldSession(sessionOwner, configuration, channel, combined)) {
|
|
521
|
+
await collector.run(sessionOwner, configuration, channel, combined);
|
|
522
|
+
}
|
|
523
|
+
if (!channel.isClosing || channel.isReleased)
|
|
524
|
+
throw failed();
|
|
525
|
+
finalSession = await channel.finalObservation();
|
|
526
|
+
value.world_final_status = finalSession.reference;
|
|
527
|
+
await retain(value);
|
|
528
|
+
await channel.release();
|
|
529
|
+
value.session_released = true;
|
|
530
|
+
await retain(value);
|
|
531
|
+
}).catch(error => { stopping.abort(); throw error; });
|
|
532
|
+
void sessionWork.catch(() => undefined);
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (item.kind !== 'finished' || !installed || finished || live?.pending || Object.keys(item).sort().join(',') !== 'id,kind,launch,schema,status'
|
|
537
|
+
|| !item.launch || typeof item.launch !== 'object' || Array.isArray(item.launch)
|
|
538
|
+
|| item.status !== null && (!item.status || typeof item.status !== 'object' || Array.isArray(item.status)))
|
|
539
|
+
throw failed();
|
|
540
|
+
const launch = item.launch;
|
|
541
|
+
if (launch.launch_id !== owner.lease || !['exited', 'launch_failed'].includes(launch.state)
|
|
542
|
+
|| Object.keys(launch).sort().join(',') !== (launch.state === 'exited' ? 'exit_code,launch_id,state' : 'launch_id,state')
|
|
543
|
+
|| launch.state === 'exited' && (!Number.isSafeInteger(launch.exit_code) || launch.exit_code < -128 || launch.exit_code > 255))
|
|
544
|
+
throw failed();
|
|
545
|
+
if (collector.purpose === 'trial')
|
|
546
|
+
trialStatus(item.status);
|
|
547
|
+
if (collector.purpose === 'world_session') {
|
|
548
|
+
if (owner.purpose !== 'world_session' || configuration.control.schema !== 'firedrill.world-session.v1'
|
|
549
|
+
|| !session?.isClosing || !session.isReleased || !sessionWork || !finalSession)
|
|
550
|
+
throw failed();
|
|
551
|
+
await sessionWork;
|
|
552
|
+
const reference = finalSession.reference, received = item.status;
|
|
553
|
+
if (reference === null ? received !== null : received === null
|
|
554
|
+
|| Object.keys(received).sort().join(',') !== 'schema,sha256,size'
|
|
555
|
+
|| received.schema !== reference.schema || received.sha256 !== reference.sha256 || received.size !== reference.size)
|
|
556
|
+
throw failed();
|
|
557
|
+
const originalLaunch = finalSession.observation.launch;
|
|
558
|
+
if (!originalLaunch || originalLaunch.launch_id !== launch.launch_id || originalLaunch.state !== launch.state
|
|
559
|
+
|| originalLaunch.exit_code !== launch.exit_code)
|
|
560
|
+
throw failed();
|
|
561
|
+
item.status = finalSession.observation.status;
|
|
562
|
+
localWorldStatus(item.status, owner, configuration.control);
|
|
563
|
+
}
|
|
564
|
+
finished = true;
|
|
565
|
+
await campaignControls?.stop();
|
|
566
|
+
campaignControls = undefined;
|
|
567
|
+
relay?.close();
|
|
568
|
+
value.launch = launch;
|
|
569
|
+
value.status = item.status;
|
|
570
|
+
value.stage = 'guest_exited';
|
|
571
|
+
await retain(value);
|
|
572
|
+
const row = await inspected(context, value, combined);
|
|
573
|
+
if (!row || row.State?.Running !== true || row.State?.Restarting)
|
|
574
|
+
throw failed();
|
|
575
|
+
requireContainerBoundary(row, policy, selected);
|
|
576
|
+
let exported = false;
|
|
577
|
+
const reply = async (kind) => {
|
|
578
|
+
const response = await incoming.next();
|
|
579
|
+
const frame = response.value;
|
|
580
|
+
if (response.done || !frame || frame.schema !== SCHEMA || frame.kind !== kind || frame.id !== owner.lease)
|
|
581
|
+
throw failed();
|
|
582
|
+
return frame;
|
|
583
|
+
};
|
|
584
|
+
const exportNative = async (kind) => {
|
|
585
|
+
const status = item.status;
|
|
586
|
+
if (exported || launch.state !== 'exited' || !['native_validation', 'native_operation_attempt'].includes(kind)
|
|
587
|
+
|| (kind === 'native_validation' ? launch.exit_code !== 0 || status?.state !== 'native_validated'
|
|
588
|
+
: launch.exit_code === 0 || status?.state !== 'failed' || status.operation_attempt_available !== true))
|
|
589
|
+
throw failed();
|
|
590
|
+
exported = true;
|
|
591
|
+
await write({ schema: SCHEMA, kind: 'export', id: owner.lease, artifact: kind });
|
|
592
|
+
const response = await reply('exported'), receipt = response.receipt;
|
|
593
|
+
if (Object.keys(response).sort().join(',') !== 'artifact,id,kind,receipt,schema' || response.artifact !== kind || !receipt
|
|
594
|
+
|| !Number.isSafeInteger(receipt.size) || receipt.size < 1 || !HASH.test(receipt.sha256)
|
|
595
|
+
|| (kind === 'native_validation' ? Object.keys(receipt).sort().join(',') !== 'runtime_validated,scope,sha256,size'
|
|
596
|
+
|| receipt.size > 128 * 1024 ** 2 || !('runtime_validated' in receipt) || receipt.runtime_validated !== false
|
|
597
|
+
|| !('scope' in receipt) || receipt.scope !== (configuration.control.validation_scope === 'target_startup_only'
|
|
598
|
+
? 'target_startup_only' : 'native_runtime_and_scenario_preconditions')
|
|
599
|
+
: Object.keys(receipt).sort().join(',') !== 'complete,sha256,size' || receipt.size > 8 * 1024 ** 2
|
|
600
|
+
|| !('complete' in receipt) || receipt.complete !== false))
|
|
601
|
+
throw failed();
|
|
602
|
+
return { kind, receipt, read: async (offset, length) => {
|
|
603
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 1 || length > CHUNK || offset + length > receipt.size)
|
|
604
|
+
throw failed();
|
|
605
|
+
await write({ schema: SCHEMA, kind: 'export_chunk', id: owner.lease, offset, length });
|
|
606
|
+
const response = await reply('export_chunk');
|
|
607
|
+
if (Object.keys(response).sort().join(',') !== 'data,id,kind,length,offset,schema,sha256'
|
|
608
|
+
|| response.offset !== offset || response.length !== length || typeof response.data !== 'string'
|
|
609
|
+
|| response.data.length > 4 * Math.ceil(CHUNK / 3))
|
|
610
|
+
throw failed();
|
|
611
|
+
const body = Buffer.from(response.data, 'base64');
|
|
612
|
+
if (body.length !== length || body.toString('base64') !== response.data || sha(body) !== response.sha256)
|
|
613
|
+
throw failed();
|
|
614
|
+
return body;
|
|
615
|
+
} };
|
|
616
|
+
};
|
|
617
|
+
const observation = { operation_id: owner.lease, container_id: value.container_id, launch,
|
|
618
|
+
status: item.status, provenance: 'customer_device' };
|
|
619
|
+
if (collector.purpose === 'trial') {
|
|
620
|
+
const control = configuration.control;
|
|
621
|
+
if (control.schema !== 'firedrill.trial.v1')
|
|
622
|
+
throw failed();
|
|
623
|
+
result = await collector.collect(owner, observation, trialReader(owner.lease, control.execution_id, control.scenario_key, launch, (kind, fields = {}) => write({ schema: SCHEMA, kind, id: owner.lease, ...fields }), reply), combined);
|
|
624
|
+
}
|
|
625
|
+
else if (collector.purpose === 'world_session') {
|
|
626
|
+
if (owner.purpose !== 'world_session' || !finalSession)
|
|
627
|
+
throw failed();
|
|
628
|
+
result = await collector.collect(owner, observation, finalSession, combined);
|
|
629
|
+
}
|
|
630
|
+
else
|
|
631
|
+
result = await collector.collect(owner, observation, exportNative, combined);
|
|
632
|
+
await write({ schema: SCHEMA, kind: 'release', id: owner.lease });
|
|
633
|
+
child.stdin.end();
|
|
634
|
+
}
|
|
635
|
+
if (!finished)
|
|
636
|
+
throw failed();
|
|
637
|
+
})();
|
|
638
|
+
// Start both handlers in this turn, before any awaited operation. Even a
|
|
639
|
+
// constructor failure goes through descriptor and child teardown below.
|
|
640
|
+
let sending = Promise.resolve();
|
|
641
|
+
try {
|
|
642
|
+
relay = new LocalModelRelay(configuration, write, combined);
|
|
643
|
+
sending = bootstrap(owner, inputs, configuration, write, combined);
|
|
644
|
+
// Both operations have handlers immediately. A malformed/closed channel
|
|
645
|
+
// aborts input writes, and no start/bootstrap call is retried.
|
|
646
|
+
await Promise.all([sending, reading]);
|
|
647
|
+
const exit = await closed, row = await inspected(context, value, combined);
|
|
648
|
+
if (!row || row.State?.Running || row.State?.Restarting || row.State?.Status !== 'exited'
|
|
649
|
+
|| !Number.isSafeInteger(row.State?.ExitCode) || exit === null || exit !== row.State.ExitCode)
|
|
650
|
+
throw failed();
|
|
651
|
+
requireContainerBoundary(row, policy, selected);
|
|
652
|
+
value.stage = 'container_exited';
|
|
653
|
+
value.exit_code = row.State.ExitCode;
|
|
654
|
+
await retain(value);
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
finalSession?.statusBytes?.fill(0);
|
|
659
|
+
relay?.close();
|
|
660
|
+
session?.close();
|
|
661
|
+
stopping.abort();
|
|
662
|
+
await campaignControls?.stop();
|
|
663
|
+
child.stdin.destroy();
|
|
664
|
+
child.stdout.destroy();
|
|
665
|
+
child.kill('SIGTERM');
|
|
666
|
+
// Killing the CLI is not container cleanup. The caller always resolves the
|
|
667
|
+
// owned container separately, under a fresh bounded cleanup signal.
|
|
668
|
+
let timer;
|
|
669
|
+
try {
|
|
670
|
+
await Promise.race([Promise.allSettled([sending, reading, closed, sessionWork]), new Promise(resolve => {
|
|
671
|
+
timer = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, 2000);
|
|
672
|
+
})]);
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
if (timer)
|
|
676
|
+
clearTimeout(timer);
|
|
677
|
+
await live?.close();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/** Internal executor; public run controls wait for completion/evidence wiring. */
|
|
682
|
+
async function executeLocalContainer(owner, inputs, configuration, collector, signal) {
|
|
683
|
+
if (!owner.resourceJournalRetained)
|
|
684
|
+
throw invalid();
|
|
685
|
+
if (owner.purpose !== collector.purpose || (collector.purpose === 'world_session'
|
|
686
|
+
? owner.purpose !== 'world_session' || configuration.control.schema !== 'firedrill.world-session.v1'
|
|
687
|
+
|| configuration.control.purpose !== 'world_inspection_session'
|
|
688
|
+
|| configuration.world_session?.session_id !== owner.session.session_id
|
|
689
|
+
|| configuration.world_session?.input_sha256 !== owner.session.input_sha256
|
|
690
|
+
: collector.purpose === 'trial'
|
|
691
|
+
? configuration.control.schema !== 'firedrill.trial.v1' || configuration.control.purpose !== 'simulation_trial'
|
|
692
|
+
: configuration.control.schema !== 'firedrill.validation.v1' || configuration.control.purpose !== 'world_validation')
|
|
693
|
+
|| configuration.operation_id !== owner.lease || configuration.job_id !== owner.job
|
|
694
|
+
|| configuration.attempt_id !== owner.attempt || configuration.manifest_sha256 !== inputs.manifestSha256)
|
|
695
|
+
throw invalid();
|
|
696
|
+
return locked(owner, owner.lease, async (lock) => {
|
|
697
|
+
signal = AbortSignal.any([signal, lock.signal]);
|
|
698
|
+
const value = { schema: 'firedrill.local-container.v1', owner: owner.owner, workspace: owner.workspace, world: owner.world,
|
|
699
|
+
purpose: collector.purpose, device: owner.device, job: owner.job, attempt: owner.attempt, lease: owner.lease, request: owner.request,
|
|
700
|
+
manifest_sha256: inputs.manifestSha256, configuration_digest: owner.configurationDigest, image: owner.runtime.image,
|
|
701
|
+
source_revision: owner.runtime.source_revision, name: 'fd-job-' + owner.lease, container_id: null, stage: 'preflight',
|
|
702
|
+
boundary: null, security: null,
|
|
703
|
+
launch: null, exit_code: null, status: null };
|
|
704
|
+
if (owner.purpose === 'world_session') {
|
|
705
|
+
value.world_session = owner.session;
|
|
706
|
+
value.session_released = false;
|
|
707
|
+
value.world_final_status = null;
|
|
708
|
+
value.world_journal = { owner: owner.owner, workspace: owner.workspace, device: owner.device, lease: owner.lease, attempt: owner.attempt,
|
|
709
|
+
nativeSession: owner.nativeSession, job: owner.job, request: owner.request, grant: owner.grant, payloadDigest: owner.payloadDigest,
|
|
710
|
+
session: owner.session, evidenceUpload: owner.evidenceUpload };
|
|
711
|
+
}
|
|
712
|
+
await retain(value, true); // Existing/uncertain request refuses replacement.
|
|
713
|
+
// The owner fence follows durable resource intent and precedes all Docker
|
|
714
|
+
// interaction. A crash cannot leave dispatched work with no cleanup journal.
|
|
715
|
+
await owner.resourceJournalRetained();
|
|
716
|
+
let result, executionError, cleaned = false, context;
|
|
717
|
+
try {
|
|
718
|
+
if (owner.purpose === 'world_session') {
|
|
719
|
+
const journal = await LocalWorldJournal.acquire(owner, inputs.manifestSha256, { retainUntil: inputs.manifest.evidence_recovery_until });
|
|
720
|
+
await journal.close(); // Real frozen manifest policy before native work.
|
|
721
|
+
}
|
|
722
|
+
context = await docker();
|
|
723
|
+
const arch = hostArch(), response = await command(context, ['info', '--format', '{{json .}}'], { signal, maximum: 128 * 1024 });
|
|
724
|
+
let host;
|
|
725
|
+
try {
|
|
726
|
+
host = JSON.parse(response.output);
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
throw failed();
|
|
730
|
+
}
|
|
731
|
+
const daemonArch = host.Architecture === 'x86_64' ? 'amd64' : host.Architecture === 'aarch64' ? 'arm64' : host.Architecture;
|
|
732
|
+
if (response.code !== 0 || host.OSType !== 'linux' || arch !== owner.runtime.arch || daemonArch !== arch
|
|
733
|
+
|| !Number.isSafeInteger(host.NCPU) || host.NCPU < owner.limits.cpu_units || !Number.isSafeInteger(host.MemTotal)
|
|
734
|
+
|| host.MemTotal < owner.limits.memory_mb * 1024 ** 2) {
|
|
735
|
+
throw new CliError('local_container_capacity_required', 'The approved local image and CPU/memory resources are not available on this device. No emulation or cloud fallback was selected.', 2);
|
|
736
|
+
}
|
|
737
|
+
const policy = await containerPolicy(arch, host.SecurityOptions), selected = boundary(owner, policy, inputs);
|
|
738
|
+
if (!await imagePresent(context, owner.runtime, signal))
|
|
739
|
+
throw new CliError('runtime_image_required', 'Install the exact approved execution image first. Nothing was downloaded or started.', 2);
|
|
740
|
+
value.boundary = selected;
|
|
741
|
+
value.security = { seccomp_sha256: policy.seccomp.sha256,
|
|
742
|
+
apparmor_profile: policy.requiredApparmor ? policy.apparmor.name : null };
|
|
743
|
+
value.stage = 'create_dispatched';
|
|
744
|
+
await retain(value);
|
|
745
|
+
const created = await command(context, ['container', 'create', '--name', value.name, '--pull', 'never', '--platform', `linux/${arch}`,
|
|
746
|
+
...Object.entries(labels(value)).flatMap(([key, item]) => ['--label', key + '=' + item]), ...boundaryArguments(selected, policy)], { signal });
|
|
747
|
+
if (created.code !== 0 || !HASH.test(created.output.trim()))
|
|
748
|
+
throw failed();
|
|
749
|
+
value.container_id = created.output.trim();
|
|
750
|
+
value.stage = 'created';
|
|
751
|
+
await retain(value);
|
|
752
|
+
const row = await inspected(context, value, signal);
|
|
753
|
+
if (!row || row.State?.Status !== 'created' || row.State?.Running || row.State?.Restarting)
|
|
754
|
+
throw failed();
|
|
755
|
+
requireContainerBoundary(row, policy, selected);
|
|
756
|
+
const deadline = AbortSignal.any([signal, AbortSignal.timeout(localExecutionTimeout(configuration, 600000))]);
|
|
757
|
+
result = await connected(context, value, owner, inputs, configuration, policy, collector, deadline);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
executionError = error;
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
try {
|
|
764
|
+
cleaned = await cleanup(context, value, AbortSignal.timeout(15000));
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
cleaned = false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (!cleaned)
|
|
771
|
+
throw new CliError('local_container_cleanup_pending', 'Exact container cleanup is not confirmed. Its journal remains for recovery. Cloud completion was not implied.');
|
|
772
|
+
if (executionError)
|
|
773
|
+
throw executionError;
|
|
774
|
+
return result;
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
export async function executeLocalValidation(owner, inputs, configuration, collect, signal) {
|
|
778
|
+
return executeLocalContainer(owner, inputs, configuration, { purpose: 'validation', collect }, signal);
|
|
779
|
+
}
|
|
780
|
+
export async function executeLocalTrial(owner, inputs, configuration, collect, signal, live, liveArtifact) {
|
|
781
|
+
return executeLocalContainer(owner, inputs, configuration, { purpose: 'trial', collect, live, liveArtifact }, signal);
|
|
782
|
+
}
|
|
783
|
+
/** Existing source approval, one claim and keeper own the entire execution. */
|
|
784
|
+
export async function withOwnedLocalValidation(context, device, job, grantRequest, signal, collect) {
|
|
785
|
+
return withLocalJobInputs(context, device, job, grantRequest, 'validation', signal, (owner, inputs, configuration, active) => executeLocalValidation(owner, inputs, configuration, collect, active));
|
|
786
|
+
}
|
|
787
|
+
/** Internal manual-world lane. Public creation/claim waits for cloud lifecycle,
|
|
788
|
+
* signed command/evidence consumers and browser/native handoff to be complete. */
|
|
789
|
+
export async function executeLocalWorldSession(owner, inputs, configuration, collector, signal) {
|
|
790
|
+
return executeLocalContainer(owner, inputs, configuration, { purpose: 'world_session', ...collector }, signal);
|
|
791
|
+
}
|
|
792
|
+
export async function withOwnedLocalWorldSession(context, device, job, grantRequest, signal, collector) {
|
|
793
|
+
return withLocalJobInputs(context, device, job, grantRequest, 'world_session', signal, (owner, inputs, configuration, active) => executeLocalWorldSession(owner, inputs, configuration, collector, active));
|
|
794
|
+
}
|