firedrill-run 2.2.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +506 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1627 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +96 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, opendir, rename, unlink } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { privateDirectory } from './credentials.js';
|
|
6
|
+
import { API_ORIGIN, CliError } from './http.js';
|
|
7
|
+
import { identifier } from './world-contracts.js';
|
|
8
|
+
import { processLifetimeLock } from './runner-process-lock.js';
|
|
9
|
+
const MAX = 2 * 1024 ** 2, MAGIC = Buffer.from('FDWE1');
|
|
10
|
+
const encryptedName = /^[a-z0-9][a-z0-9.-]{0,149}\.bin(?:\.[a-f0-9-]{36}\.tmp)?$/;
|
|
11
|
+
const markerTemporary = /^erasure\.json\.[a-f0-9-]{36}\.tmp$/;
|
|
12
|
+
const ERASURE_PASS = 1000;
|
|
13
|
+
const sha = (raw) => createHash('sha256').update(raw).digest('hex');
|
|
14
|
+
const invalid = () => new CliError('local_world_journal_unavailable', 'The original encrypted world journal could not be read safely. No action was repeated.');
|
|
15
|
+
/** Private ciphertext on disk; its random per-lease key lives only in the OS
|
|
16
|
+
* credential store. Target outputs may contain private values before the cloud
|
|
17
|
+
* whole-file inspector sees them, so Unix permissions alone are not enough.
|
|
18
|
+
* This is not source/target/provider credential storage or a new execution lane.
|
|
19
|
+
*/
|
|
20
|
+
export class LocalWorldJournal {
|
|
21
|
+
closed = false;
|
|
22
|
+
retainUntil = Infinity;
|
|
23
|
+
erasureState = null;
|
|
24
|
+
get expiresAt() { return this.retainUntil; }
|
|
25
|
+
get removalStarted() { return this.erasureState !== null; }
|
|
26
|
+
directory;
|
|
27
|
+
key;
|
|
28
|
+
aad;
|
|
29
|
+
ownership;
|
|
30
|
+
get signal() { return this.ownership.signal; }
|
|
31
|
+
keyEntry;
|
|
32
|
+
erasureOnly;
|
|
33
|
+
constructor(directory, key, aad, ownership, keyEntry, erasureOnly = false) {
|
|
34
|
+
this.directory = directory;
|
|
35
|
+
this.key = key;
|
|
36
|
+
this.aad = aad;
|
|
37
|
+
this.ownership = ownership;
|
|
38
|
+
this.keyEntry = keyEntry;
|
|
39
|
+
this.erasureOnly = erasureOnly;
|
|
40
|
+
}
|
|
41
|
+
static async acquire(owner, manifest, options = {}) {
|
|
42
|
+
if (!owner.evidenceUpload || !/^[a-f0-9]{64}$/.test(manifest))
|
|
43
|
+
throw invalid();
|
|
44
|
+
const identity = [owner.owner, owner.workspace, owner.device, owner.lease].map(identifier).join('/');
|
|
45
|
+
const directory = join(await privateDirectory(), 'world-' + identity.replaceAll('/', '-'));
|
|
46
|
+
if (!options.existingOnly)
|
|
47
|
+
await mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
48
|
+
throw error; });
|
|
49
|
+
const info = await lstat(directory);
|
|
50
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.mode & 0o077 || process.getuid && info.uid !== process.getuid())
|
|
51
|
+
throw invalid();
|
|
52
|
+
const lock = await processLifetimeLock(join(directory, 'owner.lock'));
|
|
53
|
+
let key = null;
|
|
54
|
+
try {
|
|
55
|
+
const { Entry } = await import('@napi-rs/keyring');
|
|
56
|
+
lock.assertHeld();
|
|
57
|
+
const store = new Entry('run.firedrill.simulation.world-evidence.v1', API_ORIGIN + '/' + identity, process.platform === 'linux' ? { linux: { store: 'secret-service' } } : undefined);
|
|
58
|
+
const erasure = await lstat(join(directory, 'erasure.json')).catch(error => { if (error.code === 'ENOENT')
|
|
59
|
+
return null; throw error; });
|
|
60
|
+
if (erasure && !options.erasureOnly)
|
|
61
|
+
throw new CliError('local_world_journal_erased', 'This private journal is being removed or was removed. No replacement key or target is created.');
|
|
62
|
+
let encoded = store.getPassword();
|
|
63
|
+
if (encoded === null) {
|
|
64
|
+
if (options.erasureOnly && erasure)
|
|
65
|
+
key = Buffer.alloc(0); // Erasure recovery only, never a decryption fallback.
|
|
66
|
+
else {
|
|
67
|
+
if (options.existingOnly || options.erasureOnly)
|
|
68
|
+
throw invalid();
|
|
69
|
+
// Never create a replacement key when an earlier ciphertext exists.
|
|
70
|
+
const marker = await lstat(join(directory, 'identity.bin')).catch(error => { if (error.code === 'ENOENT')
|
|
71
|
+
return null; throw error; });
|
|
72
|
+
if (marker)
|
|
73
|
+
throw invalid();
|
|
74
|
+
key = randomBytes(32);
|
|
75
|
+
encoded = key.toString('base64url');
|
|
76
|
+
store.setPassword(encoded);
|
|
77
|
+
if (store.getPassword() !== encoded)
|
|
78
|
+
throw invalid();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(encoded))
|
|
83
|
+
throw invalid();
|
|
84
|
+
key = Buffer.from(encoded, 'base64url');
|
|
85
|
+
if (key.length !== 32 || key.toString('base64url') !== encoded)
|
|
86
|
+
throw invalid();
|
|
87
|
+
}
|
|
88
|
+
const aad = Buffer.from(JSON.stringify({ schema: 'firedrill.local-world-journal.v1', origin: API_ORIGIN,
|
|
89
|
+
owner: owner.owner, workspace: owner.workspace, device: owner.device, lease: owner.lease, attempt: owner.attempt,
|
|
90
|
+
native_session: owner.nativeSession, job: owner.job, request: owner.request, grant: owner.grant, payload: owner.payloadDigest,
|
|
91
|
+
session: { session_id: owner.session.session_id, configuration_id: owner.session.configuration_id,
|
|
92
|
+
input_sha256: owner.session.input_sha256, expires_at: owner.session.expires_at }, manifest }));
|
|
93
|
+
lock.assertHeld();
|
|
94
|
+
const journal = new LocalWorldJournal(directory, key, aad, lock, store, options.erasureOnly);
|
|
95
|
+
if (erasure) {
|
|
96
|
+
const original = await journal.erasure();
|
|
97
|
+
if (!original)
|
|
98
|
+
throw invalid();
|
|
99
|
+
journal.erasureState = original.state;
|
|
100
|
+
return journal;
|
|
101
|
+
}
|
|
102
|
+
if (options.existingOnly || options.erasureOnly) {
|
|
103
|
+
const retained = await journal.get('identity');
|
|
104
|
+
try {
|
|
105
|
+
if (!retained || retained.length !== aad.length || sha(retained) !== sha(aad))
|
|
106
|
+
throw invalid();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
retained?.fill(0);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
await journal.put('identity', aad);
|
|
114
|
+
const retention = await journal.get('retention');
|
|
115
|
+
try {
|
|
116
|
+
if (retention) {
|
|
117
|
+
const value = JSON.parse(retention.toString('utf8'));
|
|
118
|
+
if (!value || Object.keys(value).sort().join(',') !== 'expires_at,manifest_sha256,schema'
|
|
119
|
+
|| value.schema !== 'firedrill.local-world-retention.v1' || value.manifest_sha256 !== manifest
|
|
120
|
+
|| typeof value.expires_at !== 'string' || !Number.isFinite(Date.parse(value.expires_at))
|
|
121
|
+
|| options.retainUntil && options.retainUntil !== value.expires_at)
|
|
122
|
+
throw invalid();
|
|
123
|
+
journal.retainUntil = Date.parse(value.expires_at);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (options.erasureOnly)
|
|
127
|
+
return journal; // Explicit cleanup can remove a partially initialized owned journal.
|
|
128
|
+
if (options.existingOnly || options.erasureOnly || !options.retainUntil || !Number.isFinite(Date.parse(options.retainUntil)))
|
|
129
|
+
throw invalid();
|
|
130
|
+
const raw = Buffer.from(JSON.stringify({ schema: 'firedrill.local-world-retention.v1', manifest_sha256: manifest, expires_at: options.retainUntil }));
|
|
131
|
+
try {
|
|
132
|
+
await journal.put('retention', raw);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
raw.fill(0);
|
|
136
|
+
}
|
|
137
|
+
journal.retainUntil = Date.parse(options.retainUntil);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
retention?.fill(0);
|
|
142
|
+
}
|
|
143
|
+
if (!options.erasureOnly && journal.retainUntil <= Date.now()) {
|
|
144
|
+
throw new CliError('local_world_journal_expired', 'Local evidence retention ended. Remove this operation with firedrill runner evidence forget. No target is run to reconstruct it.');
|
|
145
|
+
}
|
|
146
|
+
return journal;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
key?.fill(0);
|
|
150
|
+
await lock.close();
|
|
151
|
+
if (error instanceof CliError)
|
|
152
|
+
throw error;
|
|
153
|
+
throw new CliError('local_world_journal_key_unavailable', 'Unlock the OS credential store for private world evidence. No plaintext fallback or target restart is used.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
path(name) {
|
|
157
|
+
this.ownership.assertHeld();
|
|
158
|
+
if (this.closed || !/^[a-z0-9][a-z0-9.-]{0,149}$/.test(name))
|
|
159
|
+
throw invalid();
|
|
160
|
+
if (!this.erasureOnly && this.retainUntil <= Date.now())
|
|
161
|
+
throw new CliError('local_world_journal_expired', 'Local evidence retention ended. Use firedrill runner evidence forget for this operation.');
|
|
162
|
+
return join(this.directory, name + '.bin');
|
|
163
|
+
}
|
|
164
|
+
async readEnvelope(name) {
|
|
165
|
+
const path = this.path(name);
|
|
166
|
+
const before = await lstat(path).catch(error => { if (error.code === 'ENOENT')
|
|
167
|
+
return null; throw error; });
|
|
168
|
+
if (!before)
|
|
169
|
+
return null;
|
|
170
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.mode & 0o077 || before.size > MAX + 64
|
|
171
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
172
|
+
throw invalid();
|
|
173
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
174
|
+
try {
|
|
175
|
+
const actual = await file.stat();
|
|
176
|
+
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size)
|
|
177
|
+
throw invalid();
|
|
178
|
+
const raw = await file.readFile(), after = await file.stat();
|
|
179
|
+
if (after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
180
|
+
raw.fill(0);
|
|
181
|
+
throw invalid();
|
|
182
|
+
}
|
|
183
|
+
return raw;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await file.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async get(name) {
|
|
190
|
+
if (this.key.length !== 32)
|
|
191
|
+
throw invalid();
|
|
192
|
+
const raw = await this.readEnvelope(name);
|
|
193
|
+
if (!raw)
|
|
194
|
+
return null;
|
|
195
|
+
try {
|
|
196
|
+
if (raw.length < 33 || !raw.subarray(0, 5).equals(MAGIC))
|
|
197
|
+
throw invalid();
|
|
198
|
+
const decipher = createDecipheriv('aes-256-gcm', this.key, raw.subarray(5, 17));
|
|
199
|
+
decipher.setAAD(Buffer.concat([this.aad, Buffer.from('\0' + name)]));
|
|
200
|
+
decipher.setAuthTag(raw.subarray(17, 33));
|
|
201
|
+
return Buffer.concat([decipher.update(raw.subarray(33)), decipher.final()]);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
throw invalid();
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
raw.fill(0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async put(name, body, replace = false) {
|
|
211
|
+
if (this.erasureOnly || this.key.length !== 32)
|
|
212
|
+
throw invalid();
|
|
213
|
+
const path = this.path(name);
|
|
214
|
+
if (!body.length || body.length > MAX)
|
|
215
|
+
throw invalid();
|
|
216
|
+
const previous = await this.get(name);
|
|
217
|
+
if (previous) {
|
|
218
|
+
try {
|
|
219
|
+
if (previous.length === body.length && sha(previous) === sha(body))
|
|
220
|
+
return;
|
|
221
|
+
if (!replace)
|
|
222
|
+
throw invalid();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
previous.fill(0);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const iv = randomBytes(12), cipher = createCipheriv('aes-256-gcm', this.key, iv);
|
|
229
|
+
cipher.setAAD(Buffer.concat([this.aad, Buffer.from('\0' + name)]));
|
|
230
|
+
const encrypted = Buffer.concat([cipher.update(body), cipher.final()]);
|
|
231
|
+
const envelope = Buffer.concat([MAGIC, iv, cipher.getAuthTag(), encrypted]);
|
|
232
|
+
const pending = path + '.' + randomUUID() + '.tmp';
|
|
233
|
+
const file = await open(pending, 'wx', 0o600);
|
|
234
|
+
try {
|
|
235
|
+
await file.writeFile(envelope);
|
|
236
|
+
await file.sync();
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
await file.close();
|
|
240
|
+
envelope.fill(0);
|
|
241
|
+
encrypted.fill(0);
|
|
242
|
+
}
|
|
243
|
+
this.ownership.assertHeld();
|
|
244
|
+
await rename(pending, path);
|
|
245
|
+
const parent = await open(this.directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
246
|
+
try {
|
|
247
|
+
await parent.sync();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await parent.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async *commandIdentities(signal) {
|
|
254
|
+
this.ownership.assertHeld();
|
|
255
|
+
if (this.closed)
|
|
256
|
+
throw invalid();
|
|
257
|
+
// Close the directory on exhaustion, cancellation or an early consumer
|
|
258
|
+
// return. Never enumerate all artifact parts just to find command metadata.
|
|
259
|
+
for await (const item of await opendir(this.directory, { bufferSize: 32 })) {
|
|
260
|
+
signal?.throwIfAborted();
|
|
261
|
+
this.ownership.assertHeld();
|
|
262
|
+
if (this.closed)
|
|
263
|
+
throw invalid();
|
|
264
|
+
const match = /^command-([a-f0-9-]{36})-identity\.bin$/.exec(item.name);
|
|
265
|
+
if (match)
|
|
266
|
+
yield identifier(match[1]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async privateBytes(name, maximum) {
|
|
270
|
+
this.ownership.assertHeld();
|
|
271
|
+
const path = join(this.directory, name), before = await lstat(path).catch(error => { if (error.code === 'ENOENT')
|
|
272
|
+
return null; throw error; });
|
|
273
|
+
if (!before)
|
|
274
|
+
return null;
|
|
275
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.mode & 0o077 || before.size > maximum
|
|
276
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
277
|
+
throw invalid();
|
|
278
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
279
|
+
try {
|
|
280
|
+
const actual = await file.stat();
|
|
281
|
+
if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size !== before.size)
|
|
282
|
+
throw invalid();
|
|
283
|
+
const raw = await file.readFile(), after = await file.stat();
|
|
284
|
+
if (after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
285
|
+
raw.fill(0);
|
|
286
|
+
throw invalid();
|
|
287
|
+
}
|
|
288
|
+
return raw;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await file.close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async erasure() {
|
|
295
|
+
const raw = await this.privateBytes('erasure.json', 8192);
|
|
296
|
+
if (!raw)
|
|
297
|
+
return null;
|
|
298
|
+
try {
|
|
299
|
+
const value = JSON.parse(raw.toString('utf8'));
|
|
300
|
+
if (!value || Object.keys(value).sort().join(',') !== 'file_count,finished_at,identity_sha256,pending_file,requested_at,schema,state'
|
|
301
|
+
|| value.schema !== 'firedrill.local-world-erasure.v2' || value.identity_sha256 !== sha(this.aad)
|
|
302
|
+
|| !['erasing', 'removed'].includes(value.state) || !Number.isFinite(Date.parse(value.requested_at))
|
|
303
|
+
|| !Number.isSafeInteger(value.file_count) || value.file_count < 0
|
|
304
|
+
|| value.state === 'removed' && (value.pending_file !== null || !value.finished_at || !Number.isFinite(Date.parse(value.finished_at)))
|
|
305
|
+
|| value.state === 'erasing' && value.finished_at !== null)
|
|
306
|
+
throw invalid();
|
|
307
|
+
const one = value.pending_file;
|
|
308
|
+
if (one !== null && (!one || Object.keys(one).sort().join(',') !== 'device,inode,name,size'
|
|
309
|
+
|| !encryptedName.test(one.name) && !markerTemporary.test(one.name)
|
|
310
|
+
|| !Number.isSafeInteger(one.size) || one.size < 0 || one.size > (markerTemporary.test(one.name) ? 8192 : MAX + 64)
|
|
311
|
+
|| typeof one.device !== 'string' || !/^[0-9]+$/.test(one.device)
|
|
312
|
+
|| typeof one.inode !== 'string' || !/^[0-9]+$/.test(one.inode)))
|
|
313
|
+
throw invalid();
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
raw.fill(0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async writeErasure(value) {
|
|
321
|
+
this.ownership.assertHeld();
|
|
322
|
+
const path = join(this.directory, 'erasure.json'), pending = path + '.' + randomUUID() + '.tmp';
|
|
323
|
+
const body = Buffer.from(JSON.stringify(value));
|
|
324
|
+
if (body.length > 8192) {
|
|
325
|
+
body.fill(0);
|
|
326
|
+
throw invalid();
|
|
327
|
+
}
|
|
328
|
+
const file = await open(pending, 'wx', 0o600);
|
|
329
|
+
try {
|
|
330
|
+
await file.writeFile(body);
|
|
331
|
+
await file.sync();
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await file.close();
|
|
335
|
+
body.fill(0);
|
|
336
|
+
}
|
|
337
|
+
this.ownership.assertHeld();
|
|
338
|
+
await rename(pending, path);
|
|
339
|
+
const parent = await open(this.directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
340
|
+
try {
|
|
341
|
+
await parent.sync();
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
await parent.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async fileIdentity(name, maximum) {
|
|
348
|
+
this.ownership.assertHeld();
|
|
349
|
+
const path = join(this.directory, name), before = await lstat(path).catch(error => { if (error.code === 'ENOENT')
|
|
350
|
+
return null; throw error; });
|
|
351
|
+
if (!before)
|
|
352
|
+
return null;
|
|
353
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.mode & 0o077 || before.size > maximum
|
|
354
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
355
|
+
throw invalid();
|
|
356
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
357
|
+
try {
|
|
358
|
+
const actual = await file.stat();
|
|
359
|
+
if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size !== before.size)
|
|
360
|
+
throw invalid();
|
|
361
|
+
return { name, size: actual.size, device: actual.dev.toString(), inode: actual.ino.toString() };
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
await file.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** Caller first confirms cleanup of the ORIGINAL container. Revoking this
|
|
368
|
+
* random evidence key is separate from provider/target credentials. Only
|
|
369
|
+
* validated original ciphertext files are unlinked; no recursive removal.
|
|
370
|
+
* A private metadata-only tombstone prevents replacement or resurrection.
|
|
371
|
+
*/
|
|
372
|
+
async erase(signal) {
|
|
373
|
+
this.ownership.assertHeld();
|
|
374
|
+
if (!this.erasureOnly || this.closed)
|
|
375
|
+
throw invalid();
|
|
376
|
+
const active = AbortSignal.any([this.signal, ...(signal ? [signal] : [])]);
|
|
377
|
+
const check = () => { active.throwIfAborted(); this.ownership.assertHeld(); };
|
|
378
|
+
check();
|
|
379
|
+
let receipt = await this.erasure();
|
|
380
|
+
const alreadyRemoved = receipt?.state === 'removed';
|
|
381
|
+
if (!receipt) {
|
|
382
|
+
const identity = await this.get('identity');
|
|
383
|
+
try {
|
|
384
|
+
if (!identity || sha(identity) !== sha(this.aad))
|
|
385
|
+
throw invalid();
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
identity?.fill(0);
|
|
389
|
+
}
|
|
390
|
+
// Preserve the original unknown-file refusal before key destruction, but
|
|
391
|
+
// stream names in fixed directory buffers instead of retaining a manifest.
|
|
392
|
+
// This first read-only discovery can traverse the journal; later passes
|
|
393
|
+
// resume the tombstone directly without repeating the whole preflight.
|
|
394
|
+
for await (const item of await opendir(this.directory, { bufferSize: 32 })) {
|
|
395
|
+
check();
|
|
396
|
+
if (!['owner.lock', 'erasure.json'].includes(item.name)
|
|
397
|
+
&& !encryptedName.test(item.name) && !markerTemporary.test(item.name))
|
|
398
|
+
throw invalid();
|
|
399
|
+
}
|
|
400
|
+
// This constant-size original tombstone precedes key destruction and
|
|
401
|
+
// prevents collectors from recreating any file or decryption authority.
|
|
402
|
+
// Individual inode intents are recorded immediately before each unlink.
|
|
403
|
+
receipt = { schema: 'firedrill.local-world-erasure.v2', identity_sha256: sha(this.aad), state: 'erasing',
|
|
404
|
+
requested_at: new Date().toISOString(), finished_at: null, file_count: 0, pending_file: null };
|
|
405
|
+
await this.writeErasure(receipt); // Durable intent BEFORE irreversible key deletion.
|
|
406
|
+
}
|
|
407
|
+
let encoded;
|
|
408
|
+
try {
|
|
409
|
+
encoded = this.keyEntry.getPassword();
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
throw new CliError('local_world_erasure_pending', 'Unlock the OS credential store and repeat the same forget command. Key removal is not confirmed.');
|
|
413
|
+
}
|
|
414
|
+
if (encoded !== null) {
|
|
415
|
+
if (receipt.state === 'removed' || this.key.length !== 32 || encoded !== this.key.toString('base64url'))
|
|
416
|
+
throw invalid();
|
|
417
|
+
try {
|
|
418
|
+
this.keyEntry.deleteCredential();
|
|
419
|
+
}
|
|
420
|
+
catch { /* Read the original key state, never replace it. */ }
|
|
421
|
+
let absent = false;
|
|
422
|
+
try {
|
|
423
|
+
absent = this.keyEntry.getPassword() === null;
|
|
424
|
+
}
|
|
425
|
+
catch { /* Unknown is not removal proof. */ }
|
|
426
|
+
if (!absent)
|
|
427
|
+
throw new CliError('local_world_erasure_pending', 'Private evidence key removal is not confirmed. Unlock the OS credential store and repeat the same forget command.');
|
|
428
|
+
}
|
|
429
|
+
this.key.fill(0);
|
|
430
|
+
let removed = 0;
|
|
431
|
+
const complete = async (one) => {
|
|
432
|
+
check();
|
|
433
|
+
const limit = markerTemporary.test(one.name) ? 8192 : MAX + 64;
|
|
434
|
+
const actual = await this.fileIdentity(one.name, limit);
|
|
435
|
+
if (actual && (actual.size !== one.size || actual.device !== one.device || actual.inode !== one.inode))
|
|
436
|
+
throw invalid();
|
|
437
|
+
if (actual) {
|
|
438
|
+
check();
|
|
439
|
+
const current = await lstat(join(this.directory, one.name));
|
|
440
|
+
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || current.size !== one.size
|
|
441
|
+
|| current.dev.toString() !== one.device || current.ino.toString() !== one.inode)
|
|
442
|
+
throw invalid();
|
|
443
|
+
await unlink(join(this.directory, one.name));
|
|
444
|
+
}
|
|
445
|
+
const parent = await open(this.directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0));
|
|
446
|
+
try {
|
|
447
|
+
await parent.sync();
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
await parent.close();
|
|
451
|
+
}
|
|
452
|
+
if (!Number.isSafeInteger(receipt.file_count + 1))
|
|
453
|
+
throw invalid();
|
|
454
|
+
receipt = { ...receipt, file_count: receipt.file_count + 1, pending_file: null };
|
|
455
|
+
await this.writeErasure(receipt);
|
|
456
|
+
removed++;
|
|
457
|
+
};
|
|
458
|
+
// If unlink succeeded before a crash, reconcile this same inode's absence
|
|
459
|
+
// once. Never rediscover a substitute name or increment the count twice.
|
|
460
|
+
if (receipt.pending_file)
|
|
461
|
+
await complete(receipt.pending_file);
|
|
462
|
+
for await (const item of await opendir(this.directory, { bufferSize: 32 })) {
|
|
463
|
+
check();
|
|
464
|
+
if (['owner.lock', 'erasure.json'].includes(item.name))
|
|
465
|
+
continue;
|
|
466
|
+
if (alreadyRemoved || !encryptedName.test(item.name) && !markerTemporary.test(item.name))
|
|
467
|
+
throw invalid();
|
|
468
|
+
const one = await this.fileIdentity(item.name, markerTemporary.test(item.name) ? 8192 : MAX + 64);
|
|
469
|
+
if (!one)
|
|
470
|
+
continue; // An already-renamed private marker temporary.
|
|
471
|
+
receipt = { ...receipt, pending_file: one };
|
|
472
|
+
await this.writeErasure(receipt);
|
|
473
|
+
await complete(one);
|
|
474
|
+
if (removed >= ERASURE_PASS)
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
let pending = false;
|
|
478
|
+
for await (const item of await opendir(this.directory, { bufferSize: 32 })) {
|
|
479
|
+
check();
|
|
480
|
+
if (['owner.lock', 'erasure.json'].includes(item.name))
|
|
481
|
+
continue;
|
|
482
|
+
if (!encryptedName.test(item.name) && !markerTemporary.test(item.name))
|
|
483
|
+
throw invalid();
|
|
484
|
+
pending = true;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (!pending) {
|
|
488
|
+
receipt = { ...receipt, state: 'removed', finished_at: receipt.finished_at ?? new Date().toISOString() };
|
|
489
|
+
await this.writeErasure(receipt);
|
|
490
|
+
}
|
|
491
|
+
this.erasureState = receipt.state;
|
|
492
|
+
return { schema: 'firedrill.local-world-erasure-receipt.v2', state: receipt.state, key_removed: true,
|
|
493
|
+
already_removed: alreadyRemoved, file_count: receipt.file_count, finished_at: receipt.finished_at, cloud_data_changed: false,
|
|
494
|
+
files_removed_this_pass: removed, continuation_required: pending, filesystem_backup_erasure_attested: false };
|
|
495
|
+
}
|
|
496
|
+
async close() {
|
|
497
|
+
if (this.closed)
|
|
498
|
+
return;
|
|
499
|
+
try {
|
|
500
|
+
await this.ownership.close();
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
this.closed = true;
|
|
504
|
+
this.key.fill(0);
|
|
505
|
+
this.aad.fill(0);
|
|
506
|
+
}
|
|
507
|
+
// Unless erase() already removed them, the OS key/ciphertexts remain for
|
|
508
|
+
// explicit recovery within policy. Closing alone proves no erasure.
|
|
509
|
+
}
|
|
510
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { api } from './auth.js';
|
|
4
|
+
import { CliError } from './http.js';
|
|
5
|
+
import { signRunnerRequest } from './runner-identity.js';
|
|
6
|
+
import { nativeFilterReference } from './runner-world-session-protocol.js';
|
|
7
|
+
import { localWorldClaimPath, localWorldClaimTime } from './runner-world-claim.js';
|
|
8
|
+
import { localWorldIdentityMatches, LOCAL_WORLD_IDENTITY_KEYS } from '../../contracts/local-world-native-contracts.js';
|
|
9
|
+
const PART = 32768, CHUNK = 16384;
|
|
10
|
+
const sha = (body) => createHash('sha256').update(body).digest('hex');
|
|
11
|
+
const invalid = () => new CliError('local_world_filter_transfer_changed', 'The private evidence protection did not match this exact world. No command was sent.');
|
|
12
|
+
const pending = () => new CliError('local_world_filter_transfer_unconfirmed', 'Private evidence protection is not confirmed. The original local world was not replaced.');
|
|
13
|
+
function inspect(value, owner, manifest, context, reference) {
|
|
14
|
+
const count = Math.ceil(reference.size / PART);
|
|
15
|
+
const keys = [...LOCAL_WORLD_IDENTITY_KEYS, 'context_sha256', 'lease_id', 'part_bytes', 'part_count', 'provenance', 'received_parts',
|
|
16
|
+
'reference', 'schema', 'server_time', 'session_id', 'state'];
|
|
17
|
+
if (!value || !localWorldIdentityMatches(value, owner, manifest) || Object.keys(value).sort().join(',') !== keys.sort().join(',')
|
|
18
|
+
|| value.schema !== 'firedrill.local-world-native-filter-receipt.v1' || value.session_id !== owner.session.session_id
|
|
19
|
+
|| value.lease_id !== owner.lease || value.context_sha256 !== context || value.provenance !== 'customer_device'
|
|
20
|
+
|| !['not_retained', 'uploading', 'validating', 'ready', 'unavailable'].includes(value.state)
|
|
21
|
+
|| value.part_bytes !== PART || !Array.isArray(value.received_parts) || !Number.isFinite(Date.parse(value.server_time)))
|
|
22
|
+
throw invalid();
|
|
23
|
+
if (value.reference === null) {
|
|
24
|
+
if (value.state !== 'not_retained' || value.part_count !== 0 || value.received_parts.length !== 0)
|
|
25
|
+
throw invalid();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
nativeFilterReference(value.reference);
|
|
29
|
+
if (value.reference.sha256 !== reference.sha256 || value.reference.size !== reference.size || value.reference.pattern_count !== reference.pattern_count
|
|
30
|
+
|| value.part_count !== count || new Set(value.received_parts).size !== value.received_parts.length
|
|
31
|
+
|| value.received_parts.some(index => !Number.isSafeInteger(index) || index < 0 || index >= count)
|
|
32
|
+
|| ['validating', 'ready'].includes(value.state) && value.received_parts.length !== count)
|
|
33
|
+
throw invalid();
|
|
34
|
+
}
|
|
35
|
+
if (value.state === 'unavailable')
|
|
36
|
+
throw pending();
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
async function transfer(owner, configuration, channel, reference, signal) {
|
|
40
|
+
const control = configuration.control;
|
|
41
|
+
if (control.schema !== 'firedrill.world-session.v1' || !owner.evidenceUpload
|
|
42
|
+
|| !/^[a-f0-9]{64}$/.test(control.evidence_context_sha256))
|
|
43
|
+
throw invalid();
|
|
44
|
+
const prefix = localWorldClaimPath(owner);
|
|
45
|
+
const auth = { actor: owner.owner, workspace: owner.workspace, signal };
|
|
46
|
+
const request = async (action) => {
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
const sent = await localWorldClaimTime(owner, signal);
|
|
49
|
+
const payload = { schema: 'firedrill.local-world-native-filters.v1', job_id: owner.job,
|
|
50
|
+
attempt_id: owner.attempt, lease_id: owner.lease, request_key: owner.request, device_key: owner.device,
|
|
51
|
+
native_session_id: owner.nativeSession, manifest_sha256: configuration.manifest_sha256, session_id: owner.session.session_id,
|
|
52
|
+
context_sha256: control.evidence_context_sha256, filter_sha256: reference.sha256, write_key: randomUUID(), sent_at_ms: sent,
|
|
53
|
+
...action };
|
|
54
|
+
const proof = await signRunnerRequest(owner, owner.device, payload, signal);
|
|
55
|
+
// No blind transport retry. An explicit later synchronization uses the
|
|
56
|
+
// same original immutable parts/version intents, never another world/key.
|
|
57
|
+
return inspect(await api(prefix + '/world-native-filters', { ...auth, method: 'POST', body: proof }), owner, configuration.manifest_sha256, control.evidence_context_sha256, reference);
|
|
58
|
+
};
|
|
59
|
+
const file = await channel.openNativeFilters(control.evidence_context_sha256);
|
|
60
|
+
let bytes = null;
|
|
61
|
+
try {
|
|
62
|
+
if (file.size !== reference.size || file.sha256 !== reference.sha256)
|
|
63
|
+
throw invalid();
|
|
64
|
+
bytes = Buffer.alloc(reference.size);
|
|
65
|
+
try {
|
|
66
|
+
for (let offset = 0; offset < bytes.length; offset += CHUNK) {
|
|
67
|
+
const chunk = await file.read(offset, Math.min(CHUNK, bytes.length - offset));
|
|
68
|
+
try {
|
|
69
|
+
chunk.copy(bytes, offset);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
chunk.fill(0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await file.close();
|
|
78
|
+
}
|
|
79
|
+
if (sha(bytes) !== reference.sha256)
|
|
80
|
+
throw invalid();
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (let offset = 0; offset < bytes.length; offset += PART) {
|
|
83
|
+
const body = bytes.subarray(offset, offset + PART);
|
|
84
|
+
parts.push({ size: body.length, sha256: sha(body) });
|
|
85
|
+
}
|
|
86
|
+
let receipt = await request({ action: 'begin', reference, parts });
|
|
87
|
+
for (let index = 0; index < parts.length; index++) {
|
|
88
|
+
if (receipt.received_parts.includes(index))
|
|
89
|
+
continue;
|
|
90
|
+
receipt = await request({ action: 'part', index, content_base64: bytes.subarray(index * PART, (index + 1) * PART).toString('base64') });
|
|
91
|
+
if (!receipt.received_parts.includes(index))
|
|
92
|
+
throw pending();
|
|
93
|
+
}
|
|
94
|
+
if (receipt.state !== 'ready')
|
|
95
|
+
receipt = await request({ action: 'commit' });
|
|
96
|
+
bytes.fill(0);
|
|
97
|
+
bytes = null;
|
|
98
|
+
while (receipt.state !== 'ready') {
|
|
99
|
+
if (receipt.state !== 'validating')
|
|
100
|
+
throw pending();
|
|
101
|
+
// Actual durable validation is pending. The separate keeper, not this
|
|
102
|
+
// loop, owns presence; a read does not extend any execution authority.
|
|
103
|
+
await sleep(1000, undefined, { signal });
|
|
104
|
+
receipt = await request({ action: 'status' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
bytes?.fill(0); // Best effort only. JS/HTTP strings are not zeroized memory.
|
|
109
|
+
await file.close().catch(() => undefined);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** No manual command callback runs until original native matchers are retained.
|
|
113
|
+
* A world that closes before producing them may only reach closing collection.
|
|
114
|
+
*/
|
|
115
|
+
export async function protectLocalWorldSession(owner, configuration, channel, signal) {
|
|
116
|
+
while (!channel.isClosing) {
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
let reference;
|
|
119
|
+
try {
|
|
120
|
+
reference = (await channel.observe()).status?.native_filters;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (!(error instanceof CliError) || error.code !== 'local_session_observation_unavailable')
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
if (reference !== undefined && reference !== null) {
|
|
127
|
+
nativeFilterReference(reference);
|
|
128
|
+
await transfer(owner, configuration, channel, reference, signal);
|
|
129
|
+
return !channel.isClosing;
|
|
130
|
+
}
|
|
131
|
+
await Promise.race([sleep(500, undefined, { signal }), channel.waitClosing()]);
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|