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,332 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, rename } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import { readLocalClaim, changeLocalClaim } from './runner-claim-reads.js';
|
|
7
|
+
import { privateDirectory } from './credentials.js';
|
|
8
|
+
import { processLifetimeLock } from './runner-process-lock.js';
|
|
9
|
+
import { CliError } from './http.js';
|
|
10
|
+
import { executionPermission } from './runner-grants.js';
|
|
11
|
+
import { receiveLocalInputs, discardPrivateDataInputs } from './runner-inputs.js';
|
|
12
|
+
import { clearRuntimeConfiguration, receiveRuntimeConfiguration } from './runner-configuration.js';
|
|
13
|
+
import { signRunnerRequest } from './runner-identity.js';
|
|
14
|
+
import { identifier } from './world-contracts.js';
|
|
15
|
+
function canonical(value) {
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return '[' + value.map(canonical).join(',') + ']';
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
const row = value;
|
|
20
|
+
return '{' + Object.keys(row).sort().map(key => JSON.stringify(key) + ':' + canonical(row[key])).join(',') + '}';
|
|
21
|
+
}
|
|
22
|
+
const body = JSON.stringify(value);
|
|
23
|
+
if (body === undefined)
|
|
24
|
+
throw new CliError('local_owner_journal_invalid', 'The retained ownership record is incomplete.');
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
27
|
+
const digest = (value) => createHash('sha256').update(canonical(value)).digest('hex');
|
|
28
|
+
const invalid = () => new CliError('local_owner_identity_changed', 'Local ownership does not match the exact retained job. No replacement claim or launch was sent.');
|
|
29
|
+
const pathFor = async (scope, job) => join(await privateDirectory(), `job-owner-${scope.owner}-${scope.workspace}-${identifier(job)}.json`);
|
|
30
|
+
const writes = new Map();
|
|
31
|
+
async function persist(value, initial = false) {
|
|
32
|
+
const path = await pathFor({ ...value, prefix: '' }, value.job);
|
|
33
|
+
const body = JSON.stringify(value);
|
|
34
|
+
// Input progress and the lease keeper share this journal. Preserve dispatch
|
|
35
|
+
// ordering even while a file sync is pending; never clear an uncertain pulse
|
|
36
|
+
// with an older concurrent stage snapshot.
|
|
37
|
+
const write = (writes.get(path) ?? Promise.resolve()).then(async () => {
|
|
38
|
+
if (initial) {
|
|
39
|
+
const file = await open(path, 'wx', 0o600);
|
|
40
|
+
try {
|
|
41
|
+
await file.writeFile(body);
|
|
42
|
+
await file.sync();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await file.close();
|
|
46
|
+
}
|
|
47
|
+
const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY | (constants.O_NOFOLLOW ?? 0));
|
|
48
|
+
try {
|
|
49
|
+
await directory.sync();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await directory.close();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const temporary = path + '.' + randomUUID() + '.tmp';
|
|
57
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
58
|
+
try {
|
|
59
|
+
await file.writeFile(body);
|
|
60
|
+
await file.sync();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await file.close();
|
|
64
|
+
}
|
|
65
|
+
await rename(temporary, path);
|
|
66
|
+
const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY | (constants.O_NOFOLLOW ?? 0));
|
|
67
|
+
try {
|
|
68
|
+
await directory.sync();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await directory.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
writes.set(path, write);
|
|
75
|
+
try {
|
|
76
|
+
await write;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (writes.get(path) === write)
|
|
80
|
+
writes.delete(path);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function load(context, job) {
|
|
84
|
+
const path = await pathFor(context, job);
|
|
85
|
+
const before = await lstat(path).catch(error => { if (error.code === 'ENOENT')
|
|
86
|
+
return null; throw error; });
|
|
87
|
+
if (!before)
|
|
88
|
+
return null;
|
|
89
|
+
if (!before.isFile() || before.isSymbolicLink() || before.mode & 0o077 || before.nlink !== 1 || before.size > 32768
|
|
90
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
91
|
+
throw invalid();
|
|
92
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
93
|
+
try {
|
|
94
|
+
const actual = await file.stat();
|
|
95
|
+
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size)
|
|
96
|
+
throw invalid();
|
|
97
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
98
|
+
if (value.schema !== 'firedrill.local-job-owner.v1' || value.owner !== context.owner || value.workspace !== context.workspace
|
|
99
|
+
|| value.world !== context.world || value.job !== job || !['claim_dispatched', 'claimed', 'inputs_received', 'work_dispatched', 'not_dispatched'].includes(value.stage)
|
|
100
|
+
|| value.proof?.payload?.schema !== 'firedrill.local-job-claim.v1' || value.proof.payload.request_key !== value.request
|
|
101
|
+
|| value.proof.payload.job_id !== job || value.proof.payload.device_key !== value.device || value.proof.payload.grant_id !== value.grant
|
|
102
|
+
|| value.runtimeImage !== undefined && !/^[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}$/.test(value.runtimeImage)
|
|
103
|
+
|| typeof value.proof.signature !== 'string' || value.proof.signature.length !== 88)
|
|
104
|
+
throw invalid();
|
|
105
|
+
for (const id of [value.request, value.device, value.grant, value.grantRequest])
|
|
106
|
+
identifier(id);
|
|
107
|
+
if (value.lease !== null)
|
|
108
|
+
identifier(value.lease);
|
|
109
|
+
if (value.attempt !== null)
|
|
110
|
+
identifier(value.attempt);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw invalid();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await file.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Read only the original local intent. Never invent a replacement lookup key. */
|
|
121
|
+
export async function retainedLocalClaim(context, device, job) {
|
|
122
|
+
const value = await load(context, identifier(job));
|
|
123
|
+
if (value && value.device !== identifier(device))
|
|
124
|
+
throw invalid();
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
/** A durable fence under the same lock as execution, not an expired-lease guess.
|
|
128
|
+
* The caller must also exclude a resource journal before accepting the fence.
|
|
129
|
+
*/
|
|
130
|
+
export async function withUnstartedLocalClaim(context, device, job, signal, work) {
|
|
131
|
+
job = identifier(job);
|
|
132
|
+
device = identifier(device);
|
|
133
|
+
const lock = await processLifetimeLock(join(await privateDirectory(), `job-lock-${context.owner}-${context.workspace}-${job}`));
|
|
134
|
+
try {
|
|
135
|
+
const active = AbortSignal.any([signal, lock.signal]);
|
|
136
|
+
lock.assertHeld();
|
|
137
|
+
active.throwIfAborted();
|
|
138
|
+
const value = await load(context, job);
|
|
139
|
+
if (!value || value.device !== device)
|
|
140
|
+
throw invalid();
|
|
141
|
+
if (value.stage === 'work_dispatched')
|
|
142
|
+
throw new CliError('local_resource_recovery_required', 'This job handed off to a container. Use runner cleanup and evidence sync for the original operation; it cannot be abandoned as unstarted.');
|
|
143
|
+
const receipt = await readLocalClaim(value, device, job, value.request, active);
|
|
144
|
+
match(value, receipt, false);
|
|
145
|
+
if (!receipt.claim?.attempt_id)
|
|
146
|
+
throw new CliError('local_claim_unconfirmed', 'The original claim has no confirmed attempt. Keep its journal and inspect the same request later; it was not repeated.');
|
|
147
|
+
value.lease = receipt.claim.id;
|
|
148
|
+
value.attempt = receipt.claim.attempt_id;
|
|
149
|
+
return await work(value, receipt, async () => { lock.assertHeld(); value.stage = 'not_dispatched'; await persist(value); }, active);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await lock.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function match(value, receipt, active = true) {
|
|
156
|
+
if (receipt.actor_id !== value.owner || receipt.workspace_id !== value.workspace || receipt.native_session_id !== value.proof.payload.native_session_id
|
|
157
|
+
|| receipt.job_id !== value.job || receipt.world_id !== value.world || receipt.grant_id !== value.grant
|
|
158
|
+
|| receipt.device_key !== value.device || receipt.payload_digest !== value.proof.payload.payload_digest)
|
|
159
|
+
throw invalid();
|
|
160
|
+
if (!receipt.claim) {
|
|
161
|
+
if (active || receipt.request_key !== value.request)
|
|
162
|
+
throw invalid();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const claim = receipt.claim;
|
|
166
|
+
if (claim.request_key !== value.request || claim.request_digest !== digest(value.proof.payload)
|
|
167
|
+
|| value.lease && claim.id !== value.lease || value.attempt && claim.attempt_id !== value.attempt)
|
|
168
|
+
throw invalid();
|
|
169
|
+
identifier(claim.id);
|
|
170
|
+
if (claim.attempt_id)
|
|
171
|
+
identifier(claim.attempt_id);
|
|
172
|
+
if (active && (claim.state !== 'claimed' || !claim.attempt_id || !Number.isSafeInteger(Date.parse(receipt.server_time))
|
|
173
|
+
|| !Number.isFinite(Date.parse(claim.expires_at)) || Date.parse(claim.expires_at) <= Date.parse(receipt.server_time))) {
|
|
174
|
+
throw new CliError('local_claim_expired', 'This claim is no longer active. Use runner abandon <device> <job> for an unstarted claim, or runner cleanup for its original container. It was not replaced.');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function pulse(value, signal) {
|
|
178
|
+
const receipt = await readLocalClaim(value, value.device, value.job, value.request, signal);
|
|
179
|
+
match(value, receipt);
|
|
180
|
+
if (!receipt.claim || !Number.isSafeInteger(receipt.claim.last_sequence) || receipt.claim.last_sequence < 0
|
|
181
|
+
|| receipt.claim.last_sequence >= Number.MAX_SAFE_INTEGER)
|
|
182
|
+
throw invalid();
|
|
183
|
+
if (value.pending) {
|
|
184
|
+
const pending = value.pending.payload;
|
|
185
|
+
if (pending.schema !== 'firedrill.local-job-presence.v1' || pending.lease_id !== value.lease
|
|
186
|
+
|| pending.request_key !== value.request || pending.job_id !== value.job || pending.device_key !== value.device)
|
|
187
|
+
throw invalid();
|
|
188
|
+
if (receipt.claim.last_sequence !== pending.sequence || receipt.claim.last_pulse_digest !== digest(pending)) {
|
|
189
|
+
throw new CliError('local_claim_pulse_uncertain', 'The previous ownership pulse is unresolved. No pulse or work was replayed.');
|
|
190
|
+
}
|
|
191
|
+
value.pending = null;
|
|
192
|
+
await persist(value);
|
|
193
|
+
}
|
|
194
|
+
const sent = Date.parse(receipt.server_time);
|
|
195
|
+
if (!Number.isSafeInteger(sent))
|
|
196
|
+
throw invalid();
|
|
197
|
+
const nativeSession = value.proof.payload.native_session_id;
|
|
198
|
+
if (typeof nativeSession !== 'string')
|
|
199
|
+
throw invalid();
|
|
200
|
+
const payload = { schema: 'firedrill.local-job-presence.v1',
|
|
201
|
+
job_id: value.job, device_key: value.device, request_key: value.request,
|
|
202
|
+
native_session_id: identifier(nativeSession), lease_id: identifier(value.lease),
|
|
203
|
+
sent_at_ms: sent, sequence: receipt.claim.last_sequence + 1, state: 'claimed' };
|
|
204
|
+
value.pending = await signRunnerRequest(value, value.device, payload, signal);
|
|
205
|
+
await persist(value);
|
|
206
|
+
const result = await changeLocalClaim(value, value.device, value.job, value.request, 'presence', value.pending, signal);
|
|
207
|
+
match(value, result);
|
|
208
|
+
if (!result.claim || result.claim.last_sequence !== value.pending.payload.sequence || result.claim.last_pulse_digest !== digest(value.pending.payload))
|
|
209
|
+
throw invalid();
|
|
210
|
+
value.pending = null;
|
|
211
|
+
await persist(value);
|
|
212
|
+
}
|
|
213
|
+
/** Internal executor seam: exact claim -> leased input transfer -> owned work.
|
|
214
|
+
* No CLI action is advertised until the container/model/evidence path is wired.
|
|
215
|
+
* The caller owns actual resource cleanup; a returned value is NOT its proof.
|
|
216
|
+
*/
|
|
217
|
+
export async function withLocalJobInputs(context, device, job, grantRequest, purpose, signal, work) {
|
|
218
|
+
device = identifier(device);
|
|
219
|
+
job = identifier(job);
|
|
220
|
+
grantRequest = identifier(grantRequest);
|
|
221
|
+
for (const id of [context.owner, context.workspace, context.world])
|
|
222
|
+
identifier(id);
|
|
223
|
+
const lock = await processLifetimeLock(join(await privateDirectory(), `job-lock-${context.owner}-${context.workspace}-${job}`));
|
|
224
|
+
signal = AbortSignal.any([signal, lock.signal]);
|
|
225
|
+
try {
|
|
226
|
+
lock.assertHeld();
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
const permission = await executionPermission(context, grantRequest, device, purpose, signal);
|
|
229
|
+
let value = await load(context, job);
|
|
230
|
+
if (value?.stage === 'work_dispatched' || value?.stage === 'not_dispatched') {
|
|
231
|
+
throw new CliError('local_execution_recovery_required', 'This job already dispatched owned work. Reconcile the original resources; the callback and target are not replayed.');
|
|
232
|
+
}
|
|
233
|
+
if (!value) {
|
|
234
|
+
const request = randomUUID();
|
|
235
|
+
const offered = await readLocalClaim(context, device, job, request, signal);
|
|
236
|
+
if (offered.claim || offered.job_id !== job || offered.world_id !== context.world || offered.device_key !== device
|
|
237
|
+
|| offered.grant_id !== permission.id || offered.purpose !== purpose || !/^[a-f0-9]{64}$/.test(offered.payload_digest)
|
|
238
|
+
|| !Number.isSafeInteger(Date.parse(offered.server_time)))
|
|
239
|
+
throw invalid();
|
|
240
|
+
const payload = { schema: 'firedrill.local-job-claim.v1', request_key: request,
|
|
241
|
+
job_id: job, device_key: device, native_session_id: permission.document.native_session_id,
|
|
242
|
+
grant_id: permission.id, payload_digest: offered.payload_digest, sent_at_ms: Date.parse(offered.server_time) };
|
|
243
|
+
const proof = await signRunnerRequest(context, device, payload, signal);
|
|
244
|
+
value = { schema: 'firedrill.local-job-owner.v1', owner: context.owner, workspace: context.workspace, world: context.world,
|
|
245
|
+
job, device, request, grantRequest, grant: permission.id, proof, runtimeImage: permission.document.runtime.image,
|
|
246
|
+
stage: 'claim_dispatched', lease: null, attempt: null, pending: null };
|
|
247
|
+
await persist(value, true);
|
|
248
|
+
// Exactly one dispatch. An ambiguous reply is recovered only by GET of
|
|
249
|
+
// this original request; never another PUT/new claim or target retry.
|
|
250
|
+
try {
|
|
251
|
+
match(value, await changeLocalClaim(value, device, job, request, 'claim', proof, signal));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
const retained = await readLocalClaim(value, device, job, request, signal);
|
|
255
|
+
match(value, retained, false);
|
|
256
|
+
if (!retained.claim)
|
|
257
|
+
throw new CliError('local_claim_unconfirmed', 'The saved claim is not confirmed. Keep this journal and inspect its exact request before continuing.');
|
|
258
|
+
match(value, retained);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (value.device !== device || value.grant !== permission.id || value.grantRequest !== grantRequest
|
|
262
|
+
|| value.runtimeImage !== undefined && value.runtimeImage !== permission.document.runtime.image
|
|
263
|
+
|| value.proof.payload.native_session_id !== permission.document.native_session_id)
|
|
264
|
+
throw invalid();
|
|
265
|
+
const receipt = await readLocalClaim(value, value.device, job, value.request, signal);
|
|
266
|
+
match(value, receipt);
|
|
267
|
+
if (!receipt.claim?.attempt_id || receipt.purpose !== purpose)
|
|
268
|
+
throw invalid();
|
|
269
|
+
value.lease = receipt.claim.id;
|
|
270
|
+
value.attempt = receipt.claim.attempt_id;
|
|
271
|
+
value.stage = 'claimed';
|
|
272
|
+
await persist(value);
|
|
273
|
+
const common = { owner: value.owner, workspace: value.workspace, device, job, request: value.request,
|
|
274
|
+
lease: value.lease, attempt: value.attempt, nativeSession: permission.document.native_session_id,
|
|
275
|
+
grant: value.grant, payloadDigest: String(value.proof.payload.payload_digest), runtime: permission.document.runtime,
|
|
276
|
+
evidenceUpload: permission.document.consent.evidence_upload,
|
|
277
|
+
realAccountAccess: permission.document.consent.real_account_access,
|
|
278
|
+
catalogPackagesToDevice: permission.document.consent.catalog_packages_to_device,
|
|
279
|
+
world: context.world, configurationDigest: permission.document.basis.configuration_digest,
|
|
280
|
+
sourceSnapshot: permission.document.basis.source_snapshot_id, sourceRevision: permission.document.basis.source_revision,
|
|
281
|
+
limits: permission.document.limits,
|
|
282
|
+
resourceJournalRetained: async () => { lock.assertHeld(); combined.throwIfAborted(); value.stage = 'work_dispatched'; await persist(value); } };
|
|
283
|
+
let owner;
|
|
284
|
+
if (purpose === 'world_session') {
|
|
285
|
+
const session = receipt.world_session;
|
|
286
|
+
if (!session || Object.keys(session).sort().join(',') !== 'configuration_id,expires_at,input_sha256,session_id'
|
|
287
|
+
|| identifier(session.session_id) !== session.session_id || identifier(session.configuration_id) !== session.configuration_id
|
|
288
|
+
|| !/^[a-f0-9]{64}$/.test(session.input_sha256) || !Number.isSafeInteger(Date.parse(session.expires_at))
|
|
289
|
+
|| Date.parse(session.expires_at) <= Date.parse(receipt.server_time) || !permission.document.consent.evidence_upload)
|
|
290
|
+
throw invalid();
|
|
291
|
+
owner = { ...common, purpose, session };
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
owner = { ...common, purpose };
|
|
295
|
+
const stop = new AbortController(), combined = AbortSignal.any([signal, stop.signal]);
|
|
296
|
+
let failed;
|
|
297
|
+
// Refresh once BEFORE work, then maintain original sequence ownership.
|
|
298
|
+
await pulse(value, signal);
|
|
299
|
+
const keeping = (async () => {
|
|
300
|
+
while (!combined.aborted) {
|
|
301
|
+
await sleep(10000, undefined, { signal: combined });
|
|
302
|
+
await pulse(value, combined);
|
|
303
|
+
}
|
|
304
|
+
})().catch(error => { if (!combined.aborted) {
|
|
305
|
+
failed = error;
|
|
306
|
+
stop.abort();
|
|
307
|
+
} });
|
|
308
|
+
let configuration = null;
|
|
309
|
+
try {
|
|
310
|
+
const inputs = await receiveLocalInputs(owner, combined);
|
|
311
|
+
value.stage = 'inputs_received';
|
|
312
|
+
await persist(value);
|
|
313
|
+
configuration = await receiveRuntimeConfiguration(owner, inputs, combined);
|
|
314
|
+
combined.throwIfAborted();
|
|
315
|
+
const result = await work(owner, inputs, configuration, combined);
|
|
316
|
+
if (failed)
|
|
317
|
+
throw failed;
|
|
318
|
+
lock.assertHeld();
|
|
319
|
+
combined.throwIfAborted();
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
stop.abort();
|
|
324
|
+
await keeping;
|
|
325
|
+
clearRuntimeConfiguration(configuration);
|
|
326
|
+
await discardPrivateDataInputs(owner);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
await lock.close();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { controlPython } from './source-runtime.js';
|
|
4
|
+
import { CliError } from './http.js';
|
|
5
|
+
/** Fixed isolated stdlib only. The lock inode is never unlinked/replaced. The
|
|
6
|
+
* kernel releases flock when this broker exits; parent death closes its stdin.
|
|
7
|
+
* No PID-based takeover, stale timer, target import or provider operation. */
|
|
8
|
+
const BROKER = String.raw `
|
|
9
|
+
import fcntl, json, os, stat, sys
|
|
10
|
+
path, nonce = sys.argv[1:]
|
|
11
|
+
parent, name = os.path.dirname(path), os.path.basename(path)
|
|
12
|
+
directory = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
13
|
+
try:
|
|
14
|
+
base = os.fstat(directory)
|
|
15
|
+
if base.st_uid != os.getuid() or base.st_mode & 0o077:
|
|
16
|
+
raise ValueError()
|
|
17
|
+
fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600, dir_fd=directory)
|
|
18
|
+
try:
|
|
19
|
+
value = os.fstat(fd)
|
|
20
|
+
if not stat.S_ISREG(value.st_mode) or value.st_nlink != 1 or value.st_uid != os.getuid() or value.st_mode & 0o077:
|
|
21
|
+
raise ValueError()
|
|
22
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
23
|
+
current = os.stat(name, dir_fd=directory, follow_symlinks=False)
|
|
24
|
+
if (current.st_dev, current.st_ino) != (value.st_dev, value.st_ino):
|
|
25
|
+
raise ValueError()
|
|
26
|
+
body = json.dumps({'schema': 'firedrill.process-lock.v1', 'nonce': nonce, 'pid': os.getpid()}).encode()
|
|
27
|
+
os.ftruncate(fd, 0)
|
|
28
|
+
os.write(fd, body)
|
|
29
|
+
os.fsync(fd)
|
|
30
|
+
print(json.dumps({'state': 'locked', 'nonce': nonce}), flush=True)
|
|
31
|
+
if sys.stdin.buffer.read(1):
|
|
32
|
+
raise ValueError()
|
|
33
|
+
finally:
|
|
34
|
+
os.close(fd)
|
|
35
|
+
finally:
|
|
36
|
+
os.close(directory)
|
|
37
|
+
`;
|
|
38
|
+
const unavailable = () => new CliError('local_operation_owned', 'This exact local operation is active, its legacy lock needs inspection, or the trusted offline lock helper is unavailable. No lock was stolen or cleanup inferred.');
|
|
39
|
+
export async function processLifetimeLock(path) {
|
|
40
|
+
const controller = new AbortController(), nonce = randomUUID();
|
|
41
|
+
const runtime = await controlPython(AbortSignal.timeout(10000));
|
|
42
|
+
const child = spawn(runtime, ['-I', '-B', '-c', BROKER, path, nonce], {
|
|
43
|
+
shell: false, windowsHide: true, env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' }, stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
let held = false, closing = false, ended = false, transportInvalid = false;
|
|
46
|
+
let failStartup = () => { };
|
|
47
|
+
let rejectTransport = () => { };
|
|
48
|
+
const transportFailure = new Promise((_resolve, reject) => { rejectTransport = reject; });
|
|
49
|
+
void transportFailure.catch(() => undefined);
|
|
50
|
+
const invalidateTransport = () => {
|
|
51
|
+
transportInvalid = true;
|
|
52
|
+
held = false;
|
|
53
|
+
controller.abort();
|
|
54
|
+
rejectTransport(unavailable());
|
|
55
|
+
failStartup();
|
|
56
|
+
};
|
|
57
|
+
// Pipe EPIPE/errors are stream events, not necessarily child-process events.
|
|
58
|
+
// Keep listeners through teardown and never expose transport/provider text.
|
|
59
|
+
child.stdin.on('error', invalidateTransport);
|
|
60
|
+
child.stdout.on('error', invalidateTransport);
|
|
61
|
+
child.stderr.on('error', invalidateTransport);
|
|
62
|
+
child.stderr.resume();
|
|
63
|
+
const exited = new Promise((resolve, reject) => {
|
|
64
|
+
child.once('error', () => { ended = true; held = false; controller.abort(); reject(unavailable()); });
|
|
65
|
+
child.once('close', code => {
|
|
66
|
+
ended = true;
|
|
67
|
+
held = false;
|
|
68
|
+
if (!closing || code !== 0 || transportInvalid) {
|
|
69
|
+
controller.abort();
|
|
70
|
+
reject(unavailable());
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
void exited.catch(() => undefined);
|
|
77
|
+
let closePromise;
|
|
78
|
+
const stop = () => {
|
|
79
|
+
if (closePromise)
|
|
80
|
+
return closePromise;
|
|
81
|
+
closing = true;
|
|
82
|
+
closePromise = (async () => {
|
|
83
|
+
let kill, deadline;
|
|
84
|
+
try {
|
|
85
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
86
|
+
kill = setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
87
|
+
deadline = setTimeout(() => reject(unavailable()), 3000);
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
child.stdin.end();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
invalidateTransport();
|
|
94
|
+
}
|
|
95
|
+
await Promise.race([exited, transportFailure, timeout]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (kill)
|
|
99
|
+
clearTimeout(kill);
|
|
100
|
+
if (deadline)
|
|
101
|
+
clearTimeout(deadline);
|
|
102
|
+
if (transportInvalid && !ended)
|
|
103
|
+
child.kill('SIGKILL');
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return closePromise;
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
let body = '', settled = false;
|
|
111
|
+
const finish = (error) => {
|
|
112
|
+
if (settled)
|
|
113
|
+
return;
|
|
114
|
+
settled = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
if (error)
|
|
117
|
+
reject(error);
|
|
118
|
+
else
|
|
119
|
+
resolve();
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(() => finish(unavailable()), 10000);
|
|
122
|
+
failStartup = () => finish(unavailable());
|
|
123
|
+
if (transportInvalid)
|
|
124
|
+
return finish(unavailable());
|
|
125
|
+
child.stdout.on('data', (part) => {
|
|
126
|
+
if (transportInvalid)
|
|
127
|
+
return finish(unavailable());
|
|
128
|
+
body += part.toString('utf8');
|
|
129
|
+
if (body.length > 1024)
|
|
130
|
+
return finish(unavailable());
|
|
131
|
+
if (!body.endsWith('\n'))
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(body);
|
|
135
|
+
if (!value || Object.keys(value).sort().join(',') !== 'nonce,state' || value.state !== 'locked' || value.nonce !== nonce)
|
|
136
|
+
throw new Error();
|
|
137
|
+
held = true;
|
|
138
|
+
finish();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
finish(unavailable());
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
child.once('error', () => finish(unavailable()));
|
|
145
|
+
child.once('close', () => { if (!held)
|
|
146
|
+
finish(unavailable()); });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
child.kill('SIGTERM');
|
|
151
|
+
await stop().catch(() => undefined);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
return { signal: controller.signal, assertHeld: () => { if (!held || ended || transportInvalid)
|
|
155
|
+
throw unavailable(); }, close: stop };
|
|
156
|
+
}
|
|
157
|
+
/** Release-only coordination. Reacquiring never authorizes replaying the work
|
|
158
|
+
* whose immutable original journal is inspected inside the callback. A legacy
|
|
159
|
+
* directory or unexpected file fails the native owner's existing validation.
|
|
160
|
+
*/
|
|
161
|
+
export async function withProcessLifetimeLock(path, signal, work) {
|
|
162
|
+
signal.throwIfAborted();
|
|
163
|
+
const lock = await processLifetimeLock(path), active = AbortSignal.any([signal, lock.signal]);
|
|
164
|
+
try {
|
|
165
|
+
lock.assertHeld();
|
|
166
|
+
active.throwIfAborted();
|
|
167
|
+
const value = await work(active, lock.signal);
|
|
168
|
+
lock.assertHeld();
|
|
169
|
+
active.throwIfAborted();
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await lock.close();
|
|
174
|
+
}
|
|
175
|
+
}
|