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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic target-user identity (Plan20 W10, spec 3.4).
|
|
3
|
+
*
|
|
4
|
+
* Supported modes, from the descriptor's `driver.authentication`:
|
|
5
|
+
* - `anonymous` -> nothing is injected.
|
|
6
|
+
* - `synthetic-session` -> a granted session token goes into the declared header
|
|
7
|
+
* (HTTP / JSON-lines drivers), and/or the Playwright flow
|
|
8
|
+
* performs the target's real login with the granted
|
|
9
|
+
* synthetic username/password through a login accessor.
|
|
10
|
+
*
|
|
11
|
+
* Credentials never enter env, argv, URLs, logs or observation content. The
|
|
12
|
+
* password is reachable only through `PlaywrightLoginAccessor.revealPassword()`,
|
|
13
|
+
* and the session token only through `identityHeaders()` at the request
|
|
14
|
+
* injection point. MFA, passkeys, OAuth/OIDC/SAML issuers and any other mode are
|
|
15
|
+
* unsupported and refused with `native_auth_unsupported`; nothing falls back to a
|
|
16
|
+
* real account.
|
|
17
|
+
*/
|
|
18
|
+
import { inspect } from 'node:util';
|
|
19
|
+
import { CliError } from '../http.js';
|
|
20
|
+
import { isValidHeaderName } from './bindings.js';
|
|
21
|
+
export const AUTH_ERRORS = {
|
|
22
|
+
unsupported: 'native_auth_unsupported',
|
|
23
|
+
invalid: 'native_identity_invalid',
|
|
24
|
+
};
|
|
25
|
+
function authUnsupported(message) { return new CliError(AUTH_ERRORS.unsupported, message); }
|
|
26
|
+
function identityInvalid(message) { return new CliError(AUTH_ERRORS.invalid, message); }
|
|
27
|
+
/** Grant fields that would indicate an authentication flow this client does not implement. */
|
|
28
|
+
const UNSUPPORTED_GRANT_FIELDS = ['mfa', 'totp', 'otp', 'otp_secret', 'passkey', 'webauthn', 'issuer', 'oauth', 'oidc',
|
|
29
|
+
'saml', 'sso', 'client_id', 'client_secret', 'refresh_token', 'authorization_url'];
|
|
30
|
+
/**
|
|
31
|
+
* Check a grant against the descriptor before any drill starts. Returns the grant
|
|
32
|
+
* (or null for anonymous). Throws:
|
|
33
|
+
* - native_auth_unsupported when the descriptor or grant uses a mode or factor
|
|
34
|
+
* this client does not implement (MFA, passkeys, external issuers, unknown modes);
|
|
35
|
+
* - native_identity_invalid when the grant mode does not match the descriptor,
|
|
36
|
+
* a synthetic-session grant is missing, expired, or carries no usable credential.
|
|
37
|
+
*/
|
|
38
|
+
export function validateIdentityGrant(grant, descriptor, options = {}) {
|
|
39
|
+
const expected = descriptor.driver.authentication;
|
|
40
|
+
if (expected !== 'anonymous' && expected !== 'synthetic-session') {
|
|
41
|
+
throw authUnsupported('The target identity was not prepared: the descriptor names an authentication mode this client does not support.');
|
|
42
|
+
}
|
|
43
|
+
if (grant && typeof grant === 'object') {
|
|
44
|
+
const record = grant;
|
|
45
|
+
if (UNSUPPORTED_GRANT_FIELDS.some((field) => record[field] !== undefined && record[field] !== null)) {
|
|
46
|
+
throw authUnsupported('The target identity was not prepared: MFA, passkeys and external identity issuers are not supported for native drills.');
|
|
47
|
+
}
|
|
48
|
+
if (grant.mode !== 'anonymous' && grant.mode !== 'synthetic-session') {
|
|
49
|
+
throw authUnsupported('The target identity was not prepared: the grant uses an authentication mode this client does not support.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (expected === 'anonymous') {
|
|
53
|
+
if (grant && grant.mode !== 'anonymous') {
|
|
54
|
+
throw identityInvalid('The target identity was not used: the grant mode does not match the descriptor (anonymous).');
|
|
55
|
+
}
|
|
56
|
+
return grant ?? null;
|
|
57
|
+
}
|
|
58
|
+
if (!grant) {
|
|
59
|
+
throw identityInvalid('The drill was not started: the descriptor requires a synthetic session and no identity was granted.');
|
|
60
|
+
}
|
|
61
|
+
if (grant.mode !== 'synthetic-session') {
|
|
62
|
+
throw identityInvalid('The target identity was not used: the grant mode does not match the descriptor (synthetic-session).');
|
|
63
|
+
}
|
|
64
|
+
const now = options.now ?? Date.now();
|
|
65
|
+
// A synthetic session must be short-lived: a missing expiry is refused, not treated as unbounded.
|
|
66
|
+
const at = typeof grant.expires_at === 'string' ? Date.parse(grant.expires_at) : Number.NaN;
|
|
67
|
+
if (!Number.isFinite(at) || at <= now) {
|
|
68
|
+
throw identityInvalid('The target identity was not used: the synthetic identity grant has expired or has no valid expiry.');
|
|
69
|
+
}
|
|
70
|
+
const hasToken = !!grant.session_token;
|
|
71
|
+
const hasLogin = typeof grant.username === 'string' && grant.username.length > 0 && !!grant.password;
|
|
72
|
+
if (!hasToken && !hasLogin) {
|
|
73
|
+
throw identityInvalid('The target identity was not used: the synthetic identity grant carries neither a session token nor login credentials.');
|
|
74
|
+
}
|
|
75
|
+
if (hasToken && (typeof grant.session_header !== 'string' || !isValidHeaderName(grant.session_header))) {
|
|
76
|
+
throw identityInvalid('The target identity was not used: the session token has no allowed header name.');
|
|
77
|
+
}
|
|
78
|
+
if (grant.password && !hasLogin) {
|
|
79
|
+
throw identityInvalid('The target identity was not used: a synthetic password was granted without a username.');
|
|
80
|
+
}
|
|
81
|
+
return grant;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Environment contribution for the target identity. Always empty for the
|
|
85
|
+
* supported modes: identity credentials are never placed in env. `secrets`
|
|
86
|
+
* lists every identity credential so the supervisor and drivers can redact
|
|
87
|
+
* them from captured output.
|
|
88
|
+
*/
|
|
89
|
+
export function identityEnv(grant) {
|
|
90
|
+
return { env: {}, secrets: identitySecrets(grant) };
|
|
91
|
+
}
|
|
92
|
+
/** All secret material in a grant, for redaction lists. */
|
|
93
|
+
export function identitySecrets(grant) {
|
|
94
|
+
if (!grant)
|
|
95
|
+
return [];
|
|
96
|
+
return [grant.session_token, grant.password].filter((value) => !!value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Request headers carrying the synthetic session for HTTP / JSON-lines drivers.
|
|
100
|
+
* Reveals the session token: call only when building the outgoing request and
|
|
101
|
+
* never log the result. Anonymous or login-only grants return `{}`. The token is
|
|
102
|
+
* sent raw unless the grant's session_scheme is 'bearer'.
|
|
103
|
+
*/
|
|
104
|
+
export function identityHeaders(grant) {
|
|
105
|
+
if (!grant || grant.mode !== 'synthetic-session' || !grant.session_token)
|
|
106
|
+
return {};
|
|
107
|
+
const header = grant.session_header;
|
|
108
|
+
if (typeof header !== 'string' || !isValidHeaderName(header)) {
|
|
109
|
+
throw identityInvalid('The target identity was not used: the session token has no allowed header name.');
|
|
110
|
+
}
|
|
111
|
+
const value = grant.session_token.reveal();
|
|
112
|
+
if (!value || value.length > 8192 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
113
|
+
throw identityInvalid('The target identity was not used: the session token cannot be sent as a header.');
|
|
114
|
+
}
|
|
115
|
+
return { [header]: grant.session_scheme === 'bearer' ? `Bearer ${value}` : value };
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Playwright login accessor. The only path from a granted password to the
|
|
119
|
+
// target, and only inside the approved flow's `login(page, identity)`.
|
|
120
|
+
const accessorPasswords = new WeakMap();
|
|
121
|
+
export class PlaywrightLoginAccessor {
|
|
122
|
+
/** Synthetic account username (not a production account). */
|
|
123
|
+
username;
|
|
124
|
+
subject;
|
|
125
|
+
roles;
|
|
126
|
+
constructor(username, password, subject, roles) {
|
|
127
|
+
this.username = username;
|
|
128
|
+
this.subject = subject;
|
|
129
|
+
this.roles = Object.freeze([...roles]);
|
|
130
|
+
accessorPasswords.set(this, password);
|
|
131
|
+
}
|
|
132
|
+
/** Reveal the password for typing into the target's login form. Never log, store or navigate with it. */
|
|
133
|
+
revealPassword() {
|
|
134
|
+
const secret = accessorPasswords.get(this);
|
|
135
|
+
if (!secret)
|
|
136
|
+
throw identityInvalid('The login was not performed: the synthetic password is no longer available.');
|
|
137
|
+
return secret.reveal();
|
|
138
|
+
}
|
|
139
|
+
/** Drop the password once the login step is finished. */
|
|
140
|
+
dispose() { accessorPasswords.delete(this); }
|
|
141
|
+
toJSON() { return { username: '[redacted]', subject: this.subject, roles: [...this.roles] }; }
|
|
142
|
+
toString() { return '[playwright-login]'; }
|
|
143
|
+
[inspect.custom]() { return '[playwright-login]'; }
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Login accessor for the Playwright flow, or null when the grant has no
|
|
147
|
+
* username/password (anonymous, or token-only sessions). The username is kept
|
|
148
|
+
* out of env/argv too, and redacted from JSON, because it identifies the seeded
|
|
149
|
+
* synthetic account.
|
|
150
|
+
*/
|
|
151
|
+
export function playwrightLogin(grant) {
|
|
152
|
+
if (!grant || grant.mode !== 'synthetic-session')
|
|
153
|
+
return null;
|
|
154
|
+
if (typeof grant.username !== 'string' || !grant.username || !grant.password)
|
|
155
|
+
return null;
|
|
156
|
+
return new PlaywrightLoginAccessor(grant.username, grant.password, grant.subject, grant.roles ?? []);
|
|
157
|
+
}
|
|
158
|
+
/** Explicit refusal for a requested factor or issuer the client does not implement. */
|
|
159
|
+
export function assertAuthSupported(feature) {
|
|
160
|
+
throw authUnsupported(`The target identity was not prepared: ${feature} authentication is not supported for native drills.`);
|
|
161
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-drill target state handling (Plan20 W10, spec 3.4).
|
|
3
|
+
*
|
|
4
|
+
* For every declared state domain the descriptor names a reset policy. This
|
|
5
|
+
* module turns those into per-drill actions, runs approved reset hooks, records
|
|
6
|
+
* what the supervisor and driver report, and produces
|
|
7
|
+
* `AssignmentCompletion.state_coverage`.
|
|
8
|
+
*
|
|
9
|
+
* - fresh-process -> needs a per-drill launch service copy; `applied` only when
|
|
10
|
+
* the supervisor reports it started a fresh process for this drill.
|
|
11
|
+
* - fresh-session -> a new driver session / browser context per drill; `applied`
|
|
12
|
+
* when the driver reports the new session. Only `session` and
|
|
13
|
+
* `browser` domains can be reset this way: a new browser context
|
|
14
|
+
* does not reset backend storage (database, files, cache, queue,
|
|
15
|
+
* vector, scheduler, process), so those report `unavailable`.
|
|
16
|
+
* - approved-hook -> re-hash the hook file from disk, compare to the descriptor
|
|
17
|
+
* and plan digests, run it with the plan runtime argv (shell:false,
|
|
18
|
+
* minimal env, bounded time/output). Exit 0 is reported evidence
|
|
19
|
+
* (`applied`), not proof the state was reset; anything else is `failed`.
|
|
20
|
+
* - unavailable, or location `observation-only` -> `unavailable`, and drills run serially.
|
|
21
|
+
* - location `cloud-disposable` -> reset is cloud-owned; reported `not-applicable` locally.
|
|
22
|
+
*
|
|
23
|
+
* Results are client-reported evidence; the cloud remains authoritative.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
import { nativeError } from './contracts.js';
|
|
28
|
+
import { sha256Hex } from './canonical.js';
|
|
29
|
+
import { insideRoot } from './roots.js';
|
|
30
|
+
import { ownGroup, sweepGroup } from './owned-groups.js';
|
|
31
|
+
const SESSION_RESETTABLE = new Set(['session', 'browser']);
|
|
32
|
+
function planDomain(domain, plan) {
|
|
33
|
+
const base = {
|
|
34
|
+
alias: domain.alias, service_alias: domain.service_alias, kind: domain.kind, reset: domain.reset,
|
|
35
|
+
hook: null, descriptor_hook_sha256: domain.hook_sha256,
|
|
36
|
+
};
|
|
37
|
+
const declaredSerial = domain.concurrency === 'serial';
|
|
38
|
+
if (domain.location === 'observation-only') {
|
|
39
|
+
return { ...base, action: 'unavailable', serial: true, reason: 'The domain is observation-only; it is not reset locally.' };
|
|
40
|
+
}
|
|
41
|
+
if (domain.location === 'cloud-disposable') {
|
|
42
|
+
return { ...base, action: 'not-applicable', serial: declaredSerial, reason: 'The domain is cloud-disposable; its reset is cloud-owned.' };
|
|
43
|
+
}
|
|
44
|
+
switch (domain.reset) {
|
|
45
|
+
case 'unavailable':
|
|
46
|
+
return { ...base, action: 'unavailable', serial: true, reason: 'The descriptor declares no reset for this domain.' };
|
|
47
|
+
case 'fresh-session':
|
|
48
|
+
if (!SESSION_RESETTABLE.has(domain.kind)) {
|
|
49
|
+
return { ...base, action: 'unavailable', serial: true,
|
|
50
|
+
reason: `A fresh driver session does not reset ${domain.kind} state, so this domain is not reset.` };
|
|
51
|
+
}
|
|
52
|
+
return { ...base, action: 'fresh-session', serial: declaredSerial, reason: null };
|
|
53
|
+
case 'fresh-process': {
|
|
54
|
+
const service = plan.services.find((entry) => entry.alias === domain.service_alias);
|
|
55
|
+
if (!service || service.mode !== 'launch' || !service.per_drill) {
|
|
56
|
+
throw nativeError('state_unsupported', `State domain "${domain.alias}" cannot be reset: fresh-process needs its service launched as a per-drill copy in the approved plan. No drill was started.`);
|
|
57
|
+
}
|
|
58
|
+
return { ...base, action: 'fresh-process', serial: declaredSerial, reason: null };
|
|
59
|
+
}
|
|
60
|
+
case 'approved-hook': {
|
|
61
|
+
const hooks = plan.state_hooks.filter((entry) => entry.domain_alias === domain.alias);
|
|
62
|
+
if (hooks.length !== 1) {
|
|
63
|
+
throw nativeError('state_unsupported', `State domain "${domain.alias}" cannot be reset: the approved plan must name exactly one hook for it. No drill was started.`);
|
|
64
|
+
}
|
|
65
|
+
const hook = hooks[0];
|
|
66
|
+
if (!domain.hook_sha256 || hook.sha256 !== domain.hook_sha256) {
|
|
67
|
+
throw nativeError('plan_changed', `State domain "${domain.alias}" was not reset: the plan hook digest does not match the descriptor. Approve the plan again.`);
|
|
68
|
+
}
|
|
69
|
+
return { ...base, action: 'approved-hook', serial: declaredSerial, reason: null, hook };
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw nativeError('state_unsupported', `State domain "${domain.alias}" cannot be reset: its reset policy is not supported by this client.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Resolve every declared state domain to a per-drill action. Throws before any drill when the plan cannot honour a policy. */
|
|
76
|
+
export function planStateActions(descriptor, plan) {
|
|
77
|
+
const actions = descriptor.state_domains.map((domain) => planDomain(domain, plan));
|
|
78
|
+
return { actions, serial: actions.some((action) => action.serial) };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 1 when the descriptor declares no state domains (isolation unknown, matching
|
|
82
|
+
* normalizeDescriptor's requires_serial_execution and the approval text) or any
|
|
83
|
+
* domain is serial (declared serial, unavailable or observation-only);
|
|
84
|
+
* otherwise the plan limit (at least 1).
|
|
85
|
+
*/
|
|
86
|
+
export function effectiveParallelism(descriptor, plan) {
|
|
87
|
+
if (descriptor.state_domains.length === 0 || planStateActions(descriptor, plan).serial)
|
|
88
|
+
return 1;
|
|
89
|
+
const limit = Math.floor(plan.max_parallel_drills);
|
|
90
|
+
return Number.isFinite(limit) && limit > 1 ? limit : 1;
|
|
91
|
+
}
|
|
92
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 60_000;
|
|
93
|
+
const MAX_HOOK_TIMEOUT_MS = 600_000;
|
|
94
|
+
const DEFAULT_OUTPUT_BYTES = 16 * 1024;
|
|
95
|
+
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
96
|
+
/** After the hook exits, how long its output may still drain before the pipes are released. */
|
|
97
|
+
const EXIT_DRAIN_MS = 500;
|
|
98
|
+
/** After a timeout or cancel kill, how long to wait for the hook to exit before reporting the stop anyway. */
|
|
99
|
+
const KILL_GRACE_MS = 5_000;
|
|
100
|
+
function hookEnv(domainAlias, drillKey) {
|
|
101
|
+
const names = process.platform === 'win32'
|
|
102
|
+
? ['PATH', 'SystemRoot', 'TEMP', 'TMP', 'USERPROFILE', 'PATHEXT', 'ComSpec', 'LANG']
|
|
103
|
+
: ['PATH', 'HOME', 'LANG', 'TMPDIR'];
|
|
104
|
+
const env = {};
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
const value = process.env[name];
|
|
107
|
+
if (typeof value === 'string')
|
|
108
|
+
env[name] = value;
|
|
109
|
+
}
|
|
110
|
+
env.FIREDRILL_STATE_DOMAIN = domainAlias;
|
|
111
|
+
if (drillKey)
|
|
112
|
+
env.FIREDRILL_DRILL_KEY = drillKey;
|
|
113
|
+
return env;
|
|
114
|
+
}
|
|
115
|
+
function failedRun(domainAlias, code, started) {
|
|
116
|
+
return { domain_alias: domainAlias, result: 'failed', error_code: code, exit_code: null, signal: null,
|
|
117
|
+
duration_ms: Date.now() - started, output_tail_unredacted: '', output_truncated: false };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Re-hash and run one approved hook. Never throws for hook failure; returns a
|
|
121
|
+
* `failed` run with a stable code instead. The hook file is hashed immediately
|
|
122
|
+
* before launch; the runtime then reads the same path (a change in between is a
|
|
123
|
+
* local trust boundary this client cannot close without an OS sandbox).
|
|
124
|
+
*/
|
|
125
|
+
export async function runApprovedHook(action, ctx) {
|
|
126
|
+
const started = Date.now();
|
|
127
|
+
const hook = action.hook;
|
|
128
|
+
if (action.action !== 'approved-hook' || !hook)
|
|
129
|
+
return failedRun(action.alias, 'native_state_unsupported', started);
|
|
130
|
+
if (ctx.signal.aborted)
|
|
131
|
+
return failedRun(action.alias, 'native_hook_cancelled', started);
|
|
132
|
+
const root = ctx.plan.roots.find((entry) => entry.alias === hook.root_alias);
|
|
133
|
+
if (!root)
|
|
134
|
+
return failedRun(action.alias, 'native_root_outside', started);
|
|
135
|
+
let hookPath;
|
|
136
|
+
try {
|
|
137
|
+
hookPath = await insideRoot(root.path, hook.path);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return failedRun(action.alias, 'native_root_outside', started);
|
|
141
|
+
}
|
|
142
|
+
let digest;
|
|
143
|
+
try {
|
|
144
|
+
digest = sha256Hex(await readFile(hookPath));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return failedRun(action.alias, 'native_hook_unavailable', started);
|
|
148
|
+
}
|
|
149
|
+
if (digest !== hook.sha256 || digest !== action.descriptor_hook_sha256)
|
|
150
|
+
return failedRun(action.alias, 'native_plan_changed', started);
|
|
151
|
+
// The hook runs in place (not from a copy) so its own relative imports keep
|
|
152
|
+
// working. A same-user process could swap the file between this hash check and
|
|
153
|
+
// the runtime reading it; that is outside the native trust model (the user's
|
|
154
|
+
// own code is trusted), and the hash still detects ordinary edits.
|
|
155
|
+
if (typeof hook.runtime?.executable !== 'string' || !hook.runtime.executable || !Array.isArray(hook.runtime.args)) {
|
|
156
|
+
return failedRun(action.alias, 'native_hook_unavailable', started);
|
|
157
|
+
}
|
|
158
|
+
const timeoutMs = Number.isSafeInteger(hook.timeout_ms) && hook.timeout_ms > 0
|
|
159
|
+
? Math.min(hook.timeout_ms, MAX_HOOK_TIMEOUT_MS) : DEFAULT_HOOK_TIMEOUT_MS;
|
|
160
|
+
const cap = Math.min(Math.max(ctx.outputBytes ?? DEFAULT_OUTPUT_BYTES, 1024), MAX_OUTPUT_BYTES);
|
|
161
|
+
const posix = process.platform !== 'win32';
|
|
162
|
+
// Cancellation can arrive while resolving or reading the approved hook.
|
|
163
|
+
// No asynchronous preparation remains between this check and spawn.
|
|
164
|
+
if (ctx.signal.aborted)
|
|
165
|
+
return failedRun(action.alias, 'native_hook_cancelled', started);
|
|
166
|
+
return await new Promise((resolve) => {
|
|
167
|
+
let tail = Buffer.alloc(0);
|
|
168
|
+
let truncated = false;
|
|
169
|
+
let settled = false;
|
|
170
|
+
let stopCode = null;
|
|
171
|
+
let drain;
|
|
172
|
+
let grace;
|
|
173
|
+
const child = spawn(hook.runtime.executable, [...hook.runtime.args, hookPath], {
|
|
174
|
+
shell: false, cwd: root.path, env: hookEnv(action.alias, ctx.drillKey),
|
|
175
|
+
stdio: ['ignore', 'pipe', 'pipe'], detached: posix, windowsHide: true,
|
|
176
|
+
});
|
|
177
|
+
if (posix)
|
|
178
|
+
child.once('spawn', () => ownGroup(child.pid));
|
|
179
|
+
const kill = (code) => {
|
|
180
|
+
// Once the hook exited its result is decided, and its group was swept and released: that id is never signalled again.
|
|
181
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
182
|
+
return;
|
|
183
|
+
if (stopCode === null)
|
|
184
|
+
stopCode = code;
|
|
185
|
+
try {
|
|
186
|
+
if (posix && typeof child.pid === 'number')
|
|
187
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
188
|
+
else
|
|
189
|
+
child.kill('SIGKILL');
|
|
190
|
+
}
|
|
191
|
+
catch { /* already exited */ }
|
|
192
|
+
// A hook that does not exit even then must not hold the drill: the stop is reported without its exit.
|
|
193
|
+
grace ??= setTimeout(() => finish(result(null, null)), KILL_GRACE_MS);
|
|
194
|
+
};
|
|
195
|
+
const capture = (chunk) => {
|
|
196
|
+
tail = Buffer.concat([tail, chunk]);
|
|
197
|
+
if (tail.length > cap) {
|
|
198
|
+
truncated = true;
|
|
199
|
+
tail = tail.subarray(tail.length - cap);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
child.stdout?.on('data', capture);
|
|
203
|
+
child.stderr?.on('data', capture);
|
|
204
|
+
const timer = setTimeout(() => kill('native_hook_timeout'), timeoutMs);
|
|
205
|
+
const onAbort = () => kill('native_hook_cancelled');
|
|
206
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
207
|
+
const result = (code, signal) => {
|
|
208
|
+
const ok = stopCode === null && code === 0;
|
|
209
|
+
return {
|
|
210
|
+
domain_alias: action.alias, result: ok ? 'applied' : 'failed',
|
|
211
|
+
error_code: ok ? null : (stopCode ?? 'native_hook_exit'),
|
|
212
|
+
exit_code: typeof code === 'number' ? code : null, signal: signal ?? null,
|
|
213
|
+
duration_ms: Date.now() - started, output_tail_unredacted: tail.toString('utf8'), output_truncated: truncated,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const finish = (run) => {
|
|
217
|
+
if (settled)
|
|
218
|
+
return;
|
|
219
|
+
settled = true;
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
clearTimeout(drain);
|
|
222
|
+
clearTimeout(grace);
|
|
223
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
224
|
+
// A descendant that left the hook's group may still hold the pipes; it must not keep the companion waiting.
|
|
225
|
+
for (const stream of [child.stdout, child.stderr])
|
|
226
|
+
stream?.destroy();
|
|
227
|
+
resolve(run);
|
|
228
|
+
};
|
|
229
|
+
child.once('error', () => finish(failedRun(action.alias, stopCode ?? 'native_hook_unavailable', started)));
|
|
230
|
+
child.once('exit', (code, signal) => {
|
|
231
|
+
// Processes the hook left running are ours, not part of its result: end them with it.
|
|
232
|
+
if (posix)
|
|
233
|
+
sweepGroup(child.pid);
|
|
234
|
+
clearTimeout(grace);
|
|
235
|
+
// 'close' also waits for every holder of the pipes, and a descendant that escaped the group (a new session)
|
|
236
|
+
// can hold them for good: the output drains briefly, then the result is the exit.
|
|
237
|
+
drain = setTimeout(() => finish(result(code, signal)), EXIT_DRAIN_MS);
|
|
238
|
+
});
|
|
239
|
+
child.once('close', (code, signal) => finish(result(code, signal)));
|
|
240
|
+
if (ctx.signal.aborted)
|
|
241
|
+
onAbort();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Per-drill record of state handling. Hooks are applied by applyDrillState; the
|
|
246
|
+
* supervisor calls `markFreshProcess` after starting a per-drill copy, and the
|
|
247
|
+
* driver path calls `markFreshSession` after opening a new session/context.
|
|
248
|
+
* Anything still pending at `coverage()` is reported `failed` (not confirmed).
|
|
249
|
+
*/
|
|
250
|
+
export class DrillStateTracker {
|
|
251
|
+
hookRuns = [];
|
|
252
|
+
#domains = new Map();
|
|
253
|
+
constructor(statePlan) {
|
|
254
|
+
for (const action of statePlan.actions) {
|
|
255
|
+
const result = action.action === 'unavailable' ? 'unavailable'
|
|
256
|
+
: action.action === 'not-applicable' ? 'not-applicable' : 'pending';
|
|
257
|
+
this.#domains.set(action.alias, { action, result, error_code: null });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** The supervisor started a fresh process (new per-drill copy) of `serviceAlias` for this drill. */
|
|
261
|
+
markFreshProcess(serviceAlias) {
|
|
262
|
+
for (const state of this.#domains.values()) {
|
|
263
|
+
if (state.action.action === 'fresh-process' && state.action.service_alias === serviceAlias)
|
|
264
|
+
state.result = 'applied';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** The driver opened a new session (new browser context for playwright) for this drill. */
|
|
268
|
+
markFreshSession() {
|
|
269
|
+
for (const state of this.#domains.values()) {
|
|
270
|
+
if (state.action.action === 'fresh-session')
|
|
271
|
+
state.result = 'applied';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Record a failure the supervisor or driver observed for a domain's reset. */
|
|
275
|
+
markFailed(alias, errorCode) {
|
|
276
|
+
const state = this.#domains.get(alias);
|
|
277
|
+
if (state && state.action.action !== 'unavailable' && state.action.action !== 'not-applicable') {
|
|
278
|
+
state.result = 'failed';
|
|
279
|
+
state.error_code = errorCode;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
recordHook(run) {
|
|
283
|
+
this.hookRuns.push(run);
|
|
284
|
+
const state = this.#domains.get(run.domain_alias);
|
|
285
|
+
if (state) {
|
|
286
|
+
state.result = run.result;
|
|
287
|
+
state.error_code = run.error_code;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Domains whose reset failed, with stable codes. */
|
|
291
|
+
failures() {
|
|
292
|
+
return [...this.#domains.values()].filter((state) => state.result === 'failed')
|
|
293
|
+
.map((state) => ({ alias: state.action.alias, error_code: state.error_code }));
|
|
294
|
+
}
|
|
295
|
+
/** True when a hook digest no longer matches: the approval is void and the drill must stop. */
|
|
296
|
+
planChanged() {
|
|
297
|
+
return this.hookRuns.some((run) => run.error_code === 'native_plan_changed');
|
|
298
|
+
}
|
|
299
|
+
coverage() {
|
|
300
|
+
return [...this.#domains.values()].map((state) => ({
|
|
301
|
+
alias: state.action.alias, reset: state.action.reset,
|
|
302
|
+
result: state.result === 'pending' ? 'failed' : state.result,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Begin state handling for one drill: run every approved hook (sequentially, in
|
|
308
|
+
* descriptor order) and return the tracker the supervisor and driver report to.
|
|
309
|
+
* Hook failures are recorded, not thrown; callers decide whether to proceed and
|
|
310
|
+
* must stop when `tracker.planChanged()` is true.
|
|
311
|
+
*/
|
|
312
|
+
export async function applyDrillState(statePlan, ctx) {
|
|
313
|
+
const tracker = new DrillStateTracker(statePlan);
|
|
314
|
+
for (const action of statePlan.actions) {
|
|
315
|
+
if (action.action !== 'approved-hook')
|
|
316
|
+
continue;
|
|
317
|
+
if (ctx.signal.aborted) {
|
|
318
|
+
tracker.recordHook(failedRun(action.alias, 'native_hook_cancelled', Date.now()));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
tracker.recordHook(await runApprovedHook(action, ctx));
|
|
322
|
+
}
|
|
323
|
+
return tracker;
|
|
324
|
+
}
|
|
325
|
+
/** Convenience guard for callers: throw when a tracker shows the plan changed under a running session. */
|
|
326
|
+
export function assertPlanUnchanged(tracker) {
|
|
327
|
+
if (tracker.planChanged()) {
|
|
328
|
+
throw nativeError('plan_changed', 'The drill was not continued: an approved reset hook changed on disk. Approve the plan again.');
|
|
329
|
+
}
|
|
330
|
+
}
|