firedrill-run 2.2.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +465 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1622 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +61 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Original local intake. Never imports/builds the selected project or contacts a service.
|
|
2
|
+
|
|
3
|
+
Only the explicit inspect/snapshot command invokes this bundled stdlib helper.
|
|
4
|
+
The manifest is customer-runner evidence, not independent source attestation.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import gzip
|
|
8
|
+
import hashlib
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
import re
|
|
14
|
+
import select
|
|
15
|
+
import shutil
|
|
16
|
+
import signal
|
|
17
|
+
import stat
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tarfile
|
|
21
|
+
import tempfile
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
EXCLUDED = {".git", ".hg", ".svn", ".aws", ".ssh", ".gcloud", "node_modules", ".venv", "venv",
|
|
25
|
+
"__pycache__", ".next", ".turbo", "coverage", ".firedrill-simulation"}
|
|
26
|
+
PRIVATE_SUFFIXES = {".pem", ".key", ".p12", ".pfx", ".sqlite", ".sqlite3", ".db", ".kdbx", ".log",
|
|
27
|
+
".sqlite-wal", ".sqlite-shm", ".sqlite-journal", ".sqlite3-wal", ".sqlite3-shm", ".sqlite3-journal",
|
|
28
|
+
".db-wal", ".db-shm", ".db-journal"}
|
|
29
|
+
PRIVATE_NAMES = {".npmrc", ".pypirc", ".netrc", "credentials.json", "id_rsa", "id_ed25519"}
|
|
30
|
+
SECRET_PATTERNS = [re.compile(value) for value in (
|
|
31
|
+
r"\bfd_tool_[a-f0-9-]{36}\.[A-Za-z0-9_-]{64}",
|
|
32
|
+
r"-----BEGIN (?:RSA |EC |DSA |ENCRYPTED |OPENSSH )?PRIVATE KEY-----",
|
|
33
|
+
r"\b(?:sk-ant-api\d+-|sk-proj-|sk-svcacct-|gh[pousr]_|github_pat_|xox[baprs]-)[A-Za-z0-9_-]{16,}",
|
|
34
|
+
r"\bAKIA[A-Z0-9]{16}\b", r'"type"\s*:\s*"service_account"[\s\S]{0,4096}"private_key"',
|
|
35
|
+
)]
|
|
36
|
+
VARIABLE_PATTERNS = [re.compile(value) for value in (
|
|
37
|
+
r"(?:process\.env\.|import\.meta\.env\.)([A-Za-z_][A-Za-z0-9_]*)",
|
|
38
|
+
r"(?:process\.env|os\.environ)\s*\[\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]\s*\]",
|
|
39
|
+
r"(?:os\.(?:getenv|environ\.get)|env)\s*\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]",
|
|
40
|
+
)]
|
|
41
|
+
MAX_FILES, MAX_FILE_BYTES, MAX_BYTES = 100000, 50 * 1024**2, 2 * 1024**3
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class IntakeError(Exception):
|
|
45
|
+
def __init__(self, code, message):
|
|
46
|
+
self.code, self.message = code, message
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def canonical(value):
|
|
50
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def safe_path(raw):
|
|
54
|
+
try:
|
|
55
|
+
raw.encode("utf-8")
|
|
56
|
+
except UnicodeError:
|
|
57
|
+
raise IntakeError("source_path_invalid", "A source path is not valid Unicode.") from None
|
|
58
|
+
path = PurePosixPath(raw)
|
|
59
|
+
if (not raw or len(raw) > 1024 or "\\" in raw or any(ord(c) < 32 or ord(c) == 127 for c in raw)
|
|
60
|
+
or path.is_absolute() or ".." in path.parts or not path.parts or ":" in path.parts[0]
|
|
61
|
+
or path.as_posix() != raw):
|
|
62
|
+
raise IntakeError("source_path_invalid", "A source path is not supported.")
|
|
63
|
+
return path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def excluded(path):
|
|
67
|
+
return (bool(EXCLUDED.intersection(path.parts)) or path.suffix.lower() in PRIVATE_SUFFIXES
|
|
68
|
+
or path.name.startswith(".env") or path.name in PRIVATE_NAMES)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def git(root, *args, optional=False):
|
|
72
|
+
executable = shutil.which("git")
|
|
73
|
+
if not executable:
|
|
74
|
+
if optional:
|
|
75
|
+
return None
|
|
76
|
+
raise IntakeError("git_required", "Install Git to retain repository identity for this source.")
|
|
77
|
+
executable = str(Path(executable).resolve(strict=True))
|
|
78
|
+
if Path(executable) == root or root in Path(executable).parents:
|
|
79
|
+
raise IntakeError("git_runtime_refused", "Use Git installed outside the project. A project-supplied executable was not run.")
|
|
80
|
+
environment = {key: os.environ[key] for key in ("PATH", "SYSTEMROOT") if key in os.environ}
|
|
81
|
+
environment.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, GIT_TERMINAL_PROMPT="0", GIT_OPTIONAL_LOCKS="0")
|
|
82
|
+
try:
|
|
83
|
+
# Never run a repository hook/fsmonitor, diff driver, filter, checkout,
|
|
84
|
+
# submodule command or network action. stderr may contain local values.
|
|
85
|
+
with subprocess.Popen([executable, "--no-optional-locks", "-c", "core.fsmonitor=false", "-c",
|
|
86
|
+
"core.hooksPath=" + os.devnull, "-c", "core.untrackedCache=false", "-C", str(root), *args],
|
|
87
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=environment) as process:
|
|
88
|
+
output, deadline = bytearray(), time.monotonic() + 30
|
|
89
|
+
try:
|
|
90
|
+
while True:
|
|
91
|
+
remaining = deadline - time.monotonic()
|
|
92
|
+
if remaining <= 0 or not select.select([process.stdout], [], [], remaining)[0]:
|
|
93
|
+
raise IntakeError("git_metadata_unavailable", "Repository metadata did not finish within the read deadline.")
|
|
94
|
+
chunk = os.read(process.stdout.fileno(), 65536)
|
|
95
|
+
if not chunk:
|
|
96
|
+
break
|
|
97
|
+
output.extend(chunk)
|
|
98
|
+
if len(output) > 32 * 1024**2:
|
|
99
|
+
raise IntakeError("source_capacity_required", "Repository metadata exceeds local intake capacity. Select a smaller project root.")
|
|
100
|
+
returncode = process.wait(timeout=max(0.01, deadline - time.monotonic()))
|
|
101
|
+
finally:
|
|
102
|
+
if process.poll() is None:
|
|
103
|
+
process.kill()
|
|
104
|
+
process.wait()
|
|
105
|
+
if returncode:
|
|
106
|
+
if optional:
|
|
107
|
+
return None
|
|
108
|
+
raise IntakeError("git_metadata_unavailable", "Repository metadata could not be read safely.")
|
|
109
|
+
return bytes(output)
|
|
110
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
111
|
+
raise IntakeError("git_metadata_unavailable", "Repository metadata could not be read safely.") from None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def repository(root):
|
|
115
|
+
top = git(root, "rev-parse", "--show-toplevel", optional=True)
|
|
116
|
+
if top is None:
|
|
117
|
+
# A .git marker with a failed Git read must not silently become a plain
|
|
118
|
+
# folder. Check parents for actual repository markers without reading keys.
|
|
119
|
+
if any((parent / ".git").exists() for parent in (root, *root.parents)):
|
|
120
|
+
raise IntakeError("git_metadata_unavailable", "Repository identity could not be confirmed. No folder fallback was used.")
|
|
121
|
+
return None, None, {}
|
|
122
|
+
try:
|
|
123
|
+
git_root = Path(os.fsdecode(top.rstrip(b"\r\n"))).resolve(strict=True)
|
|
124
|
+
relative = root.relative_to(git_root).as_posix()
|
|
125
|
+
head = git(root, "rev-parse", "--verify", "HEAD", optional=True)
|
|
126
|
+
commit = head.strip().decode("ascii") if head else None
|
|
127
|
+
if commit is not None and not re.fullmatch(r"[a-f0-9]{40}|[a-f0-9]{64}", commit):
|
|
128
|
+
raise ValueError()
|
|
129
|
+
listed = git(root, "ls-files", "--cached", "--others", "--exclude-standard", "-z")
|
|
130
|
+
paths = sorted({safe_path(os.fsdecode(raw)).as_posix() for raw in listed.split(b"\0") if raw})
|
|
131
|
+
if len(paths) > MAX_FILES:
|
|
132
|
+
raise IntakeError("source_capacity_required", "Select a project root with fewer than 100,000 source entries.")
|
|
133
|
+
baseline = {}
|
|
134
|
+
if commit:
|
|
135
|
+
tree = git(git_root, "ls-tree", "-r", "-z", commit, "--", relative)
|
|
136
|
+
prefix = "" if relative == "." else relative + "/"
|
|
137
|
+
for record in tree.split(b"\0"):
|
|
138
|
+
if not record:
|
|
139
|
+
continue
|
|
140
|
+
header, raw = record.split(b"\t", 1)
|
|
141
|
+
mode, kind, oid = header.decode("ascii").split(" ")
|
|
142
|
+
name = os.fsdecode(raw)
|
|
143
|
+
if not name.startswith(prefix):
|
|
144
|
+
raise ValueError()
|
|
145
|
+
path = safe_path(name[len(prefix):])
|
|
146
|
+
if excluded(path):
|
|
147
|
+
continue
|
|
148
|
+
if not re.fullmatch(r"[a-f0-9]{40}|[a-f0-9]{64}", oid) or len(oid) != len(commit):
|
|
149
|
+
raise ValueError()
|
|
150
|
+
if kind == "blob" and mode in {"100644", "100755"}:
|
|
151
|
+
baseline[path.as_posix()] = {"oid": oid, "mode": 0o755 if mode == "100755" else 0o644}
|
|
152
|
+
elif kind not in {"blob", "commit"}:
|
|
153
|
+
raise ValueError()
|
|
154
|
+
if len(baseline) > MAX_FILES:
|
|
155
|
+
raise IntakeError("source_capacity_required", "The base revision exceeds local inspection capacity. Select a smaller root.")
|
|
156
|
+
return {"base_commit": commit, "project_root": "" if relative == "." else relative,
|
|
157
|
+
"selection": "git_index_and_unignored_files", "unborn": commit is None}, paths, baseline
|
|
158
|
+
except (ValueError, UnicodeError, OSError):
|
|
159
|
+
raise IntakeError("git_metadata_unavailable", "Repository identity or paths could not be read safely.") from None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def directory_paths(root_fd):
|
|
163
|
+
paths, omitted = [], 0
|
|
164
|
+
|
|
165
|
+
def visit(fd, prefix=""):
|
|
166
|
+
nonlocal omitted
|
|
167
|
+
with os.scandir(fd) as entries:
|
|
168
|
+
names = []
|
|
169
|
+
for entry in entries:
|
|
170
|
+
names.append(entry.name)
|
|
171
|
+
if len(names) > MAX_FILES:
|
|
172
|
+
raise IntakeError("source_capacity_required", "Select a smaller source folder.")
|
|
173
|
+
names.sort()
|
|
174
|
+
for name in names:
|
|
175
|
+
relative = prefix + name
|
|
176
|
+
path = safe_path(relative)
|
|
177
|
+
if excluded(path):
|
|
178
|
+
omitted += 1
|
|
179
|
+
continue
|
|
180
|
+
metadata = os.stat(name, dir_fd=fd, follow_symlinks=False)
|
|
181
|
+
if stat.S_ISDIR(metadata.st_mode):
|
|
182
|
+
child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
|
|
183
|
+
try:
|
|
184
|
+
visit(child, relative + "/")
|
|
185
|
+
finally:
|
|
186
|
+
os.close(child)
|
|
187
|
+
else:
|
|
188
|
+
paths.append(relative)
|
|
189
|
+
if len(paths) > MAX_FILES:
|
|
190
|
+
raise IntakeError("source_capacity_required", "Select a smaller source folder.")
|
|
191
|
+
visit(root_fd)
|
|
192
|
+
return paths, omitted
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def opened(root_fd, path):
|
|
196
|
+
parent = os.dup(root_fd)
|
|
197
|
+
try:
|
|
198
|
+
for name in path.parts[:-1]:
|
|
199
|
+
child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent)
|
|
200
|
+
os.close(parent)
|
|
201
|
+
parent = child
|
|
202
|
+
metadata = os.stat(path.name, dir_fd=parent, follow_symlinks=False)
|
|
203
|
+
if not stat.S_ISREG(metadata.st_mode):
|
|
204
|
+
return None, metadata
|
|
205
|
+
fd = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
|
|
206
|
+
actual = os.fstat(fd)
|
|
207
|
+
if not stat.S_ISREG(actual.st_mode) or (actual.st_dev, actual.st_ino) != (metadata.st_dev, metadata.st_ino):
|
|
208
|
+
os.close(fd)
|
|
209
|
+
raise IntakeError("source_changed", "A source file changed while it was being opened. Create a new snapshot.")
|
|
210
|
+
return fd, actual
|
|
211
|
+
finally:
|
|
212
|
+
os.close(parent)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def inspect(root, copy_to=None):
|
|
216
|
+
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW"):
|
|
217
|
+
raise IntakeError("local_source_platform_unsupported", "Safe local intake currently requires macOS or Linux. Use source upload in the web app on this platform.")
|
|
218
|
+
root = root.resolve(strict=True)
|
|
219
|
+
if root == Path(root.anchor) or root == Path.home().resolve() or not root.is_dir():
|
|
220
|
+
raise IntakeError("source_root_invalid", "Choose a project folder, not the filesystem root or home folder.")
|
|
221
|
+
root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
222
|
+
try:
|
|
223
|
+
root_identity = os.fstat(root_fd)
|
|
224
|
+
identity, paths, baseline = repository(root)
|
|
225
|
+
current_root = root.lstat()
|
|
226
|
+
if (current_root.st_dev, current_root.st_ino) != (root_identity.st_dev, root_identity.st_ino):
|
|
227
|
+
raise IntakeError("source_changed", "The project directory changed during capture.")
|
|
228
|
+
omitted = 0
|
|
229
|
+
if paths is None:
|
|
230
|
+
paths, omitted = directory_paths(root_fd)
|
|
231
|
+
files, variables, changes, skipped, total = [], {}, [], {"private_or_generated": omitted, "non_regular": 0, "missing": 0}, 0
|
|
232
|
+
observed = set()
|
|
233
|
+
for raw in paths:
|
|
234
|
+
path = safe_path(raw)
|
|
235
|
+
if excluded(path):
|
|
236
|
+
skipped["private_or_generated"] += 1
|
|
237
|
+
continue
|
|
238
|
+
try:
|
|
239
|
+
fd, before = opened(root_fd, path)
|
|
240
|
+
except FileNotFoundError:
|
|
241
|
+
skipped["missing"] += 1
|
|
242
|
+
continue
|
|
243
|
+
if fd is None:
|
|
244
|
+
skipped["non_regular"] += 1
|
|
245
|
+
continue
|
|
246
|
+
try:
|
|
247
|
+
if before.st_size > MAX_FILE_BYTES or total + before.st_size > MAX_BYTES:
|
|
248
|
+
raise IntakeError("source_capacity_required", "Source exceeds local inspection capacity. Select a smaller root or request support.")
|
|
249
|
+
with os.fdopen(fd, "rb", closefd=False) as incoming:
|
|
250
|
+
body = incoming.read(MAX_FILE_BYTES + 1)
|
|
251
|
+
after = os.fstat(fd)
|
|
252
|
+
if (before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns) or len(body) != before.st_size:
|
|
253
|
+
raise IntakeError("source_changed", "Source changed during capture. Create a new snapshot rather than reusing this one.")
|
|
254
|
+
finally:
|
|
255
|
+
os.close(fd)
|
|
256
|
+
text = body.decode("utf-8", errors="replace")
|
|
257
|
+
if any(pattern.search(text) for pattern in SECRET_PATTERNS):
|
|
258
|
+
raise IntakeError("source_credentials_detected", "A possible credential or private key was detected. Move it to local configuration or Vault before capturing source.")
|
|
259
|
+
for pattern in VARIABLE_PATTERNS:
|
|
260
|
+
for match in pattern.finditer(text):
|
|
261
|
+
name = match.group(1)
|
|
262
|
+
if len(name) > 128 or name not in variables and len(variables) >= 10000:
|
|
263
|
+
raise IntakeError("source_capacity_required", "Detected configuration names exceed local inspection capacity. Select a smaller root or request support.")
|
|
264
|
+
if name not in variables:
|
|
265
|
+
variables[name] = {"name": name, "source_path": raw, "line": text.count("\n", 0, match.start()) + 1,
|
|
266
|
+
"phase": "unresolved", "required": None, "has_value": False}
|
|
267
|
+
mode = 0o755 if before.st_mode & 0o111 else 0o644
|
|
268
|
+
observed.add(raw)
|
|
269
|
+
if identity:
|
|
270
|
+
base = baseline.get(raw)
|
|
271
|
+
algorithm = "sha256" if len(identity["base_commit"] or "") == 64 else "sha1"
|
|
272
|
+
oid = hashlib.new(algorithm, b"blob " + str(len(body)).encode("ascii") + b"\0" + body).hexdigest()
|
|
273
|
+
change = "added" if base is None else "modified" if base["oid"] != oid else "mode_changed" if base["mode"] != mode else None
|
|
274
|
+
if change:
|
|
275
|
+
changes.append({"path": raw, "kind": change})
|
|
276
|
+
files.append({"path": raw, "size": len(body), "sha256": hashlib.sha256(body).hexdigest(), "mode": mode})
|
|
277
|
+
total += len(body)
|
|
278
|
+
if copy_to is not None:
|
|
279
|
+
destination = copy_to.joinpath(*path.parts)
|
|
280
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
281
|
+
with destination.open("xb") as target:
|
|
282
|
+
target.write(body)
|
|
283
|
+
destination.chmod(mode & 0o700)
|
|
284
|
+
if not files:
|
|
285
|
+
raise IntakeError("source_root_empty", "No eligible source files were found.")
|
|
286
|
+
if identity:
|
|
287
|
+
# Check the same base identity again; retained file hashes, not a
|
|
288
|
+
# clean/dirty label, define captured working-copy content.
|
|
289
|
+
last = git(root, "rev-parse", "--verify", "HEAD", optional=True)
|
|
290
|
+
if (last.strip().decode("ascii") if last else None) != identity["base_commit"]:
|
|
291
|
+
raise IntakeError("source_changed", "The repository revision changed during capture. Capture it again.")
|
|
292
|
+
for raw in sorted(set(baseline) - observed):
|
|
293
|
+
try:
|
|
294
|
+
fd, _ = opened(root_fd, safe_path(raw))
|
|
295
|
+
except FileNotFoundError:
|
|
296
|
+
changes.append({"path": raw, "kind": "deleted"})
|
|
297
|
+
else:
|
|
298
|
+
if fd is not None:
|
|
299
|
+
os.close(fd)
|
|
300
|
+
# Present but unselected/unsupported is not a deletion.
|
|
301
|
+
skipped["non_regular"] += 1
|
|
302
|
+
ordered = sorted(files, key=lambda item: item["path"])
|
|
303
|
+
current_root = root.lstat()
|
|
304
|
+
if (current_root.st_dev, current_root.st_ino) != (root_identity.st_dev, root_identity.st_ino):
|
|
305
|
+
raise IntakeError("source_changed", "The project directory changed during capture.")
|
|
306
|
+
result = {"schema": "firedrill.local-source.v1", "files": ordered, "expanded_bytes": total,
|
|
307
|
+
"content_digest": hashlib.sha256(canonical({"schema": "firedrill.local-source-content.v1", "files": ordered})).hexdigest(),
|
|
308
|
+
"repository": identity, "variables": sorted(variables.values(), key=lambda item: item["name"]), "excluded": skipped,
|
|
309
|
+
"excluded_unit": "observed_entries_not_recursive_file_totals", "git_ignored_entries_enumerated": False,
|
|
310
|
+
"changes_from_base": sorted(changes, key=lambda item: item["path"]),
|
|
311
|
+
"state": "captured_working_copy", "capture_boundary": "per_file_consistency_not_filesystem_atomic",
|
|
312
|
+
"provenance": "customer_device", "uploaded": False,
|
|
313
|
+
"disclosure": "Source pattern screening is not complete secret or personal-data detection. Review selected files before any explicit transfer. No environment values were read."}
|
|
314
|
+
if len(canonical(result)) > 24 * 1024**2:
|
|
315
|
+
raise IntakeError("source_capacity_required", "The local source manifest is too large. Select a smaller root or request support.")
|
|
316
|
+
return result
|
|
317
|
+
finally:
|
|
318
|
+
os.close(root_fd)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def snapshot(root, storage):
|
|
322
|
+
metadata = storage.lstat()
|
|
323
|
+
if (not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode) or metadata.st_uid != os.getuid()
|
|
324
|
+
or metadata.st_mode & 0o077):
|
|
325
|
+
raise IntakeError("source_storage_unsafe", "Local source storage must be private and owned by this OS user.")
|
|
326
|
+
temporary = Path(tempfile.mkdtemp(prefix="source-", dir=storage))
|
|
327
|
+
try:
|
|
328
|
+
files = temporary / "files"
|
|
329
|
+
files.mkdir(mode=0o700)
|
|
330
|
+
manifest = inspect(root, files)
|
|
331
|
+
if manifest["excluded"]["non_regular"]:
|
|
332
|
+
raise IntakeError("source_entries_unsupported", "This project contains selected links, submodules or unsupported files. Inspect the source and choose a self-contained project root before creating a snapshot.")
|
|
333
|
+
if shutil.disk_usage(storage).free < manifest["expanded_bytes"] + 32 * 1024**2:
|
|
334
|
+
raise IntakeError("source_storage_full", "Free disk space before creating the source archive.")
|
|
335
|
+
archive_path = temporary / "source.tar.gz"
|
|
336
|
+
with archive_path.open("xb") as outgoing, gzip.GzipFile(filename="", mode="wb", fileobj=outgoing, mtime=0) as compressed:
|
|
337
|
+
with tarfile.open(mode="w|", fileobj=compressed, format=tarfile.PAX_FORMAT) as archive:
|
|
338
|
+
for record in manifest["files"]:
|
|
339
|
+
source = files.joinpath(*PurePosixPath(record["path"]).parts)
|
|
340
|
+
with source.open("rb") as incoming:
|
|
341
|
+
body = incoming.read(MAX_FILE_BYTES + 1)
|
|
342
|
+
if len(body) != record["size"] or hashlib.sha256(body).hexdigest() != record["sha256"]:
|
|
343
|
+
raise IntakeError("source_changed", "The private source copy changed. Capture source again.")
|
|
344
|
+
info = tarfile.TarInfo("project/" + record["path"])
|
|
345
|
+
info.size, info.mode, info.mtime = record["size"], record["mode"], 0
|
|
346
|
+
archive.addfile(info, io.BytesIO(body))
|
|
347
|
+
archive_path.chmod(0o600)
|
|
348
|
+
with archive_path.open("rb") as incoming:
|
|
349
|
+
manifest["archive"] = {"filename": "source.tar.gz", "size": archive_path.stat().st_size,
|
|
350
|
+
"sha256": hashlib.file_digest(incoming, "sha256").hexdigest()}
|
|
351
|
+
with (temporary / "manifest.json").open("xb") as target:
|
|
352
|
+
target.write(canonical(manifest))
|
|
353
|
+
target.flush()
|
|
354
|
+
os.fsync(target.fileno())
|
|
355
|
+
(temporary / "manifest.json").chmod(0o600)
|
|
356
|
+
return {**manifest, "local_snapshot_path": str(temporary)}
|
|
357
|
+
except BaseException:
|
|
358
|
+
# Only the exact mkdtemp directory created here is removed on failure.
|
|
359
|
+
shutil.rmtree(temporary)
|
|
360
|
+
raise
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main():
|
|
364
|
+
try:
|
|
365
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="strict")
|
|
366
|
+
if not (3, 12) <= sys.version_info[:2] < (3, 14):
|
|
367
|
+
raise IntakeError("python_runtime_required", "Local source intake requires Python 3.12 or 3.13. No runtime was installed.")
|
|
368
|
+
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW"):
|
|
369
|
+
raise IntakeError("local_source_platform_unsupported", "Safe local intake currently requires macOS or Linux. Use source upload in the web app on this platform.")
|
|
370
|
+
parser = argparse.ArgumentParser()
|
|
371
|
+
parser.add_argument("action", choices=("inspect", "snapshot"))
|
|
372
|
+
parser.add_argument("--root", required=True)
|
|
373
|
+
parser.add_argument("--storage")
|
|
374
|
+
args = parser.parse_args()
|
|
375
|
+
def interrupted(*_):
|
|
376
|
+
raise IntakeError("source_canceled", "Source capture stopped. No upload or target execution occurred.")
|
|
377
|
+
signal.signal(signal.SIGTERM, interrupted)
|
|
378
|
+
signal.signal(signal.SIGINT, interrupted)
|
|
379
|
+
result = inspect(Path(args.root)) if args.action == "inspect" else snapshot(Path(args.root), Path(args.storage))
|
|
380
|
+
print(json.dumps({"ok": True, "data": result}, ensure_ascii=False, separators=(",", ":")))
|
|
381
|
+
except IntakeError as error:
|
|
382
|
+
print(json.dumps({"ok": False, "error": {"code": error.code, "message": error.message}}))
|
|
383
|
+
return 2
|
|
384
|
+
except Exception:
|
|
385
|
+
print(json.dumps({"ok": False, "error": {"code": "source_capture_failed", "message": "Source capture could not finish safely. No upload or execution occurred."}}))
|
|
386
|
+
return 1
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
if __name__ == "__main__":
|
|
391
|
+
sys.exit(main())
|