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,600 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* playwright.v1 driver (Plan20 spec 3.3).
|
|
11
|
+
*
|
|
12
|
+
* Playwright is customer-provided: it is resolved from the approved customer
|
|
13
|
+
* root with createRequire(<root>/package.json) and loaded with a dynamic import
|
|
14
|
+
* of the resolved file URL. Firedrill never bundles, installs or downloads it
|
|
15
|
+
* (nor its browsers). describe() reports unavailable, with the reason, when the
|
|
16
|
+
* package cannot be resolved; prepare() also fails clearly when the browser
|
|
17
|
+
* binary cannot be launched. The browser used is Chromium, headless.
|
|
18
|
+
*
|
|
19
|
+
* Flow module: an approved file inside the root, pinned by ctx.adapterSha256,
|
|
20
|
+
* re-hashed before import and imported via its file URL (digest in the query so
|
|
21
|
+
* a changed file is never served from the module cache). It exports
|
|
22
|
+
* login(page, identity) identity = target-auth PlaywrightLoginAccessor | null
|
|
23
|
+
* send(page, step) step = target-visible view only; returns observed output
|
|
24
|
+
* reset(page) optional; resets client-side view state
|
|
25
|
+
* as named exports or as properties of the default export.
|
|
26
|
+
*
|
|
27
|
+
* One browser context per drill session (fresh cookies/storage). A context reset
|
|
28
|
+
* or the flow's reset() is reported as a browser-context reset with
|
|
29
|
+
* backend_reset:false; it is never reported as a backend reset.
|
|
30
|
+
*
|
|
31
|
+
* Screenshots are off unless the approved plan turns them on (`screenshots`).
|
|
32
|
+
* Then each one is the visible page as a PNG, taken with every form field and
|
|
33
|
+
* the plan's selectors blacked out first (screenshots.ts), re-encoded without
|
|
34
|
+
* metadata (png.ts) and capped in count. Masking is not secret detection: other
|
|
35
|
+
* text on screen is captured as shown, so flows should keep secrets off screen.
|
|
36
|
+
* Each drill records that capture was requested and whether it can run
|
|
37
|
+
* (`capture.requested`); a step whose capture produced no image records why
|
|
38
|
+
* (`capture.unavailable`), never the error or page content.
|
|
39
|
+
* With `screenshots.frames` the driver also captures masked frames while each
|
|
40
|
+
* step's send() runs, one per interval and at most `max_per_step` per step,
|
|
41
|
+
* with animations left running so the flow's page is not changed. They are
|
|
42
|
+
* captured frames, not a video. A frame that produced no image, and every
|
|
43
|
+
* interval past the step's count, is counted with its reason.
|
|
44
|
+
* With `trace: "sanitized-actions"` the flow gets its page through a recording
|
|
45
|
+
* view (action-trace.ts) that keeps only operation kinds, outcomes and times.
|
|
46
|
+
* Playwright video, Playwright trace files and HAR are never used: they would
|
|
47
|
+
* carry unmasked frames, input values or network bodies.
|
|
48
|
+
* The flow runs in the companion process as trusted, user-approved local code.
|
|
49
|
+
* A step whose send() timed out leaves the page in an unknown state, so that
|
|
50
|
+
* session's context is closed and later steps in it fail explicitly.
|
|
51
|
+
*/
|
|
52
|
+
import { readFileSync } from 'node:fs';
|
|
53
|
+
import { createRequire } from 'node:module';
|
|
54
|
+
import path from 'node:path';
|
|
55
|
+
import { pathToFileURL } from 'node:url';
|
|
56
|
+
import { CliError } from '../../http.js';
|
|
57
|
+
import { CAPTURE_SCHEMA, FRAME_OMISSION_REASONS, NATIVE_ERRORS, nativeError } from '../contracts.js';
|
|
58
|
+
import { metadataFreePng } from '../png.js';
|
|
59
|
+
import { MASK_COLOR, maskingPolicy } from '../screenshots.js';
|
|
60
|
+
import { ActionTrace } from './action-trace.js';
|
|
61
|
+
import { sha256Hex } from '../canonical.js';
|
|
62
|
+
import { insideRoot } from '../roots.js';
|
|
63
|
+
import { playwrightLogin } from '../target-auth.js';
|
|
64
|
+
import { DRIVER_ERRORS, OwnDigest, StepRecorder, clockOf, driverBounds, driverPlanFor, emit, emitTrace, loopbackBase, loopbackTarget, planError, precheck, readPinned, rootPath, serviceHandle, stepAbort, stepInput, stepTimeoutMs, unattested, } from './types.js';
|
|
65
|
+
/** Playwright's own version from the project's installed package, or null when it cannot be read. */
|
|
66
|
+
function playwrightVersion(root) {
|
|
67
|
+
try {
|
|
68
|
+
const manifest = createRequire(path.join(root, 'package.json')).resolve('playwright/package.json');
|
|
69
|
+
const version = JSON.parse(readFileSync(manifest, 'utf8'))?.version;
|
|
70
|
+
const match = typeof version === 'string' ? /^(\d+)\.(\d+)\./.exec(version) : null;
|
|
71
|
+
return match ? [Number(match[1]), Number(match[2])] : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Screenshot masking with a fixed mask color needs Playwright 1.35 or later. */
|
|
78
|
+
const MASK_MIN_VERSION = [1, 35];
|
|
79
|
+
/** Resolve the customer's Playwright entry from a root, or null when absent. */
|
|
80
|
+
export function resolvePlaywright(root) {
|
|
81
|
+
if (!root || !path.isAbsolute(root))
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
return createRequire(path.join(root, 'package.json')).resolve('playwright');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function browserTypeFrom(module) {
|
|
91
|
+
const record = module;
|
|
92
|
+
const candidate = (record?.chromium ?? record?.default?.chromium);
|
|
93
|
+
return candidate && typeof candidate.launch === 'function' ? candidate : null;
|
|
94
|
+
}
|
|
95
|
+
function flowFrom(module) {
|
|
96
|
+
const record = module;
|
|
97
|
+
const source = (typeof record?.send === 'function' ? record : record?.default);
|
|
98
|
+
if (!source || typeof source.login !== 'function' || typeof source.send !== 'function')
|
|
99
|
+
return null;
|
|
100
|
+
if (source.reset !== undefined && typeof source.reset !== 'function')
|
|
101
|
+
return null;
|
|
102
|
+
return {
|
|
103
|
+
login: source.login,
|
|
104
|
+
send: source.send,
|
|
105
|
+
reset: source.reset,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const BROWSER_ENV = ['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TMP', 'TEMP', 'SystemRoot', 'USERPROFILE', 'LOCALAPPDATA', 'XDG_RUNTIME_DIR'];
|
|
109
|
+
function browserEnv(env) {
|
|
110
|
+
const output = {};
|
|
111
|
+
for (const name of BROWSER_ENV)
|
|
112
|
+
if (typeof env[name] === 'string')
|
|
113
|
+
output[name] = env[name];
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
class FlowAborted extends Error {
|
|
117
|
+
}
|
|
118
|
+
class FlowTimeout extends Error {
|
|
119
|
+
}
|
|
120
|
+
/** Frames wait in memory until their step ends: at most this many bytes of them per step. */
|
|
121
|
+
const FRAME_STEP_MAX_BYTES = 16 * 1024 * 1024;
|
|
122
|
+
/** Wait `ms`, ending early when `signal` aborts; true when the whole wait passed. */
|
|
123
|
+
function pause(ms, signal) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
if (signal.aborted) {
|
|
126
|
+
resolve(false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const done = (passed) => { clearTimeout(timer); signal.removeEventListener('abort', aborted); resolve(passed); };
|
|
130
|
+
const aborted = () => done(false);
|
|
131
|
+
const timer = setTimeout(() => done(true), ms);
|
|
132
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function withFrames(capture, frames) {
|
|
136
|
+
return capture && frames ? { ...capture, frames } : capture;
|
|
137
|
+
}
|
|
138
|
+
function raceAbort(work, signal) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
if (signal.aborted) {
|
|
141
|
+
void work.catch(() => undefined);
|
|
142
|
+
reject(new FlowAborted());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const onAbort = () => reject(new FlowAborted());
|
|
146
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
147
|
+
work.then((value) => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); });
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function withTimeout(work, ms, signal) {
|
|
151
|
+
if (signal.aborted)
|
|
152
|
+
throw new FlowAborted();
|
|
153
|
+
const timer = AbortSignal.timeout(Math.max(1, ms));
|
|
154
|
+
const combined = AbortSignal.any([timer, signal]);
|
|
155
|
+
try {
|
|
156
|
+
return await raceAbort(Promise.resolve().then(() => { if (combined.aborted)
|
|
157
|
+
throw new FlowAborted(); return work(); }), combined);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error instanceof FlowAborted && timer.aborted)
|
|
161
|
+
throw new FlowTimeout();
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** A timed-out allocation may still return a handle. Keep its original cleanup
|
|
166
|
+
* obligation until that exact handle has closed; never turn timeout into absence. */
|
|
167
|
+
async function acquire(work, ms, signal, pending) {
|
|
168
|
+
if (signal.aborted)
|
|
169
|
+
throw new FlowAborted();
|
|
170
|
+
let finish;
|
|
171
|
+
const obligation = new Promise(resolve => { finish = resolve; });
|
|
172
|
+
pending.add(obligation);
|
|
173
|
+
void obligation.then(() => pending.delete(obligation), () => undefined);
|
|
174
|
+
const original = Promise.resolve().then(() => { if (signal.aborted)
|
|
175
|
+
throw new FlowAborted(); return work(); });
|
|
176
|
+
try {
|
|
177
|
+
const handle = await withTimeout(() => original, ms, signal);
|
|
178
|
+
finish();
|
|
179
|
+
return handle;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const cleanup = original.then(handle => handle.close(), () => undefined);
|
|
183
|
+
finish(cleanup);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function errorSummary(error) {
|
|
188
|
+
if (error instanceof Error)
|
|
189
|
+
return { name: error.name, message: error.message };
|
|
190
|
+
return { name: typeof error };
|
|
191
|
+
}
|
|
192
|
+
export class PlaywrightDriver {
|
|
193
|
+
profile = 'playwright.v1';
|
|
194
|
+
plan;
|
|
195
|
+
prepared = null;
|
|
196
|
+
sessions = new Set();
|
|
197
|
+
pendingCleanup = new Set();
|
|
198
|
+
cleanupMs = 60_000;
|
|
199
|
+
disposing = false;
|
|
200
|
+
lifecycle = new AbortController();
|
|
201
|
+
constructor(options = {}) { this.plan = options.plan ?? null; }
|
|
202
|
+
async describe() {
|
|
203
|
+
const plan = this.plan;
|
|
204
|
+
const roots = !plan ? [] : plan.driver.profile === 'playwright.v1'
|
|
205
|
+
? plan.roots.filter((root) => root.alias === plan.driver.root_alias)
|
|
206
|
+
: plan.roots;
|
|
207
|
+
if (!roots.length)
|
|
208
|
+
return { available: false, reason: 'No approved project root is available to resolve the customer-provided Playwright package from.' };
|
|
209
|
+
for (const root of roots)
|
|
210
|
+
if (resolvePlaywright(root.path))
|
|
211
|
+
return { available: true, reason: null };
|
|
212
|
+
return { available: false,
|
|
213
|
+
reason: 'Playwright is not installed in the approved project root. Firedrill does not install or bundle it; add it to the project to run browser drills.' };
|
|
214
|
+
}
|
|
215
|
+
async prepare(ctx) {
|
|
216
|
+
ctx = { ...ctx, signal: AbortSignal.any([ctx.signal, this.lifecycle.signal]) };
|
|
217
|
+
const plan = driverPlanFor(ctx, 'playwright.v1');
|
|
218
|
+
if (this.prepared)
|
|
219
|
+
throw planError('The browser driver is already prepared; a second browser was not started.');
|
|
220
|
+
const bounds = driverBounds(ctx);
|
|
221
|
+
this.cleanupMs = bounds.flowSetupMs;
|
|
222
|
+
if (this.disposing || this.pendingCleanup.size)
|
|
223
|
+
throw planError('The previous browser startup has unresolved cleanup. No replacement was started.');
|
|
224
|
+
const root = rootPath(ctx, plan.root_alias);
|
|
225
|
+
const resolved = resolvePlaywright(root);
|
|
226
|
+
if (!resolved)
|
|
227
|
+
throw nativeError('driver_unavailable', 'Playwright is not installed in the approved project root, so no browser was started. Firedrill does not install it.');
|
|
228
|
+
let browserType = null;
|
|
229
|
+
try {
|
|
230
|
+
browserType = browserTypeFrom(await withTimeout(() => import(__rewriteRelativeImportExtension(pathToFileURL(resolved).href)), bounds.flowSetupMs, ctx.signal));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
browserType = null;
|
|
234
|
+
}
|
|
235
|
+
if (!browserType)
|
|
236
|
+
throw nativeError('driver_unavailable', 'The project\'s Playwright package could not be loaded, so no browser was started.');
|
|
237
|
+
const flowPath = await withTimeout(() => insideRoot(root, plan.flow), bounds.flowSetupMs, ctx.signal);
|
|
238
|
+
const pinned = await withTimeout(() => readPinned(flowPath, ctx.adapterSha256), bounds.flowSetupMs, ctx.signal);
|
|
239
|
+
let flow = null;
|
|
240
|
+
try {
|
|
241
|
+
flow = flowFrom(await withTimeout(() => import(__rewriteRelativeImportExtension(`${pathToFileURL(flowPath).href}?sha256=${pinned.sha256}`)), bounds.flowSetupMs, ctx.signal));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
flow = null;
|
|
245
|
+
}
|
|
246
|
+
if (!flow)
|
|
247
|
+
throw planError('The approved browser flow could not be loaded or does not export login(page, identity) and send(page, step), so no browser was started.');
|
|
248
|
+
emit(ctx, 'state.observed', { type: 'adapter.verified', profile: this.profile, adapter_sha256: new OwnDigest(pinned.sha256) });
|
|
249
|
+
const handle = serviceHandle(ctx, plan.service_alias);
|
|
250
|
+
const base = loopbackBase(handle.url, plan.service_alias);
|
|
251
|
+
const start = loopbackTarget(base, plan.start_path, plan.service_alias);
|
|
252
|
+
let browser;
|
|
253
|
+
// Playwright's own signal handlers close the browser and then exit the process (130) on the first Ctrl-C,
|
|
254
|
+
// which would skip the companion's stop, signed close, local-run close and owned-group sweep. The companion
|
|
255
|
+
// owns signals and closes the browser itself (retainClose / dispose).
|
|
256
|
+
try {
|
|
257
|
+
browser = await acquire(() => browserType.launch({ headless: true, env: browserEnv(ctx.env), timeout: bounds.flowSetupMs,
|
|
258
|
+
handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false }), bounds.flowSetupMs, ctx.signal, this.pendingCleanup);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw nativeError('driver_unavailable', 'The local Playwright browser could not be started (its browser binary may not be installed in this project), so no browser drill ran.');
|
|
262
|
+
}
|
|
263
|
+
if (this.disposing || ctx.signal.aborted) {
|
|
264
|
+
await this.retainClose(browser);
|
|
265
|
+
throw nativeError('driver_unavailable', 'Browser startup was cancelled. No browser session was opened.');
|
|
266
|
+
}
|
|
267
|
+
// Screenshots only with the approved policy and a Playwright that can apply it; never an unmasked image.
|
|
268
|
+
// One record per drill says capture was requested and whether it can run (selector count, never selectors).
|
|
269
|
+
let masking = null;
|
|
270
|
+
if (plan.screenshots) {
|
|
271
|
+
const approved = maskingPolicy(plan.screenshots);
|
|
272
|
+
const version = playwrightVersion(root);
|
|
273
|
+
const available = version !== null
|
|
274
|
+
&& (version[0] > MASK_MIN_VERSION[0] || (version[0] === MASK_MIN_VERSION[0] && version[1] >= MASK_MIN_VERSION[1]));
|
|
275
|
+
const frames = plan.screenshots.frames ?? null;
|
|
276
|
+
if (available)
|
|
277
|
+
masking = { selectors: approved.policy.selectors, sha256: approved.sha256, frames };
|
|
278
|
+
emit(ctx, 'state.observed', { type: 'capture.requested', schema: CAPTURE_SCHEMA, capture: 'screenshot',
|
|
279
|
+
masking_policy_sha256: new OwnDigest(approved.sha256), selector_count: approved.policy.selectors.length,
|
|
280
|
+
max_per_session: bounds.maxScreenshotsPerSession, available,
|
|
281
|
+
// Masked frames while each step runs (null when the plan takes none). Frames count toward max_per_session.
|
|
282
|
+
frame_interval_ms: frames?.interval_ms ?? null, max_frames_per_step: frames?.max_per_step ?? null,
|
|
283
|
+
// Playwright older than 1.35, or its version could not be read.
|
|
284
|
+
reason: available ? null : 'driver_unsupported' });
|
|
285
|
+
}
|
|
286
|
+
this.prepared = { browser, flow, base, start, flowSha256: pinned.sha256, masking, trace: plan.trace === 'sanitized-actions' };
|
|
287
|
+
}
|
|
288
|
+
async openSession(ctx, sessionKey) {
|
|
289
|
+
ctx = { ...ctx, signal: AbortSignal.any([ctx.signal, this.lifecycle.signal]) };
|
|
290
|
+
const prepared = this.prepared;
|
|
291
|
+
if (!prepared || this.disposing)
|
|
292
|
+
throw new CliError(DRIVER_ERRORS.session_failed, 'The browser driver was not prepared, so no session was opened.');
|
|
293
|
+
if (!sessionKey)
|
|
294
|
+
throw new CliError(DRIVER_ERRORS.session_failed, 'The session key is empty, so no session was opened.');
|
|
295
|
+
const bounds = driverBounds(ctx);
|
|
296
|
+
let context = null;
|
|
297
|
+
let trace = null;
|
|
298
|
+
const accessor = playwrightLogin(ctx.identity);
|
|
299
|
+
try {
|
|
300
|
+
context = await acquire(() => prepared.browser.newContext({ baseURL: prepared.base.origin, acceptDownloads: false }), bounds.flowSetupMs, ctx.signal, this.pendingCleanup);
|
|
301
|
+
const page = await withTimeout(() => context.newPage(), bounds.flowSetupMs, ctx.signal);
|
|
302
|
+
await withTimeout(() => page.goto(prepared.start.href, { timeout: bounds.flowSetupMs, waitUntil: 'load' }), bounds.flowSetupMs, ctx.signal);
|
|
303
|
+
// The approved trace records the flow's own calls, from login on; the driver's navigation above is not one.
|
|
304
|
+
// One capture per browser session.
|
|
305
|
+
trace = prepared.trace ? new ActionTrace(clockOf(ctx), (segment) => { emitTrace(ctx, segment); return true; }) : null;
|
|
306
|
+
const flowPage = trace ? trace.view(page) : page;
|
|
307
|
+
await withTimeout(() => prepared.flow.login(flowPage, accessor), bounds.flowSetupMs, ctx.signal);
|
|
308
|
+
if (this.disposing || ctx.signal.aborted)
|
|
309
|
+
throw new FlowAborted();
|
|
310
|
+
trace?.cut();
|
|
311
|
+
emit(ctx, 'state.observed', {
|
|
312
|
+
type: 'browser.context.fresh', scope: 'browser-context', session_key: sessionKey, backend_reset: false,
|
|
313
|
+
login: accessor ? 'synthetic-credentials' : 'none',
|
|
314
|
+
});
|
|
315
|
+
const session = new PlaywrightSession(ctx, prepared.flow, context, page, flowPage, trace, sessionKey, prepared.masking, () => this.sessions.delete(session));
|
|
316
|
+
this.sessions.add(session);
|
|
317
|
+
emit(ctx, 'action.observed', { type: 'session.opened', profile: this.profile, session_key: sessionKey });
|
|
318
|
+
return session;
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
trace?.end(ctx.signal.aborted ? 'cancelled' : 'driver_failed');
|
|
322
|
+
if (context)
|
|
323
|
+
await this.retainClose(context);
|
|
324
|
+
emit(ctx, 'driver.failed', { type: 'browser.session_failed', session_key: sessionKey,
|
|
325
|
+
cause: error instanceof FlowTimeout ? 'timeout' : 'error' });
|
|
326
|
+
throw new CliError(DRIVER_ERRORS.session_failed, 'The browser session could not be opened (start page or flow login did not finish), so no step was sent.');
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
accessor?.dispose();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** SHA-256 of the HTML the approved start path served, loaded in a throwaway browser context (closed after). */
|
|
333
|
+
async readinessProbe(ctx) {
|
|
334
|
+
ctx = { ...ctx, signal: AbortSignal.any([ctx.signal, this.lifecycle.signal]) };
|
|
335
|
+
const prepared = this.prepared;
|
|
336
|
+
if (!prepared || this.disposing)
|
|
337
|
+
throw unattested('The browser driver was not prepared, so the start document cannot be attested. Nothing was claimed.');
|
|
338
|
+
const bounds = driverBounds(ctx);
|
|
339
|
+
let context = null;
|
|
340
|
+
try {
|
|
341
|
+
context = await acquire(() => prepared.browser.newContext({ baseURL: prepared.base.origin, acceptDownloads: false }), bounds.flowSetupMs, ctx.signal, this.pendingCleanup);
|
|
342
|
+
const page = await withTimeout(() => context.newPage(), bounds.flowSetupMs, ctx.signal);
|
|
343
|
+
await withTimeout(() => page.goto(prepared.start.href, { timeout: bounds.flowSetupMs, waitUntil: 'load' }), bounds.flowSetupMs, ctx.signal);
|
|
344
|
+
const html = await withTimeout(() => page.content(), bounds.flowSetupMs, ctx.signal);
|
|
345
|
+
if (typeof html !== 'string')
|
|
346
|
+
throw new Error('no document');
|
|
347
|
+
return { kind: 'browser_document', response_sha256: sha256Hex(Buffer.from(html, 'utf8')) };
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
throw unattested('The approved start page did not load in the local browser, so it cannot be attested. Nothing was claimed.');
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
if (context)
|
|
354
|
+
await this.retainClose(context);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async retainClose(handle) {
|
|
358
|
+
const close = Promise.resolve().then(() => handle.close());
|
|
359
|
+
this.pendingCleanup.add(close);
|
|
360
|
+
void close.then(() => this.pendingCleanup.delete(close), () => undefined);
|
|
361
|
+
await withTimeout(() => close, this.cleanupMs, new AbortController().signal).catch(() => undefined);
|
|
362
|
+
}
|
|
363
|
+
async dispose() {
|
|
364
|
+
this.disposing = true;
|
|
365
|
+
this.lifecycle.abort();
|
|
366
|
+
const prepared = this.prepared;
|
|
367
|
+
this.prepared = null;
|
|
368
|
+
// Close the browser concurrently: a stuck context must not prevent browser cleanup.
|
|
369
|
+
const work = [...this.sessions].map(session => session.close());
|
|
370
|
+
if (prepared)
|
|
371
|
+
work.push(this.retainClose(prepared.browser));
|
|
372
|
+
const results = await Promise.allSettled(work);
|
|
373
|
+
if (this.pendingCleanup.size)
|
|
374
|
+
await withTimeout(() => Promise.all([...this.pendingCleanup]), this.cleanupMs, new AbortController().signal);
|
|
375
|
+
if (results.some(result => result.status === 'rejected'))
|
|
376
|
+
throw new CliError(DRIVER_ERRORS.session_failed, 'Browser cleanup is not confirmed. Keep the original local execution for recovery.');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
class PlaywrightSession {
|
|
380
|
+
sessionKey;
|
|
381
|
+
ctx;
|
|
382
|
+
flow;
|
|
383
|
+
context;
|
|
384
|
+
/** The real page: the driver's own captures use it. */
|
|
385
|
+
page;
|
|
386
|
+
/** What the flow gets: the page itself, or its recording view when the approved trace is on. */
|
|
387
|
+
flowPage;
|
|
388
|
+
trace;
|
|
389
|
+
onClose;
|
|
390
|
+
bounds;
|
|
391
|
+
/** The approved masking policy, or null when the plan captures no screenshots. */
|
|
392
|
+
masking;
|
|
393
|
+
/** Captures taken in this session, screenshots and frames together. */
|
|
394
|
+
screenshots = 0;
|
|
395
|
+
closed = false;
|
|
396
|
+
busy = false;
|
|
397
|
+
closing = null;
|
|
398
|
+
constructor(ctx, flow, context, page, flowPage, trace, sessionKey, masking, onClose) {
|
|
399
|
+
this.ctx = ctx;
|
|
400
|
+
this.flow = flow;
|
|
401
|
+
this.context = context;
|
|
402
|
+
this.page = page;
|
|
403
|
+
this.sessionKey = sessionKey;
|
|
404
|
+
this.onClose = onClose;
|
|
405
|
+
this.flowPage = flowPage;
|
|
406
|
+
this.trace = trace;
|
|
407
|
+
this.bounds = driverBounds(ctx);
|
|
408
|
+
this.masking = masking;
|
|
409
|
+
}
|
|
410
|
+
none(masking, unavailable) {
|
|
411
|
+
return { unavailable, masking_policy_sha256: masking.sha256, session_key: this.sessionKey };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The step's approved capture: the masked image, or why there is none (a
|
|
415
|
+
* reason only, never the error or page content); null when the plan captures
|
|
416
|
+
* no screenshots. Best-effort evidence only: skipped once the step is
|
|
417
|
+
* aborted, and raced against the abort with a short bound so a slow capture
|
|
418
|
+
* never turns a completed step into an interrupted one or leaves the session busy.
|
|
419
|
+
*/
|
|
420
|
+
async capture(signal) {
|
|
421
|
+
return this.masking ? this.shoot(this.masking, signal, 'disabled') : null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* One masked capture: the step's screenshot after send() returned (animations
|
|
425
|
+
* settled, caret hidden), or a frame while it runs (`allow`: animations and
|
|
426
|
+
* the caret are left as they are, so the flow's page is not changed).
|
|
427
|
+
*/
|
|
428
|
+
async shoot(masking, signal, motion) {
|
|
429
|
+
const none = (unavailable) => this.none(masking, unavailable);
|
|
430
|
+
if (this.closed)
|
|
431
|
+
return none('page_closed');
|
|
432
|
+
if (this.screenshots >= this.bounds.maxScreenshotsPerSession)
|
|
433
|
+
return none('session_limit');
|
|
434
|
+
if (signal.aborted)
|
|
435
|
+
return none('capture_failed');
|
|
436
|
+
try {
|
|
437
|
+
if (this.page.isClosed())
|
|
438
|
+
return none('page_closed');
|
|
439
|
+
this.screenshots += 1;
|
|
440
|
+
// Playwright paints the box of every element the selectors match before capture. A selector that
|
|
441
|
+
// matches nothing masks nothing (absent form fields are normal); an error while capturing or
|
|
442
|
+
// masking fails closed, with no image. Masking hides matching elements, not every sensitive value.
|
|
443
|
+
const mask = masking.selectors.map((selector) => this.page.locator(selector));
|
|
444
|
+
const bytes = await raceAbort(Promise.resolve(this.page.screenshot({ type: 'png', fullPage: false, scale: 'css', timeout: 2_000,
|
|
445
|
+
animations: motion, caret: motion === 'disabled' ? 'hide' : 'initial', mask, maskColor: MASK_COLOR })), AbortSignal.any([signal, AbortSignal.timeout(2_000)]));
|
|
446
|
+
if (!(bytes instanceof Uint8Array))
|
|
447
|
+
return none('capture_failed');
|
|
448
|
+
if (bytes.byteLength > this.bounds.maxAttachmentBytes * 8)
|
|
449
|
+
return none('image_too_large');
|
|
450
|
+
const png = metadataFreePng(bytes);
|
|
451
|
+
return png ? { png, masking_policy_sha256: masking.sha256, session_key: this.sessionKey } : none('image_unsupported');
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return none('capture_failed');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Masked frames while the flow's send() runs (approved `screenshots.frames`):
|
|
459
|
+
* one per interval, at most `max_per_step` per step, each masked like the
|
|
460
|
+
* step's screenshot and counted toward the session's captures. A frame that
|
|
461
|
+
* produced no image is counted with its reason, and so is every interval past
|
|
462
|
+
* the step's count (`step_limit`), so the step says how much of it the frames
|
|
463
|
+
* cover. `stop()` lets a frame being taken finish (bounded like any capture)
|
|
464
|
+
* and hands the frames over once.
|
|
465
|
+
*/
|
|
466
|
+
frames(signal) {
|
|
467
|
+
const masking = this.masking;
|
|
468
|
+
const plan = masking?.frames;
|
|
469
|
+
if (!masking || !plan)
|
|
470
|
+
return null;
|
|
471
|
+
const shots = [];
|
|
472
|
+
const reasons = new Set();
|
|
473
|
+
let omitted = 0, bytes = 0, handed = false;
|
|
474
|
+
const skip = (reason) => { omitted += 1; reasons.add(reason); };
|
|
475
|
+
const stopped = new AbortController();
|
|
476
|
+
const ticks = AbortSignal.any([signal, stopped.signal]);
|
|
477
|
+
const now = clockOf(this.ctx);
|
|
478
|
+
const running = (async () => {
|
|
479
|
+
while (await pause(plan.interval_ms, ticks)) {
|
|
480
|
+
if (shots.length >= plan.max_per_step) {
|
|
481
|
+
skip('step_limit');
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const at = now();
|
|
485
|
+
const frame = await this.shoot(masking, signal, 'allow');
|
|
486
|
+
if (!('png' in frame)) {
|
|
487
|
+
skip(frame.unavailable);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (bytes + frame.png.byteLength > FRAME_STEP_MAX_BYTES) {
|
|
491
|
+
skip('evidence_capacity');
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
bytes += frame.png.byteLength;
|
|
495
|
+
shots.push({ png: frame.png, masking_policy_sha256: frame.masking_policy_sha256, session_key: frame.session_key, at_ms: at });
|
|
496
|
+
}
|
|
497
|
+
})().catch(() => undefined);
|
|
498
|
+
return {
|
|
499
|
+
stop: async () => {
|
|
500
|
+
stopped.abort();
|
|
501
|
+
await running;
|
|
502
|
+
if (handed)
|
|
503
|
+
return undefined;
|
|
504
|
+
handed = true;
|
|
505
|
+
return { shots, omitted, reasons: FRAME_OMISSION_REASONS.filter((reason) => reasons.has(reason)) };
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async invoke(step, signal) {
|
|
510
|
+
const recorder = new StepRecorder(this.ctx, step);
|
|
511
|
+
if (this.closed)
|
|
512
|
+
return recorder.finish('failed', null, DRIVER_ERRORS.session_closed);
|
|
513
|
+
if (this.busy)
|
|
514
|
+
return recorder.finish('failed', null, DRIVER_ERRORS.step_unsupported);
|
|
515
|
+
const refused = precheck(step, this.sessionKey, ['message', 'task']);
|
|
516
|
+
if (refused)
|
|
517
|
+
return recorder.finish('failed', null, refused);
|
|
518
|
+
const input = stepInput(step);
|
|
519
|
+
const view = Object.freeze({
|
|
520
|
+
step_id: step.step_id, sequence: step.sequence, kind: input.kind, session_key: this.sessionKey,
|
|
521
|
+
text: input.kind === 'message' ? input.input : null,
|
|
522
|
+
input: input.kind === 'task' ? input.input : null,
|
|
523
|
+
});
|
|
524
|
+
recorder.record('action.observed', { type: 'target.exchange', direction: 'to_target', kind: input.kind, session_key: this.sessionKey, content: input.input });
|
|
525
|
+
const abort = stepAbort(this.ctx, signal, stepTimeoutMs(step, this.bounds));
|
|
526
|
+
if (abort.signal.aborted) {
|
|
527
|
+
abort.dispose();
|
|
528
|
+
return recorder.interrupted(abort.cause() ?? 'cancelled', false);
|
|
529
|
+
}
|
|
530
|
+
this.busy = true;
|
|
531
|
+
// Trace segments cut while this step runs are its own records, queued after its capture so frames keep their times.
|
|
532
|
+
this.trace?.attach((segment) => recorder.recordTrace(segment), abort.signal);
|
|
533
|
+
const frames = this.frames(abort.signal);
|
|
534
|
+
try {
|
|
535
|
+
const output = await raceAbort(Promise.resolve().then(() => this.flow.send(this.flowPage, view)), abort.signal);
|
|
536
|
+
// Frames end before the step's screenshot. The completed result is kept whether or not either produced an image.
|
|
537
|
+
const shots = await frames?.stop();
|
|
538
|
+
const capture = withFrames(await this.capture(abort.signal), shots);
|
|
539
|
+
recorder.record('action.observed', { type: 'target.exchange', direction: 'from_target', content: output }, undefined, capture);
|
|
540
|
+
this.trace?.cut();
|
|
541
|
+
return recorder.finish('completed', output, null);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
const shots = await frames?.stop();
|
|
545
|
+
if (error instanceof FlowAborted) {
|
|
546
|
+
// send() may still be acting on the page: no screenshot is taken and this context is retired. Frames
|
|
547
|
+
// taken before the stop are kept, recorded ahead of the cleanup so they keep their times.
|
|
548
|
+
const cause = abort.cause() ?? 'cancelled';
|
|
549
|
+
recorder.noteInterrupted(cause, true, this.masking ? withFrames(this.none(this.masking, 'capture_failed'), shots) : null);
|
|
550
|
+
recorder.record('state.observed', { type: 'browser.context.closing', scope: 'browser-context', backend_reset: false, reason: 'step interrupted' });
|
|
551
|
+
this.trace?.end('cancelled');
|
|
552
|
+
await this.close();
|
|
553
|
+
recorder.record('state.observed', { type: 'browser.context.closed', scope: 'browser-context', backend_reset: false, reason: 'step interrupted' });
|
|
554
|
+
return recorder.interrupted(cause, true);
|
|
555
|
+
}
|
|
556
|
+
const capture = withFrames(await this.capture(abort.signal), shots);
|
|
557
|
+
recorder.record('action.observed', { type: 'flow.error', error: errorSummary(error) }, undefined, capture);
|
|
558
|
+
this.trace?.cut();
|
|
559
|
+
return recorder.interrupted('thrown', true);
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
this.trace?.detach();
|
|
563
|
+
this.busy = false;
|
|
564
|
+
abort.dispose();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** Run the flow's optional reset(page). Client-side only; never a backend reset. */
|
|
568
|
+
async resetLocal() {
|
|
569
|
+
const result = (applied, error_code) => {
|
|
570
|
+
emit(this.ctx, 'state.observed', { type: 'browser.context.reset', scope: 'browser-context', session_key: this.sessionKey,
|
|
571
|
+
applied, backend_reset: false, error_code });
|
|
572
|
+
return { scope: 'browser-context', applied, backend_reset: false, error_code };
|
|
573
|
+
};
|
|
574
|
+
if (this.closed)
|
|
575
|
+
return result(false, DRIVER_ERRORS.session_closed);
|
|
576
|
+
const reset = this.flow.reset;
|
|
577
|
+
if (!reset)
|
|
578
|
+
return result(false, NATIVE_ERRORS.state_unsupported);
|
|
579
|
+
try {
|
|
580
|
+
await withTimeout(() => reset(this.flowPage), this.bounds.flowSetupMs, this.ctx.signal);
|
|
581
|
+
return result(true, null);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return result(false, DRIVER_ERRORS.session_failed);
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
this.trace?.cut();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async close() {
|
|
591
|
+
if (this.closing)
|
|
592
|
+
return withTimeout(() => this.closing, this.bounds.flowSetupMs, new AbortController().signal);
|
|
593
|
+
this.closed = true;
|
|
594
|
+
// The capture ends with the session, before the context closes, so calls the close ends are not recorded as failures.
|
|
595
|
+
this.trace?.end();
|
|
596
|
+
this.closing = Promise.resolve().then(() => this.context.close());
|
|
597
|
+
void this.closing.then(() => this.onClose(), () => undefined);
|
|
598
|
+
await withTimeout(() => this.closing, this.bounds.flowSetupMs, new AbortController().signal);
|
|
599
|
+
}
|
|
600
|
+
}
|