firedrill-run 2.2.5 → 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
package/README.md
CHANGED
|
@@ -1,35 +1,191 @@
|
|
|
1
|
-
# Firedrill
|
|
1
|
+
# Firedrill CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
your
|
|
3
|
+
Firedrill simulates your AI agent before it meets real users. Your agent runs on
|
|
4
|
+
your machine as an ordinary local process. Firedrill keeps the reusable cloud
|
|
5
|
+
resources: worlds, synthetic users, scenarios, hosted tools, evaluations and
|
|
6
|
+
reports. The web app shows the same resources and progress as this CLI.
|
|
7
|
+
|
|
8
|
+
Use the CLI yourself, or let a coding agent operate it for you. Local execution
|
|
9
|
+
is not an operating-system sandbox. Approved commands run with your own user
|
|
10
|
+
permissions.
|
|
11
|
+
|
|
12
|
+
**Release candidate:** the proposed public package is `firedrill-run@3.0.0`.
|
|
13
|
+
It is not published or approved for public activation yet. The existing
|
|
14
|
+
unversioned package resolves to the older product. Do not use `@latest` to
|
|
15
|
+
install this candidate. Once the owner approves and publishes this release,
|
|
16
|
+
the intended commands are:
|
|
5
17
|
|
|
6
18
|
```sh
|
|
7
|
-
npx firedrill-run
|
|
19
|
+
npx --yes firedrill-run@3.0.0 init
|
|
20
|
+
npx --yes firedrill-run@3.0.0 --help
|
|
8
21
|
```
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
Until then, use only an explicitly supplied private candidate tarball.
|
|
24
|
+
`firedrill --help` of that exact installed candidate is authoritative.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js `>=22.23.4 <23`.
|
|
29
|
+
- macOS, Windows or Linux are source-supported targets. A release qualification
|
|
30
|
+
must name the actual OS/architecture used; support is not proof each platform
|
|
31
|
+
was exercised. Native keyring availability also depends on its platform build.
|
|
32
|
+
- An OS credential store for persistent sign-in:
|
|
33
|
+
- macOS: Keychain;
|
|
34
|
+
- Windows: Credential Manager;
|
|
35
|
+
- desktop Linux: Secret Service (GNOME Keyring, KWallet or KeePassXC).
|
|
36
|
+
|
|
37
|
+
Without a credential store, for example on a headless or remote Linux machine,
|
|
38
|
+
use session-only sign-in. Credentials are never written to a file.
|
|
39
|
+
Source snapshot and candidate-copy commands also need an external `python3`
|
|
40
|
+
on PATH. Browser drivers need their own supported browser/driver installation.
|
|
41
|
+
These are not included in the npm package. Ordinary cloud metadata commands
|
|
42
|
+
do not require Docker or a bundled Python environment.
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
## Sign in
|
|
18
45
|
|
|
19
46
|
```sh
|
|
20
|
-
npm install --global firedrill-run
|
|
21
47
|
firedrill init
|
|
22
48
|
```
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
`init` signs you in with WorkOS device authorization, or starts account creation.
|
|
51
|
+
It then selects a workspace and checks this machine.
|
|
52
|
+
|
|
53
|
+
- `--json --no-browser` prints the verification link and code instead of
|
|
54
|
+
opening a browser. A person approves in the browser.
|
|
55
|
+
- `--session-only` keeps credentials in memory in a companion process. Keep that
|
|
56
|
+
terminal open while other `firedrill` commands use it. Stop it with Ctrl-C or
|
|
57
|
+
`firedrill logout`.
|
|
58
|
+
- `--persistent` uses the OS credential store. This is the default when one is
|
|
59
|
+
available.
|
|
60
|
+
- `--workspace <id>` selects a workspace without a prompt.
|
|
61
|
+
|
|
62
|
+
`firedrill status` shows the account, workspace, credential mode, companion and
|
|
63
|
+
local drivers. `firedrill logout` revokes terminal access and removes saved
|
|
64
|
+
credentials.
|
|
65
|
+
|
|
66
|
+
Terminal sign-in and remote MCP sign-in are separate authorizations. Account
|
|
67
|
+
creation does not authorize execution or payment.
|
|
68
|
+
|
|
69
|
+
## Simulate your agent
|
|
70
|
+
|
|
71
|
+
| Step | Command |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| Register the agent: metadata only, no source, paths, commands or secrets | `firedrill register --descriptor descriptor.json --name "<name>"` |
|
|
74
|
+
| Find reusable resources | `firedrill library --kind persona\|scenario\|evaluation [--search <prefix>]`, `firedrill tools catalog` |
|
|
75
|
+
| Author a private draft | `firedrill library save --kind <kind> --file <path>` |
|
|
76
|
+
| Declare a world (nothing starts) | `firedrill world save --agent <id> --file world.json` |
|
|
77
|
+
| Review the plan and its cloud-work estimate | `firedrill plan --world <world-id>` |
|
|
78
|
+
| Apply after approval | `firedrill apply <plan-id> --approve --follow` |
|
|
79
|
+
| Enroll this machine once | `firedrill runner enroll` |
|
|
80
|
+
| Run the drills on this machine | `firedrill run --plan local-plan.json --descriptor descriptor.json --campaign <campaign-id>` |
|
|
81
|
+
| Watch progress | `firedrill watch <campaign> --follow` |
|
|
82
|
+
| Read results and evidence | `firedrill report <campaign>`, `… quality`, `… drill <drill> [results \| effects \| conversation \| evidence]` |
|
|
83
|
+
| Cancel | `firedrill simulation cancel <campaign> --approve-change` |
|
|
84
|
+
|
|
85
|
+
What to expect at each step:
|
|
86
|
+
|
|
87
|
+
- **Plan.** `plan` saves a review but starts no tools or drills and makes no
|
|
88
|
+
charge. It shows:
|
|
89
|
+
- pinned versions;
|
|
90
|
+
- how each dependency is reached;
|
|
91
|
+
- unmet requirements;
|
|
92
|
+
- the maximum cloud work.
|
|
93
|
+
|
|
94
|
+
Local target compute is never charged.
|
|
95
|
+
- **Apply.** `apply` reserves the approved estimate and prepares the plan. When
|
|
96
|
+
preparation is ready, it creates the campaign. No target starts at this step.
|
|
97
|
+
- **Run.** `run` shows the exact local plan: roots, argv, working directories,
|
|
98
|
+
environment variable names, readiness checks, driver and reset hooks. Nothing
|
|
99
|
+
runs until you approve that exact plan. In JSON mode, pass `--approve` only
|
|
100
|
+
after a person has approved it.
|
|
101
|
+
- **Report.** The report keeps four things separate:
|
|
102
|
+
- execution state;
|
|
103
|
+
- the observed outcome;
|
|
104
|
+
- per-criterion quality;
|
|
105
|
+
- required checks.
|
|
106
|
+
|
|
107
|
+
Missing evidence is never a zero or a pass.
|
|
108
|
+
|
|
109
|
+
## JSON mode
|
|
110
|
+
|
|
111
|
+
Every command takes `--json`. In JSON mode the CLI:
|
|
112
|
+
|
|
113
|
+
- never prompts;
|
|
114
|
+
- opens no browser unless `--open` is given;
|
|
115
|
+
- prints one `{"type", "data"}` object per line.
|
|
116
|
+
|
|
117
|
+
Errors go to stderr with a nonzero exit code and a stable `code`.
|
|
118
|
+
|
|
119
|
+
## Hosted tools outside a drill
|
|
120
|
+
|
|
121
|
+
Start reviewed catalog tools in the cloud for a set time and use them from a
|
|
122
|
+
local agent:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
firedrill tools runnable
|
|
126
|
+
firedrill tools start <version-id>... --seconds <n>
|
|
127
|
+
firedrill tools sessions
|
|
128
|
+
firedrill tools access <session-id> [--allow-writes]
|
|
129
|
+
firedrill tools session <session-id> stop
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `start` shows a cost estimate first. After approval, run the printed
|
|
133
|
+
`--usage-quote <key> --approve-tool-change` command. It admits exactly the
|
|
134
|
+
quotes you approved.
|
|
135
|
+
- `access` serves loopback MCP and JSON URLs. They are read-only unless
|
|
136
|
+
`--allow-writes` is given.
|
|
137
|
+
|
|
138
|
+
Custom tool packages use `firedrill tools init`, `validate` and `push`. Pushing a
|
|
139
|
+
private version does not publish it or prove it runs.
|
|
140
|
+
|
|
141
|
+
## Remote MCP
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
firedrill mcp --agent codex|claude|other
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This prints connection guidance for the management MCP server. The MCP client
|
|
148
|
+
signs in through its own browser OAuth. Remote MCP manages cloud resources but
|
|
149
|
+
cannot run anything on this machine.
|
|
150
|
+
|
|
151
|
+
## Safety
|
|
152
|
+
|
|
153
|
+
- Commands, argv, roots and hooks come only from the plan you approved. A later
|
|
154
|
+
change to argv or a hashed file voids that approval.
|
|
155
|
+
- A plan never:
|
|
156
|
+
- disables TLS verification;
|
|
157
|
+
- adds a certificate authority;
|
|
158
|
+
- sets a proxy;
|
|
159
|
+
- injects a loader (`LD_*`, `DYLD_*`).
|
|
160
|
+
|
|
161
|
+
Secret configuration is never a fixed plan value.
|
|
162
|
+
- Simulated dependencies are reached through per-drill loopback URLs. Short-lived
|
|
163
|
+
access stays in the CLI's memory and never reaches the target. Only `observed`
|
|
164
|
+
tool results are passed to the target. A pending or unknown outcome arrives as
|
|
165
|
+
an error with a recovery path.
|
|
166
|
+
- Attached services are only checked for readiness and are never signalled.
|
|
167
|
+
Launched services run in their own process group and are stopped with it.
|
|
168
|
+
- On Windows there are no process groups. Only each started process is stopped,
|
|
169
|
+
and processes it started may keep running.
|
|
170
|
+
|
|
171
|
+
## Recovery
|
|
172
|
+
|
|
173
|
+
- Commands that change cloud state print a request key. Rerun with
|
|
174
|
+
`--request <key>` to read the original result. That never applies or charges
|
|
175
|
+
twice.
|
|
176
|
+
- A step whose outcome is unknown stays unknown and is never replayed under a new
|
|
177
|
+
key.
|
|
178
|
+
- If the CLI stops during a run, a lost local process is not resumed. Inspect the
|
|
179
|
+
campaign with `watch` and `report`, and follow the recovery it returns.
|
|
180
|
+
|
|
181
|
+
## Advanced commands
|
|
29
182
|
|
|
30
|
-
|
|
31
|
-
agents and CI should also use the CLI's documented `--json --non-interactive`
|
|
32
|
-
inputs for the action they intend to run. First-use browser consent stays human.
|
|
183
|
+
`firedrill help advanced` lists the remaining commands, including:
|
|
33
184
|
|
|
34
|
-
|
|
35
|
-
|
|
185
|
+
- catalog tool management (`tools`);
|
|
186
|
+
- tool sessions (`sessions`);
|
|
187
|
+
- runner devices and grants (`runner`);
|
|
188
|
+
- billing and usage (`billing`);
|
|
189
|
+
- CI connections (`ci`);
|
|
190
|
+
- team, workspace and settings;
|
|
191
|
+
- `login`, `signup` and `workspace` as separate steps of `init`.
|
package/bin/firedrill.mjs
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
2
|
+
// Keep this bootstrap plain JavaScript. An unsupported Node runtime must not
|
|
3
|
+
// try to parse TypeScript before it can explain the required toolchain.
|
|
4
|
+
const [major, minor, patch] = process.versions.node.split('.').map(Number);
|
|
5
|
+
const supported = major === 22 && (minor > 23 || minor === 23 && patch >= 4);
|
|
6
|
+
function report(code, message, exitCode = 1) {
|
|
7
|
+
process.stderr.write(process.argv.includes('--json')
|
|
8
|
+
? JSON.stringify({ type: 'error', error: { code, message } }) + '\n'
|
|
9
|
+
: message + '\n');
|
|
10
|
+
process.exitCode = exitCode;
|
|
11
|
+
}
|
|
12
|
+
if (!supported) {
|
|
13
|
+
report('node_runtime_unsupported', 'Firedrill requires Node 22.23.4 or a later 22.x release. Update Node, then run the command again.');
|
|
14
|
+
} else {
|
|
15
|
+
let entry;
|
|
16
|
+
try { entry = await import('../dist/cli/src/main.js'); }
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error?.code === 'platform_configuration_invalid') {
|
|
19
|
+
report('platform_configuration_invalid', 'Use FIREDRILL_PLATFORM=sim-dev for isolated hosted development with all FIREDRILL_LOCAL_* variables unset. Unset all platform variables to select the public platform. Unknown or mixed profiles are refused.', 2);
|
|
20
|
+
} else if (error?.code === 'local_platform_configuration_invalid') {
|
|
21
|
+
report('local_platform_configuration_invalid', 'Local development requires FIREDRILL_LOCAL_PLATFORM=1 and paired HTTP loopback API and app origins with explicit ports. Arbitrary service URLs are refused.', 2);
|
|
22
|
+
} else {
|
|
23
|
+
report('cli_installation_unavailable', 'The CLI could not load. Restore the Firedrill installation, then try again.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (entry) {
|
|
27
|
+
try { await entry.main(); }
|
|
28
|
+
catch { report('command_unconfirmed', 'The command could not finish. No success is implied. Inspect its original request before trying again.'); }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "firedrill.cli-assembly.v1",
|
|
3
|
+
"package": "firedrill-run",
|
|
4
|
+
"version": "3.0.0",
|
|
5
|
+
"entry": "cli/src/main.js",
|
|
6
|
+
"helpers": [
|
|
7
|
+
"cli/runtime/source_snapshot.py",
|
|
8
|
+
"cli/runtime/candidate_copy.py"
|
|
9
|
+
],
|
|
10
|
+
"shared": [
|
|
11
|
+
"contracts/world-command.js"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Original, data-only candidate export. Never imports or runs customer source.
|
|
2
|
+
|
|
3
|
+
Only creates a new owned destination, or resumes that exact destination after
|
|
4
|
+
checking every existing file. It never overwrites or deletes destination files.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
import struct
|
|
14
|
+
import tempfile
|
|
15
|
+
import zipfile
|
|
16
|
+
import sqlite3
|
|
17
|
+
from contextlib import closing
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Refused(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def canonical(value):
|
|
25
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def private_json(path, maximum):
|
|
29
|
+
before = os.lstat(path)
|
|
30
|
+
if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() or before.st_mode & 0o077
|
|
31
|
+
or not 0 < before.st_size <= maximum):
|
|
32
|
+
raise Refused()
|
|
33
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
34
|
+
try:
|
|
35
|
+
actual = os.fstat(fd)
|
|
36
|
+
if (actual.st_dev, actual.st_ino, actual.st_size, actual.st_mtime_ns) != (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns):
|
|
37
|
+
raise Refused()
|
|
38
|
+
with os.fdopen(os.dup(fd), "rb") as stream:
|
|
39
|
+
body = stream.read(maximum + 1)
|
|
40
|
+
after = os.fstat(fd)
|
|
41
|
+
if (after.st_size, after.st_mtime_ns, after.st_ctime_ns) != (actual.st_size, actual.st_mtime_ns, actual.st_ctime_ns):
|
|
42
|
+
raise Refused()
|
|
43
|
+
return json.loads(body)
|
|
44
|
+
finally:
|
|
45
|
+
os.close(fd)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def receipt(path, value):
|
|
49
|
+
# A private cache file, never a customer destination. Atomic replacement
|
|
50
|
+
# retains recovery through a crash; source files use exclusive links below.
|
|
51
|
+
fd, temporary = tempfile.mkstemp(prefix="receipt-", dir=path.parent)
|
|
52
|
+
try:
|
|
53
|
+
with os.fdopen(fd, "wb") as stream:
|
|
54
|
+
stream.write(canonical(value))
|
|
55
|
+
stream.flush()
|
|
56
|
+
os.fsync(stream.fileno())
|
|
57
|
+
os.replace(temporary, path)
|
|
58
|
+
parent = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
59
|
+
try:
|
|
60
|
+
os.fsync(parent)
|
|
61
|
+
finally:
|
|
62
|
+
os.close(parent)
|
|
63
|
+
finally:
|
|
64
|
+
if os.path.exists(temporary):
|
|
65
|
+
os.unlink(temporary)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def safe_path(raw):
|
|
69
|
+
path = PurePosixPath(raw)
|
|
70
|
+
denied = {".git", ".hg", ".svn", ".aws", ".ssh", ".gcloud", "node_modules", ".venv", "venv", "__pycache__", ".next", ".turbo", "coverage", ".firedrill-simulation"}
|
|
71
|
+
if (not raw or len(raw) > 1024 or path.is_absolute() or path.as_posix() != raw or ".." in path.parts
|
|
72
|
+
or "\\" in raw or any(ord(one) < 32 or ord(one) == 127 for one in raw)
|
|
73
|
+
or ":" in path.parts[0] or denied.intersection(path.parts)
|
|
74
|
+
or any(one.startswith(".env") for one in path.parts)
|
|
75
|
+
or path.suffix.lower() in {".pem", ".key", ".p12", ".pfx", ".sqlite", ".sqlite3", ".db", ".kdbx"}
|
|
76
|
+
or path.name in {".npmrc", ".pypirc", ".netrc", "credentials.json", "id_rsa", "id_ed25519"}):
|
|
77
|
+
raise Refused()
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def directories(root, parts, create=False):
|
|
82
|
+
current = os.dup(root)
|
|
83
|
+
try:
|
|
84
|
+
for part in parts:
|
|
85
|
+
if create:
|
|
86
|
+
try:
|
|
87
|
+
os.mkdir(part, 0o700, dir_fd=current)
|
|
88
|
+
os.fsync(current)
|
|
89
|
+
except FileExistsError:
|
|
90
|
+
pass
|
|
91
|
+
new = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=current)
|
|
92
|
+
os.close(current)
|
|
93
|
+
current = new
|
|
94
|
+
return current
|
|
95
|
+
except BaseException:
|
|
96
|
+
os.close(current)
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def matching(fd, expected):
|
|
101
|
+
before = os.fstat(fd)
|
|
102
|
+
if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() or before.st_size != expected["size"]
|
|
103
|
+
or stat.S_IMODE(before.st_mode) != expected["mode"]):
|
|
104
|
+
raise Refused()
|
|
105
|
+
with os.fdopen(os.dup(fd), "rb") as stream:
|
|
106
|
+
if hashlib.file_digest(stream, "sha256").hexdigest() != expected["sha256"]:
|
|
107
|
+
raise Refused()
|
|
108
|
+
after = os.fstat(fd)
|
|
109
|
+
if (before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns):
|
|
110
|
+
raise Refused()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class FileIndex:
|
|
114
|
+
"""Disk-backed identity index. Each read retains at most one manifest row."""
|
|
115
|
+
def __init__(self, db):
|
|
116
|
+
self.db = db
|
|
117
|
+
self.count = 0
|
|
118
|
+
db.execute("CREATE TABLE files(path TEXT PRIMARY KEY, document TEXT NOT NULL, archive_seen INTEGER NOT NULL DEFAULT 0)")
|
|
119
|
+
|
|
120
|
+
def __len__(self):
|
|
121
|
+
return self.count
|
|
122
|
+
|
|
123
|
+
def __contains__(self, key):
|
|
124
|
+
return self.db.execute("SELECT 1 FROM files WHERE path=?", (key,)).fetchone() is not None
|
|
125
|
+
|
|
126
|
+
def get(self, key):
|
|
127
|
+
row = self.db.execute("SELECT document FROM files WHERE path=?", (key,)).fetchone()
|
|
128
|
+
return json.loads(row[0]) if row else None
|
|
129
|
+
|
|
130
|
+
def __getitem__(self, key):
|
|
131
|
+
result = self.get(key)
|
|
132
|
+
if result is None:
|
|
133
|
+
raise Refused()
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def add(self, value):
|
|
137
|
+
self.db.execute("INSERT INTO files(path,document) VALUES(?,?)", (value["path"], canonical(value).decode()))
|
|
138
|
+
self.count += 1
|
|
139
|
+
|
|
140
|
+
def items(self):
|
|
141
|
+
for path, document in self.db.execute("SELECT path,document FROM files ORDER BY path"):
|
|
142
|
+
yield path, json.loads(document)
|
|
143
|
+
|
|
144
|
+
def observe_archive(self, key):
|
|
145
|
+
if self.db.execute("UPDATE files SET archive_seen=1 WHERE path=? AND archive_seen=0", (key,)).rowcount != 1:
|
|
146
|
+
raise Refused()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FolderIndex:
|
|
150
|
+
def __init__(self, db):
|
|
151
|
+
self.db = db
|
|
152
|
+
self.count = 0
|
|
153
|
+
db.execute("CREATE TABLE folders(path TEXT PRIMARY KEY)")
|
|
154
|
+
|
|
155
|
+
def __len__(self):
|
|
156
|
+
return self.count
|
|
157
|
+
|
|
158
|
+
def __contains__(self, key):
|
|
159
|
+
return self.db.execute("SELECT 1 FROM folders WHERE path=?", (key,)).fetchone() is not None
|
|
160
|
+
|
|
161
|
+
def add(self, key):
|
|
162
|
+
self.db.execute("INSERT INTO folders(path) VALUES(?)", (key,))
|
|
163
|
+
self.count += 1
|
|
164
|
+
|
|
165
|
+
def overlaps_files(self):
|
|
166
|
+
return self.db.execute("SELECT 1 FROM folders JOIN files USING(path) LIMIT 1").fetchone() is not None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def inspect_existing(root, files, folders, parts=(), seen=None):
|
|
170
|
+
seen = [0] if seen is None else seen
|
|
171
|
+
maximum = len(files) + len(folders)
|
|
172
|
+
with os.scandir(root) as entries:
|
|
173
|
+
for entry in entries:
|
|
174
|
+
name = entry.name
|
|
175
|
+
path = "/".join((*parts, name))
|
|
176
|
+
seen[0] += 1
|
|
177
|
+
if seen[0] > maximum:
|
|
178
|
+
raise Refused()
|
|
179
|
+
before = os.stat(name, dir_fd=root, follow_symlinks=False)
|
|
180
|
+
if stat.S_ISDIR(before.st_mode) and path in folders and before.st_uid == os.getuid():
|
|
181
|
+
child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root)
|
|
182
|
+
try:
|
|
183
|
+
inspect_existing(child, files, folders, (*parts, name), seen)
|
|
184
|
+
finally:
|
|
185
|
+
os.close(child)
|
|
186
|
+
elif path in files and stat.S_ISREG(before.st_mode):
|
|
187
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=root)
|
|
188
|
+
try:
|
|
189
|
+
matching(fd, files[path])
|
|
190
|
+
finally:
|
|
191
|
+
os.close(fd)
|
|
192
|
+
else:
|
|
193
|
+
raise Refused()
|
|
194
|
+
return seen[0]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def copy(args, db):
|
|
198
|
+
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "getuid"):
|
|
199
|
+
raise Refused()
|
|
200
|
+
cache = Path(args.cache)
|
|
201
|
+
if not cache.is_absolute() or cache != cache.resolve(strict=True):
|
|
202
|
+
raise Refused()
|
|
203
|
+
owned = cache.stat()
|
|
204
|
+
if not stat.S_ISDIR(owned.st_mode) or owned.st_uid != os.getuid() or owned.st_mode & 0o077:
|
|
205
|
+
raise Refused()
|
|
206
|
+
intent = private_json(cache / "intent.json", 65536)
|
|
207
|
+
document = private_json(cache / "manifest.json", 65536)
|
|
208
|
+
identity = document["identity"]
|
|
209
|
+
if (intent["schema"] != "firedrill.native-candidate-copy.v1" or identity["schema"] != "firedrill.agent-change-export.v1"
|
|
210
|
+
or document.get("schema") != "firedrill.candidate-manifest-pages.v1"
|
|
211
|
+
or not re.fullmatch(r"[a-f0-9-]{36}", document.get("page_set", ""))
|
|
212
|
+
or identity["change_id"] != intent["change"] or identity["baseline_id"] != intent["campaign"]
|
|
213
|
+
or identity["scope_sha256"] != intent["scope_sha256"] or identity["draft_sha256"] != intent["draft_sha256"]
|
|
214
|
+
or type(identity["file_count"]) is not int or not 1 <= identity["file_count"] <= identity["capacity"]["files"]
|
|
215
|
+
or type(document.get("pages")) is not int or document["pages"] != (identity["file_count"] + 99) // 100):
|
|
216
|
+
raise Refused()
|
|
217
|
+
files, folders, total, folder_bytes, count = FileIndex(db), FolderIndex(db), 0, 0, 0
|
|
218
|
+
manifest_hash = hashlib.sha256(b"[")
|
|
219
|
+
for page in range(1, document["pages"] + 1):
|
|
220
|
+
packet = private_json(cache / f"manifest-{document['page_set']}-{page}.json", 1024 * 1024)
|
|
221
|
+
entries = packet.get("entries")
|
|
222
|
+
if (packet.get("page") != page or not isinstance(entries, list) or not 1 <= len(entries) <= 100
|
|
223
|
+
or page < document["pages"] and len(entries) != 100):
|
|
224
|
+
raise Refused()
|
|
225
|
+
for one in entries:
|
|
226
|
+
if (not isinstance(one, dict) or set(one) != {"path", "size", "sha256", "mode"} or type(one["size"]) is not int
|
|
227
|
+
or not 0 <= one["size"] <= identity["capacity"]["file_bytes"] or one["mode"] not in {0o644, 0o755}
|
|
228
|
+
or not re.fullmatch("[0-9a-f]{64}", one["sha256"])):
|
|
229
|
+
raise Refused()
|
|
230
|
+
path = safe_path(one["path"])
|
|
231
|
+
if count:
|
|
232
|
+
manifest_hash.update(b",")
|
|
233
|
+
manifest_hash.update(canonical(one))
|
|
234
|
+
count += 1
|
|
235
|
+
if count > identity["file_count"]:
|
|
236
|
+
raise Refused()
|
|
237
|
+
files.add(one)
|
|
238
|
+
total += one["size"]
|
|
239
|
+
for parent in path.parents:
|
|
240
|
+
name = parent.as_posix()
|
|
241
|
+
if name != "." and name not in folders:
|
|
242
|
+
folder_bytes += len(name.encode("utf-8")) + 64
|
|
243
|
+
if folder_bytes > identity["capacity"]["directory_bytes"]:
|
|
244
|
+
raise Refused()
|
|
245
|
+
folders.add(name)
|
|
246
|
+
manifest_hash.update(b"]")
|
|
247
|
+
if (count != identity["file_count"] or manifest_hash.hexdigest() != identity["files_sha256"]
|
|
248
|
+
or total != identity["expanded_bytes"] or total > identity["capacity"]["expanded_bytes"] or folders.overlaps_files()):
|
|
249
|
+
raise Refused()
|
|
250
|
+
destination = Path(intent["destination"])
|
|
251
|
+
if not destination.is_absolute() or destination.name in {"", ".", ".."}:
|
|
252
|
+
raise Refused()
|
|
253
|
+
parent = destination.parent.resolve(strict=True)
|
|
254
|
+
if destination != parent / destination.name:
|
|
255
|
+
raise Refused()
|
|
256
|
+
archive = cache / "source.zip"
|
|
257
|
+
fd = os.open(archive, os.O_RDONLY | os.O_NOFOLLOW)
|
|
258
|
+
with os.fdopen(fd, "rb") as raw:
|
|
259
|
+
stored = os.fstat(raw.fileno())
|
|
260
|
+
if (not stat.S_ISREG(stored.st_mode) or stored.st_uid != os.getuid() or stored.st_mode & 0o077
|
|
261
|
+
or stored.st_size != identity["snapshot"]["size"] or stored.st_size > identity["capacity"]["archive_bytes"]
|
|
262
|
+
or hashlib.file_digest(raw, "sha256").hexdigest() != identity["snapshot"]["sha256"]):
|
|
263
|
+
raise Refused()
|
|
264
|
+
# Bound the central directory read before ZipFile allocates entries.
|
|
265
|
+
class Bounded:
|
|
266
|
+
def __init__(self, stream):
|
|
267
|
+
self.stream = stream
|
|
268
|
+
def __getattr__(self, name):
|
|
269
|
+
return getattr(self.stream, name)
|
|
270
|
+
def read(self, size=-1):
|
|
271
|
+
maximum = identity["capacity"]["directory_bytes"]
|
|
272
|
+
if size > maximum:
|
|
273
|
+
raise Refused()
|
|
274
|
+
body = self.stream.read(maximum + 1 if size < 0 else size)
|
|
275
|
+
if len(body) > maximum:
|
|
276
|
+
raise Refused()
|
|
277
|
+
if size >= 46 and body.startswith(b"PK\x01\x02"):
|
|
278
|
+
at, count = 0, 0
|
|
279
|
+
while body[at:at + 4] == b"PK\x01\x02":
|
|
280
|
+
if len(body) - at < 46:
|
|
281
|
+
raise Refused()
|
|
282
|
+
name, extra, comment = struct.unpack_from("<HHH", body, at + 28)
|
|
283
|
+
at += 46 + name + extra + comment
|
|
284
|
+
count += 1
|
|
285
|
+
if at > len(body) or count > len(files):
|
|
286
|
+
raise Refused()
|
|
287
|
+
return body
|
|
288
|
+
raw.seek(0)
|
|
289
|
+
with zipfile.ZipFile(Bounded(raw)) as opened, tempfile.TemporaryDirectory(prefix=".firedrill-candidate-", dir=parent) as temporary:
|
|
290
|
+
members = opened.infolist()
|
|
291
|
+
if len(members) != len(files):
|
|
292
|
+
raise Refused()
|
|
293
|
+
staging = Path(temporary)
|
|
294
|
+
for member in members:
|
|
295
|
+
files.observe_archive(member.filename)
|
|
296
|
+
expected = files.get(member.filename)
|
|
297
|
+
mode = member.external_attr >> 16
|
|
298
|
+
if (expected is None or member.flag_bits & 1 or stat.S_IFMT(mode) not in {0, stat.S_IFREG}
|
|
299
|
+
or (0o755 if mode & 0o111 else 0o644) != expected["mode"] or member.file_size != expected["size"]):
|
|
300
|
+
raise Refused()
|
|
301
|
+
target = staging / safe_path(member.filename)
|
|
302
|
+
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
303
|
+
hashed, size = hashlib.sha256(), 0
|
|
304
|
+
with opened.open(member) as incoming, target.open("xb") as output:
|
|
305
|
+
while chunk := incoming.read(1024 * 1024):
|
|
306
|
+
size += len(chunk)
|
|
307
|
+
if size > expected["size"]:
|
|
308
|
+
raise Refused()
|
|
309
|
+
hashed.update(chunk)
|
|
310
|
+
output.write(chunk)
|
|
311
|
+
output.flush()
|
|
312
|
+
os.fsync(output.fileno())
|
|
313
|
+
if size != expected["size"] or hashed.hexdigest() != expected["sha256"]:
|
|
314
|
+
raise Refused()
|
|
315
|
+
target.chmod(expected["mode"])
|
|
316
|
+
parentfd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
317
|
+
try:
|
|
318
|
+
receipt_path = cache / "copy-receipt.json"
|
|
319
|
+
proof = {"schema": "firedrill.candidate-copy-receipt.v1", "request_key": intent["key"],
|
|
320
|
+
"destination": str(destination), "source_id": identity["source_id"], "snapshot_sha256": identity["snapshot"]["sha256"],
|
|
321
|
+
"files_sha256": identity["files_sha256"]}
|
|
322
|
+
if receipt_path.exists():
|
|
323
|
+
previous = private_json(receipt_path, 65536)
|
|
324
|
+
if any(previous.get(key) != value for key, value in proof.items()):
|
|
325
|
+
raise Refused()
|
|
326
|
+
else:
|
|
327
|
+
# Never use exist_ok, checkout, reset, rename or replacement
|
|
328
|
+
# over a destination. A crash before the ownership receipt
|
|
329
|
+
# leaves an unknown folder that must not be claimed later.
|
|
330
|
+
os.mkdir(destination.name, 0o700, dir_fd=parentfd)
|
|
331
|
+
os.fsync(parentfd)
|
|
332
|
+
owned = os.stat(destination.name, dir_fd=parentfd, follow_symlinks=False)
|
|
333
|
+
previous = {**proof, "device": owned.st_dev, "inode": owned.st_ino, "complete": False}
|
|
334
|
+
receipt(receipt_path, previous)
|
|
335
|
+
root = os.open(destination.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parentfd)
|
|
336
|
+
try:
|
|
337
|
+
actual = os.fstat(root)
|
|
338
|
+
if (actual.st_dev, actual.st_ino, actual.st_uid) != (previous["device"], previous["inode"], os.getuid()):
|
|
339
|
+
raise Refused()
|
|
340
|
+
inspect_existing(root, files, folders)
|
|
341
|
+
for name, expected in files.items():
|
|
342
|
+
path = PurePosixPath(name)
|
|
343
|
+
containing = directories(root, path.parts[:-1], create=True)
|
|
344
|
+
try:
|
|
345
|
+
try:
|
|
346
|
+
# Unlinking our private staged name immediately
|
|
347
|
+
# removes the second link. No target file is
|
|
348
|
+
# truncated even after interrupted publication.
|
|
349
|
+
os.link(staging / name, path.name, dst_dir_fd=containing, follow_symlinks=False)
|
|
350
|
+
os.unlink(staging / name)
|
|
351
|
+
os.fsync(containing)
|
|
352
|
+
except FileExistsError:
|
|
353
|
+
fd = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=containing)
|
|
354
|
+
try:
|
|
355
|
+
matching(fd, expected)
|
|
356
|
+
finally:
|
|
357
|
+
os.close(fd)
|
|
358
|
+
finally:
|
|
359
|
+
os.close(containing)
|
|
360
|
+
if inspect_existing(root, files, folders) != len(files) + len(folders):
|
|
361
|
+
raise Refused()
|
|
362
|
+
if os.stat(destination.name, dir_fd=parentfd, follow_symlinks=False).st_ino != actual.st_ino:
|
|
363
|
+
raise Refused()
|
|
364
|
+
receipt(receipt_path, {**previous, "complete": True})
|
|
365
|
+
finally:
|
|
366
|
+
os.close(root)
|
|
367
|
+
finally:
|
|
368
|
+
os.close(parentfd)
|
|
369
|
+
return {"schema": "firedrill.candidate-copy.v1", "destination": str(destination), "source_id": identity["source_id"],
|
|
370
|
+
"world_id": identity["world_id"], "agent_id": identity["agent_id"], "files_sha256": identity["files_sha256"],
|
|
371
|
+
"file_count": len(files), "target_executed": False, "original_files_changed": False}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def run(args):
|
|
375
|
+
# No retained full source manifest in memory. Private temporary SQLite is
|
|
376
|
+
# an index only, never execution state and never copied into the candidate.
|
|
377
|
+
with tempfile.TemporaryDirectory(prefix="firedrill-copy-index-") as directory:
|
|
378
|
+
with closing(sqlite3.connect(Path(directory) / "index.sqlite")) as db:
|
|
379
|
+
db.execute("PRAGMA cache_size=-2048")
|
|
380
|
+
db.execute("PRAGMA temp_store=FILE")
|
|
381
|
+
db.execute("PRAGMA mmap_size=0")
|
|
382
|
+
return copy(args, db)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
if __name__ == "__main__":
|
|
386
|
+
parser = argparse.ArgumentParser()
|
|
387
|
+
parser.add_argument("--cache", required=True)
|
|
388
|
+
args = parser.parse_args()
|
|
389
|
+
try:
|
|
390
|
+
result = run(args)
|
|
391
|
+
print(json.dumps({"ok": True, "data": result}))
|
|
392
|
+
except Exception:
|
|
393
|
+
print(json.dumps({"ok": False, "error": {"code": "candidate_copy_refused",
|
|
394
|
+
"message": "The exact candidate copy could not be confirmed. Existing destination files were not overwritten or removed. Inspect the retained copy request before resuming."}}))
|
|
395
|
+
raise SystemExit(1)
|