firedrill-run 2.2.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +465 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1622 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +61 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata-free PNG for root's `client-masked-png-v1` screenshot profile
|
|
3
|
+
* (Plan20 W12, root `native_image_evidence.py`, 2026-11-10).
|
|
4
|
+
*
|
|
5
|
+
* Root accepts only a bounded 8-bit RGB or RGBA, non-interlaced PNG made of
|
|
6
|
+
* IHDR, contiguous IDAT and IEND, with nothing after IEND: no text, EXIF, color
|
|
7
|
+
* profile, physical size, animation or private chunk. A browser's PNG carries
|
|
8
|
+
* some of those, so the capture is re-encoded here: its scanlines are inflated,
|
|
9
|
+
* checked against the declared dimensions and filter types, and deflated again
|
|
10
|
+
* into one IDAT. Pixels are not changed, so this removes metadata only; it is
|
|
11
|
+
* not redaction. Masking happens before capture (drivers/playwright.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { deflateSync, inflateSync } from 'node:zlib';
|
|
14
|
+
/** Root's image bounds. */
|
|
15
|
+
export const PNG_MAX_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
export const PNG_MAX_EDGE = 4096;
|
|
17
|
+
export const PNG_MAX_PIXELS = 8_000_000;
|
|
18
|
+
const PNG_MIN_BYTES = 45;
|
|
19
|
+
const MAX_INPUT_BYTES = 64 * 1024 * 1024;
|
|
20
|
+
const MAX_CHUNKS = 4096;
|
|
21
|
+
const SIGNATURE = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
22
|
+
const CRC_TABLE = (() => {
|
|
23
|
+
const table = new Uint32Array(256);
|
|
24
|
+
for (let n = 0; n < 256; n++) {
|
|
25
|
+
let c = n;
|
|
26
|
+
for (let k = 0; k < 8; k++)
|
|
27
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
28
|
+
table[n] = c >>> 0;
|
|
29
|
+
}
|
|
30
|
+
return table;
|
|
31
|
+
})();
|
|
32
|
+
function crc32(parts) {
|
|
33
|
+
let c = 0xffffffff;
|
|
34
|
+
for (const part of parts)
|
|
35
|
+
for (let i = 0; i < part.length; i++)
|
|
36
|
+
c = CRC_TABLE[(c ^ part[i]) & 0xff] ^ (c >>> 8);
|
|
37
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
function chunk(type, data) {
|
|
40
|
+
const kind = Buffer.from(type, 'latin1');
|
|
41
|
+
const out = Buffer.alloc(12 + data.length);
|
|
42
|
+
out.writeUInt32BE(data.length, 0);
|
|
43
|
+
kind.copy(out, 4);
|
|
44
|
+
Buffer.from(data.buffer, data.byteOffset, data.byteLength).copy(out, 8);
|
|
45
|
+
out.writeUInt32BE(crc32([kind, data]), 8 + data.length);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The same image as a metadata-free PNG within root's bounds, or null when the
|
|
50
|
+
* input is not a well-formed 8-bit RGB/RGBA non-interlaced PNG, is too large
|
|
51
|
+
* once re-encoded, or exceeds the dimension limits. Never throws.
|
|
52
|
+
*/
|
|
53
|
+
export function metadataFreePng(input) {
|
|
54
|
+
try {
|
|
55
|
+
if (!(input instanceof Uint8Array) || input.byteLength < PNG_MIN_BYTES || input.byteLength > MAX_INPUT_BYTES)
|
|
56
|
+
return null;
|
|
57
|
+
const body = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
58
|
+
if (!body.subarray(0, 8).equals(SIGNATURE))
|
|
59
|
+
return null;
|
|
60
|
+
let offset = 8, chunks = 0, header = null, ended = false, dataEnded = false;
|
|
61
|
+
let width = 0, height = 0, channels = 0;
|
|
62
|
+
const compressed = [];
|
|
63
|
+
while (offset < body.length && !ended) {
|
|
64
|
+
if (++chunks > MAX_CHUNKS || offset + 12 > body.length)
|
|
65
|
+
return null;
|
|
66
|
+
const size = body.readUInt32BE(offset);
|
|
67
|
+
const end = offset + 12 + size;
|
|
68
|
+
if (size > 0x7fffffff || end > body.length)
|
|
69
|
+
return null;
|
|
70
|
+
const kind = body.subarray(offset + 4, offset + 8);
|
|
71
|
+
const data = body.subarray(offset + 8, end - 4);
|
|
72
|
+
if (crc32([kind, data]) !== body.readUInt32BE(end - 4))
|
|
73
|
+
return null;
|
|
74
|
+
const name = kind.toString('latin1');
|
|
75
|
+
if (!/^[A-Za-z]{4}$/.test(name))
|
|
76
|
+
return null;
|
|
77
|
+
// Transparency and animation affect displayed pixels, not just metadata.
|
|
78
|
+
// Dropping them could expose RGB values that were previously invisible.
|
|
79
|
+
if (['tRNS', 'acTL', 'fcTL', 'fdAT'].includes(name))
|
|
80
|
+
return null;
|
|
81
|
+
if (compressed.length && name !== 'IDAT')
|
|
82
|
+
dataEnded = true;
|
|
83
|
+
if (chunks === 1) {
|
|
84
|
+
if (name !== 'IHDR' || size !== 13)
|
|
85
|
+
return null;
|
|
86
|
+
width = data.readUInt32BE(0);
|
|
87
|
+
height = data.readUInt32BE(4);
|
|
88
|
+
const [depth, color, compression, filter, interlace] = [data[8], data[9], data[10], data[11], data[12]];
|
|
89
|
+
if (width < 1 || height < 1 || width > PNG_MAX_EDGE || height > PNG_MAX_EDGE || width * height > PNG_MAX_PIXELS
|
|
90
|
+
|| depth !== 8 || (color !== 2 && color !== 6) || compression !== 0 || filter !== 0 || interlace !== 0)
|
|
91
|
+
return null;
|
|
92
|
+
channels = color === 2 ? 3 : 4;
|
|
93
|
+
header = Buffer.from(data);
|
|
94
|
+
}
|
|
95
|
+
else if (name === 'IDAT') {
|
|
96
|
+
if (dataEnded)
|
|
97
|
+
return null;
|
|
98
|
+
compressed.push(data);
|
|
99
|
+
}
|
|
100
|
+
else if (name === 'IEND') {
|
|
101
|
+
if (size !== 0)
|
|
102
|
+
return null;
|
|
103
|
+
ended = true;
|
|
104
|
+
}
|
|
105
|
+
else if (name === 'PLTE') {
|
|
106
|
+
// A suggested palette for a truecolor image: optional, dropped.
|
|
107
|
+
}
|
|
108
|
+
else if (name.charCodeAt(0) < 0x61) {
|
|
109
|
+
// Any other critical chunk changes how the image decodes: refuse rather than guess.
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Ancillary chunks (text, EXIF, color profile, physical size, time, private) are dropped.
|
|
113
|
+
offset = end;
|
|
114
|
+
}
|
|
115
|
+
if (!ended || offset !== body.length || !header || compressed.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
const expected = height * (1 + width * channels);
|
|
118
|
+
const pixels = inflateSync(Buffer.concat(compressed), { maxOutputLength: expected + 1 });
|
|
119
|
+
if (pixels.length !== expected)
|
|
120
|
+
return null;
|
|
121
|
+
const stride = 1 + width * channels;
|
|
122
|
+
for (let row = 0; row < height; row++)
|
|
123
|
+
if (pixels[row * stride] > 4)
|
|
124
|
+
return null;
|
|
125
|
+
const out = Buffer.concat([Buffer.from(SIGNATURE), chunk('IHDR', header), chunk('IDAT', deflateSync(pixels, { level: 9 })),
|
|
126
|
+
chunk('IEND', new Uint8Array(0))]);
|
|
127
|
+
return out.length >= PNG_MIN_BYTES && out.length <= PNG_MAX_BYTES ? new Uint8Array(out) : null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Whether bytes already have root's metadata-free shape (signature, IHDR first, one IDAT run, IEND last). */
|
|
134
|
+
export function isMetadataFreePng(bytes) {
|
|
135
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < PNG_MIN_BYTES || bytes.byteLength > PNG_MAX_BYTES)
|
|
136
|
+
return false;
|
|
137
|
+
const body = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
138
|
+
if (!body.subarray(0, 8).equals(SIGNATURE))
|
|
139
|
+
return false;
|
|
140
|
+
let offset = 8, index = 0, seenData = false, dataEnded = false;
|
|
141
|
+
while (offset < body.length) {
|
|
142
|
+
if (offset + 12 > body.length)
|
|
143
|
+
return false;
|
|
144
|
+
const size = body.readUInt32BE(offset);
|
|
145
|
+
const end = offset + 12 + size;
|
|
146
|
+
if (end > body.length)
|
|
147
|
+
return false;
|
|
148
|
+
const name = body.subarray(offset + 4, offset + 8).toString('latin1');
|
|
149
|
+
if (index === 0 ? name !== 'IHDR' || size !== 13 : name === 'IDAT' ? dataEnded : name !== 'IEND' || size !== 0 || !seenData || end !== body.length)
|
|
150
|
+
return false;
|
|
151
|
+
if (name === 'IDAT')
|
|
152
|
+
seenData = true;
|
|
153
|
+
else if (index > 0)
|
|
154
|
+
dataEnded = true;
|
|
155
|
+
offset = end;
|
|
156
|
+
index++;
|
|
157
|
+
}
|
|
158
|
+
return dataEnded;
|
|
159
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback port allocation (Plan20 W09).
|
|
3
|
+
*
|
|
4
|
+
* The operating system picks a free port on 127.0.0.1; the listener is closed
|
|
5
|
+
* before the port is returned, so another local process could take it before the
|
|
6
|
+
* service binds. Readiness checks surface that case as a readiness failure.
|
|
7
|
+
*/
|
|
8
|
+
import { createServer } from 'node:net';
|
|
9
|
+
import { CliError } from '../http.js';
|
|
10
|
+
const unavailable = () => new CliError('native_port_unavailable', 'A free loopback port could not be reserved. No service was started on it.');
|
|
11
|
+
/** Bind 127.0.0.1:0, read the assigned port, release it and return the port number. */
|
|
12
|
+
export async function allocateLoopbackPort(signal) {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
const server = createServer(socket => socket.destroy());
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let settled = false;
|
|
17
|
+
const finish = (error, port = 0) => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
signal?.removeEventListener('abort', onAbort);
|
|
23
|
+
server.off('error', onError);
|
|
24
|
+
const done = () => error ? reject(error) : resolve(port);
|
|
25
|
+
if (server.listening)
|
|
26
|
+
server.close(() => done());
|
|
27
|
+
else
|
|
28
|
+
done();
|
|
29
|
+
};
|
|
30
|
+
const onError = () => finish(unavailable());
|
|
31
|
+
const onAbort = () => finish(new CliError('canceled', 'Port allocation was canceled. No service was started.', 130));
|
|
32
|
+
const timer = setTimeout(onError, 5000);
|
|
33
|
+
server.once('error', onError);
|
|
34
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
35
|
+
server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
|
|
36
|
+
const address = server.address();
|
|
37
|
+
if (!address || typeof address !== 'object' || !Number.isSafeInteger(address.port) || address.port < 1)
|
|
38
|
+
finish(unavailable());
|
|
39
|
+
else
|
|
40
|
+
finish(null, address.port);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drill readiness evidence (root `execution-readiness.v1`, Plan20 FIR-492).
|
|
3
|
+
*
|
|
4
|
+
* Root admits a drill (`starting` -> `ready`) on its own, after the client
|
|
5
|
+
* commits one signed readiness record (role `driver.started`) and the single
|
|
6
|
+
* `driver.started` observation that references it. This module assembles what
|
|
7
|
+
* that record attests, from what this client actually observed:
|
|
8
|
+
* - every declared service exactly once, with its launch identity and
|
|
9
|
+
* ownership: launched for this drill (`owned`), already running
|
|
10
|
+
* (`attached`), or launched once for the campaign's local run (`owned_run`);
|
|
11
|
+
* - the entry service's probe in the kind root requires for the driver
|
|
12
|
+
* profile (the driver attests it), and for other services the 2xx status of
|
|
13
|
+
* their approved HTTP readiness check, or that they are running;
|
|
14
|
+
* - one committed `state.observed` record per local state domain, naming its
|
|
15
|
+
* exact declared reset.
|
|
16
|
+
*
|
|
17
|
+
* Probe digests (agreed with root on Slack 2026-11-10; signed device
|
|
18
|
+
* observations, not platform probes): jsonl_handshake hashes the exact stdout
|
|
19
|
+
* line that matched the approved readiness text, UTF-8, without its line
|
|
20
|
+
* terminator; function_import the exact pinned wrapper bytes (equal to the
|
|
21
|
+
* descriptor adapter digest), only once the wrapper started; browser_document the
|
|
22
|
+
* document serialization a throwaway context returned after navigating to the
|
|
23
|
+
* approved start path; signed_event_handshake the exact body bytes of the approved
|
|
24
|
+
* HTTP readiness response. Raw content is never uploaded. Nothing unobserved is
|
|
25
|
+
* upgraded into success: a local domain without a reset, or a service that is
|
|
26
|
+
* not running, refuses readiness instead. Observation-only domains are not reset
|
|
27
|
+
* and are not listed.
|
|
28
|
+
*/
|
|
29
|
+
import { CliError } from '../http.js';
|
|
30
|
+
import { READINESS_MAX_RESETS, READINESS_MAX_SERVICES, nativeError } from './contracts.js';
|
|
31
|
+
export const READINESS_ERRORS = {
|
|
32
|
+
/** A declared service is not running locally when the drill must become ready. */
|
|
33
|
+
service_missing: 'native_readiness_service_missing',
|
|
34
|
+
/** A shared launched service has no local-run launch identity to report it under. */
|
|
35
|
+
run_service: 'native_readiness_run_service',
|
|
36
|
+
/** Too many services or local state domains for root's evidence inspection. */
|
|
37
|
+
limit: 'native_readiness_limit',
|
|
38
|
+
/** A local domain's reset evidence record was refused or dropped, so readiness cannot cite it. */
|
|
39
|
+
evidence_missing: 'native_readiness_evidence_missing',
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The local state domains root requires reset evidence for, checked before
|
|
43
|
+
* anything starts: each must have a reset this client performs. A local domain
|
|
44
|
+
* without one (declared `unavailable`, or a fresh session that does not reset
|
|
45
|
+
* its kind) can never be admitted, so the drill is refused up front.
|
|
46
|
+
*/
|
|
47
|
+
export function readinessDomains(descriptor, statePlan) {
|
|
48
|
+
if (descriptor.services.length > READINESS_MAX_SERVICES) {
|
|
49
|
+
throw new CliError(READINESS_ERRORS.limit, `The service admits at most ${READINESS_MAX_SERVICES} declared services per drill. No drill was started.`);
|
|
50
|
+
}
|
|
51
|
+
const actions = new Map(statePlan.actions.map((action) => [action.alias, action]));
|
|
52
|
+
const local = [];
|
|
53
|
+
for (const domain of descriptor.state_domains) {
|
|
54
|
+
if (domain.location !== 'local')
|
|
55
|
+
continue;
|
|
56
|
+
const action = actions.get(domain.alias);
|
|
57
|
+
if (!action || (action.action !== 'fresh-session' && action.action !== 'fresh-process' && action.action !== 'approved-hook')) {
|
|
58
|
+
throw nativeError('state_unsupported', `Local state domain "${domain.alias}" has no reset this client performs, and the service admits a drill only with reset evidence for every local domain. No drill was started.`);
|
|
59
|
+
}
|
|
60
|
+
local.push(action);
|
|
61
|
+
}
|
|
62
|
+
if (local.length > READINESS_MAX_RESETS) {
|
|
63
|
+
throw new CliError(READINESS_ERRORS.limit, `The service admits at most ${READINESS_MAX_RESETS} local state domains per drill. No drill was started.`);
|
|
64
|
+
}
|
|
65
|
+
return local;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `state.observed` content for one local domain's reset, once services and
|
|
69
|
+
* hooks ran. A hook or fresh process that did not apply throws (the drill never
|
|
70
|
+
* claims a reset it did not see); a fresh session is applied as each session
|
|
71
|
+
* opens, so it is recorded as scheduled for every driver session of this drill.
|
|
72
|
+
*/
|
|
73
|
+
export function resetEvidence(action, tracker, freshLaunch) {
|
|
74
|
+
const base = { type: 'state.reset', domain: action.alias, reset: action.reset, hook_sha256: action.descriptor_hook_sha256 };
|
|
75
|
+
if (action.action === 'approved-hook') {
|
|
76
|
+
const run = tracker.hookRuns.find((entry) => entry.domain_alias === action.alias);
|
|
77
|
+
if (!run || run.result !== 'applied')
|
|
78
|
+
throw new CliError('native_state_reset_failed', `The reset hook for "${action.alias}" did not apply. No drill step was sent.`);
|
|
79
|
+
return { ...base, result: 'applied', exit_code: run.exit_code, duration_ms: run.duration_ms };
|
|
80
|
+
}
|
|
81
|
+
if (action.action === 'fresh-process') {
|
|
82
|
+
const launchId = freshLaunch(action.service_alias);
|
|
83
|
+
const applied = tracker.coverage().some((entry) => entry.alias === action.alias && entry.result === 'applied');
|
|
84
|
+
if (!applied || !launchId) {
|
|
85
|
+
throw new CliError('native_state_reset_failed', `"${action.alias}" was not reset: its service did not start as a fresh copy for this drill.`);
|
|
86
|
+
}
|
|
87
|
+
return { ...base, result: 'applied', service: action.service_alias, launch_id: launchId };
|
|
88
|
+
}
|
|
89
|
+
return { ...base, result: 'scheduled', scope: 'each-driver-session' };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every declared service as it stands now, in descriptor order, for the
|
|
93
|
+
* readiness record. Throws when one is not running or cannot be reported
|
|
94
|
+
* truthfully (a shared launched service outside a registered local run).
|
|
95
|
+
*/
|
|
96
|
+
export function readinessServices(descriptor, plan, handles, driver, entryProbe, drillKey, runService) {
|
|
97
|
+
const driverOwned = new Map((driver.ownedProcesses?.() ?? []).map((item) => [item.alias, item]));
|
|
98
|
+
const services = [];
|
|
99
|
+
for (const { alias } of descriptor.services) {
|
|
100
|
+
const probe = (handle) => {
|
|
101
|
+
if (alias === descriptor.entry_service)
|
|
102
|
+
return entryProbe;
|
|
103
|
+
const ready = handle?.ready() ?? null;
|
|
104
|
+
return ready?.kind === 'http' && ready.status_code >= 200 && ready.status_code <= 299
|
|
105
|
+
? { kind: 'http', status_code: ready.status_code } : { kind: 'process', observation: 'running' };
|
|
106
|
+
};
|
|
107
|
+
const owned = driverOwned.get(alias);
|
|
108
|
+
if (owned) {
|
|
109
|
+
if (owned.exit !== null)
|
|
110
|
+
throw nativeError('process_exited', `Service ${alias} exited before the drill became ready.`);
|
|
111
|
+
services.push({ service: alias, launch_id: owned.launch_id, ownership: 'owned', run_id: null, probe: probe(null) });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const handle = handles.get(alias);
|
|
115
|
+
if (!handle || handle.ready() === null) {
|
|
116
|
+
const planned = plan.services.some((service) => service.alias === alias);
|
|
117
|
+
throw new CliError(READINESS_ERRORS.service_missing, planned
|
|
118
|
+
? `Service ${alias} is not running, so the drill cannot become ready.`
|
|
119
|
+
: `Declared service ${alias} is not in the approved plan, so the drill cannot become ready. Approve a plan that runs or attaches it.`);
|
|
120
|
+
}
|
|
121
|
+
if (handle.mode === 'attach') {
|
|
122
|
+
services.push({ service: alias, launch_id: handle.launchId, ownership: 'attached', run_id: null, probe: probe(handle) });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (handle.exit() !== null)
|
|
126
|
+
throw nativeError('process_exited', `Service ${alias} exited before the drill became ready.`);
|
|
127
|
+
if (handle.drillKey === drillKey) {
|
|
128
|
+
services.push({ service: alias, launch_id: handle.launchId, ownership: 'owned', run_id: null, probe: probe(handle) });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Launched once for the run and shared by its drills: reported only under the run's registered identity.
|
|
132
|
+
const run = runService(alias);
|
|
133
|
+
if (!run || run.launch_id !== handle.launchId) {
|
|
134
|
+
throw new CliError(READINESS_ERRORS.run_service, `Shared service ${alias} has no launch registered in this campaign's local run, so the drill cannot become ready.`);
|
|
135
|
+
}
|
|
136
|
+
services.push({ service: alias, launch_id: run.launch_id, ownership: 'owned_run', run_id: run.run_id, probe: probe(handle) });
|
|
137
|
+
}
|
|
138
|
+
return services;
|
|
139
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native reassessment (Plan20 R34; root `native_reassessment*.py`).
|
|
3
|
+
*
|
|
4
|
+
* A reassessment is a separately quoted and approved new evaluation of one
|
|
5
|
+
* finished drill's retained evidence:
|
|
6
|
+
* - it never invokes the target again and never charges local compute;
|
|
7
|
+
* - it never merges into the drill's original result or its quality cohorts;
|
|
8
|
+
* each reassessment is its own attempt with its own criterion results.
|
|
9
|
+
*
|
|
10
|
+
* Root reviews it under the caller's request key and freezes a quote at once.
|
|
11
|
+
* Nothing is reserved or evaluated until `admit` names that exact quote with the
|
|
12
|
+
* human's consent. A reviewed reassessment can be cancelled; an admitted one
|
|
13
|
+
* cannot. Writes and the request-key read belong to the member who reviewed it.
|
|
14
|
+
*/
|
|
15
|
+
import { api } from '../auth.js';
|
|
16
|
+
import { creditApprovalPolicy, withoutCreditPolicyKeys } from './credit-approval.js';
|
|
17
|
+
import { CliError } from '../http.js';
|
|
18
|
+
import { criterionRow } from './cloud-resources.js';
|
|
19
|
+
export const REASSESSMENT_STATES = ['reviewed', 'evaluating', 'completed', 'cancelled'];
|
|
20
|
+
const PAGE = 25;
|
|
21
|
+
const SCHEMA = 'firedrill.native-reassessment.v1';
|
|
22
|
+
/** Root's own bound on one reassessment's criteria. */
|
|
23
|
+
const MAX_CRITERIA = 256;
|
|
24
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
26
|
+
const RESPONSE_LIMIT = 4 * 1024 * 1024;
|
|
27
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
const uuid = (value) => typeof value === 'string' && UUID.test(value);
|
|
29
|
+
const count = (value, min = 0) => typeof value === 'number' && Number.isSafeInteger(value) && value >= min;
|
|
30
|
+
const time = (value) => typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
31
|
+
const text = (value, max) => typeof value === 'string' && value.length >= 1 && value.length <= max;
|
|
32
|
+
const keys = (value) => Object.keys(value).sort().join(',');
|
|
33
|
+
const lower = (value) => value.toLowerCase();
|
|
34
|
+
const unconfirmed = (what) => new CliError('native_response_unconfirmed', `The ${what} response could not be confirmed. Nothing further was sent for it.`);
|
|
35
|
+
const scoped = (scope, signal) => ({ actor: scope.owner, workspace: scope.workspace, signal, maximumResponseBytes: RESPONSE_LIMIT });
|
|
36
|
+
const base = (scope, assignmentId) => `/api/workspaces/${scope.workspace}/native/assignments/${assignmentId}/reassessments`;
|
|
37
|
+
/** A UUID argument exactly as Firedrill printed it, lowercased. */
|
|
38
|
+
export function reassessmentId(value, label) {
|
|
39
|
+
if (!uuid(value))
|
|
40
|
+
throw new CliError('arguments_invalid', `Use the ${label} exactly as Firedrill printed it. Nothing was sent.`, 2);
|
|
41
|
+
return lower(value);
|
|
42
|
+
}
|
|
43
|
+
const ROW_KEYS = 'assignment_id,basis_id,completed_at,created_at,criteria_count,id,profile_changed,profile_comparison,profile_digest,quote,'
|
|
44
|
+
+ 'request_key,source_basis_id,state';
|
|
45
|
+
const COMPARISONS = ['same', 'different', 'unknown'];
|
|
46
|
+
const QUOTE_KEYS = 'activity,credit_scale,denomination,estimated_quantities,estimated_units,execution_location,expires_at,id,maximum_units,organization_id,'
|
|
47
|
+
+ 'resource_class,scope,scope_digest,tariff_digest,tariff_version,workspace_id';
|
|
48
|
+
const PAGE_KEYS = 'count,next,page,page_size,previous,results';
|
|
49
|
+
/**
|
|
50
|
+
* One reassessment exactly as root defines it:
|
|
51
|
+
* - this drill's assignment, re-evaluating a basis other than its own;
|
|
52
|
+
* - no basis or completion time while reviewed; its own basis once admitted
|
|
53
|
+
* (evaluating), and a completion time as well once completed. A cancelled
|
|
54
|
+
* one has neither (cancelled before admission) or both (stopped by root
|
|
55
|
+
* after admission, before every criterion finished);
|
|
56
|
+
* - the evaluator comparison agrees with profile_changed;
|
|
57
|
+
* - a managed evaluation quote in this workspace, frozen to this reassessment,
|
|
58
|
+
* its source basis and its evaluator profile, that never charges local compute.
|
|
59
|
+
* The quote scope's identity fields are checked; its digests of the original are root's.
|
|
60
|
+
*/
|
|
61
|
+
function reassessmentRow(scope, assignmentId) {
|
|
62
|
+
return (value) => {
|
|
63
|
+
const quote = object(value) ? value.quote : null;
|
|
64
|
+
const frozen = object(quote) ? quote.scope : null;
|
|
65
|
+
const quantities = object(quote) ? quote.estimated_quantities : null;
|
|
66
|
+
const based = object(value) && value.basis_id !== null;
|
|
67
|
+
const done = object(value) && value.completed_at !== null;
|
|
68
|
+
const lifecycle = !object(value) ? false
|
|
69
|
+
: value.state === 'reviewed' ? !based && !done
|
|
70
|
+
: value.state === 'evaluating' ? based && !done
|
|
71
|
+
: value.state === 'completed' ? based && done
|
|
72
|
+
: value.state === 'cancelled' && based === done;
|
|
73
|
+
if (!object(value) || keys(value) !== ROW_KEYS || !uuid(value.id) || value.assignment_id !== assignmentId
|
|
74
|
+
|| !uuid(value.source_basis_id) || !(value.basis_id === null || uuid(value.basis_id)) || !lifecycle
|
|
75
|
+
|| (value.basis_id !== null && lower(value.basis_id) === lower(value.source_basis_id))
|
|
76
|
+
|| !uuid(value.request_key) || !time(value.created_at) || !(value.completed_at === null || time(value.completed_at))
|
|
77
|
+
|| typeof value.profile_changed !== 'boolean' || !COMPARISONS.includes(value.profile_comparison)
|
|
78
|
+
|| value.profile_changed !== (value.profile_comparison === 'different')
|
|
79
|
+
|| typeof value.profile_digest !== 'string' || !SHA256.test(value.profile_digest)
|
|
80
|
+
|| !count(value.criteria_count, 1) || value.criteria_count > MAX_CRITERIA
|
|
81
|
+
|| !object(quote) || withoutCreditPolicyKeys(quote) !== QUOTE_KEYS || !uuid(quote.id) || quote.workspace_id !== lower(scope.workspace)
|
|
82
|
+
|| !uuid(quote.organization_id) || quote.activity !== 'simulation' || quote.execution_location !== 'managed'
|
|
83
|
+
|| !text(quote.resource_class, 64) || !text(quote.tariff_version, 64) || !text(quote.denomination, 64)
|
|
84
|
+
|| typeof quote.scope_digest !== 'string' || !SHA256.test(quote.scope_digest)
|
|
85
|
+
|| typeof quote.tariff_digest !== 'string' || !SHA256.test(quote.tariff_digest) || !count(quote.credit_scale, 1)
|
|
86
|
+
|| !count(quote.estimated_units) || !count(quote.maximum_units) || quote.estimated_units > quote.maximum_units || !time(quote.expires_at)
|
|
87
|
+
|| !object(quantities) || keys(quantities) !== 'work_units' || !count(quantities.work_units, 1)
|
|
88
|
+
|| !object(frozen) || frozen.schema !== SCHEMA || frozen.component !== 'evaluation' || frozen.reassessment_id !== value.id
|
|
89
|
+
|| frozen.assignment_id !== assignmentId || frozen.source_basis_id !== value.source_basis_id
|
|
90
|
+
|| frozen.local_target_compute_charged !== false
|
|
91
|
+
|| frozen.evaluation_profile_digest !== value.profile_digest)
|
|
92
|
+
throw unconfirmed('reassessment');
|
|
93
|
+
return {
|
|
94
|
+
id: lower(value.id), assignment_id: assignmentId, source_basis_id: lower(value.source_basis_id),
|
|
95
|
+
basis_id: value.basis_id === null ? null : lower(value.basis_id), request_key: lower(value.request_key),
|
|
96
|
+
state: value.state, created_at: value.created_at, completed_at: value.completed_at,
|
|
97
|
+
profile_changed: value.profile_changed, profile_comparison: value.profile_comparison,
|
|
98
|
+
profile_digest: value.profile_digest, criteria_count: value.criteria_count,
|
|
99
|
+
quote: { ...creditApprovalPolicy(quote), id: lower(quote.id), denomination: quote.denomination, credit_scale: quote.credit_scale, estimated_units: quote.estimated_units,
|
|
100
|
+
maximum_units: quote.maximum_units, expires_at: quote.expires_at, resource_class: quote.resource_class, work_units: quantities.work_units },
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Root's exact page envelope for a 25-row page that was asked for. */
|
|
105
|
+
function pageOf(raw, page, row, what) {
|
|
106
|
+
if (!object(raw) || keys(raw) !== PAGE_KEYS || !count(raw.count) || raw.page !== page || raw.page_size !== PAGE
|
|
107
|
+
|| (raw.next !== null && typeof raw.next !== 'string') || (raw.previous !== null && typeof raw.previous !== 'string')
|
|
108
|
+
|| !Array.isArray(raw.results) || raw.results.length > PAGE || (raw.next !== null) !== (page * PAGE < raw.count))
|
|
109
|
+
throw unconfirmed(what);
|
|
110
|
+
return { count: raw.count, page, next: raw.next, previous: raw.previous, results: raw.results.map(row) };
|
|
111
|
+
}
|
|
112
|
+
function pageNumber(value) {
|
|
113
|
+
const page = value ?? 1;
|
|
114
|
+
if (!count(page, 1))
|
|
115
|
+
throw new CliError('arguments_invalid', 'Use --page with a positive page number.', 2);
|
|
116
|
+
return page;
|
|
117
|
+
}
|
|
118
|
+
/** A drill's reassessments, newest first (25 per page), the reviewing member's and others'. */
|
|
119
|
+
export async function listReassessments(scope, input) {
|
|
120
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
121
|
+
const page = pageNumber(input.page);
|
|
122
|
+
const raw = await api(`${base(scope, assignmentId)}?page=${page}&page_size=${PAGE}`, scoped(scope, input.signal));
|
|
123
|
+
return pageOf(raw, page, reassessmentRow(scope, assignmentId), 'reassessments');
|
|
124
|
+
}
|
|
125
|
+
/** One reassessment by id. */
|
|
126
|
+
export async function getReassessment(scope, input) {
|
|
127
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
128
|
+
const id = reassessmentId(input.reassessmentId, 'reassessment id');
|
|
129
|
+
const row = reassessmentRow(scope, assignmentId)(await api(`${base(scope, assignmentId)}/${id}`, scoped(scope, input.signal)));
|
|
130
|
+
if (row.id !== id)
|
|
131
|
+
throw unconfirmed('reassessment');
|
|
132
|
+
return row;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Review a new evaluation of the drill's retained evidence from `basisId` (its
|
|
136
|
+
* original evaluation basis), under the caller's request key. Root freezes a
|
|
137
|
+
* quote; nothing is reserved or evaluated. The same key returns the same
|
|
138
|
+
* original review, and a different basis under it is refused by root.
|
|
139
|
+
*/
|
|
140
|
+
export async function reviewReassessment(scope, input) {
|
|
141
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
142
|
+
const basisId = reassessmentId(input.basisId, 'evaluation basis id');
|
|
143
|
+
const requestKey = reassessmentId(input.requestKey, 'request key');
|
|
144
|
+
const row = reassessmentRow(scope, assignmentId)(await api(base(scope, assignmentId), {
|
|
145
|
+
...scoped(scope, input.signal), method: 'POST', body: { request_key: requestKey, basis_id: basisId }
|
|
146
|
+
}));
|
|
147
|
+
if (row.request_key !== requestKey || row.source_basis_id !== basisId)
|
|
148
|
+
throw unconfirmed('reassessment');
|
|
149
|
+
return row;
|
|
150
|
+
}
|
|
151
|
+
/** The caller's original review under this key, or null when root has none (which does not prove it will never arrive). */
|
|
152
|
+
export async function recoverReassessment(scope, input) {
|
|
153
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
154
|
+
const requestKey = reassessmentId(input.requestKey, 'request key');
|
|
155
|
+
try {
|
|
156
|
+
const row = reassessmentRow(scope, assignmentId)(await api(`${base(scope, assignmentId)}/requests/${requestKey}`, scoped(scope, input.signal)));
|
|
157
|
+
if (row.request_key !== requestKey)
|
|
158
|
+
throw unconfirmed('reassessment');
|
|
159
|
+
return row;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof CliError && error.httpStatus === 404)
|
|
163
|
+
return null;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Admit a reviewed reassessment with exactly its quote, after the human
|
|
169
|
+
* approved it: root reserves up to that quote and starts the new evaluation.
|
|
170
|
+
* An already admitted one is returned unchanged, so a lost answer is recovered,
|
|
171
|
+
* never charged twice.
|
|
172
|
+
*/
|
|
173
|
+
export async function admitReassessment(scope, input) {
|
|
174
|
+
const assignmentId = reassessmentId(input.reassessment.assignment_id, 'assignment id');
|
|
175
|
+
const id = reassessmentId(input.reassessment.id, 'reassessment id');
|
|
176
|
+
const row = reassessmentRow(scope, assignmentId)(await api(`${base(scope, assignmentId)}/${id}/admit`, {
|
|
177
|
+
...scoped(scope, input.signal), method: 'POST', body: { quote_id: input.reassessment.quote.id, reassess_consent: true, credit_approval: input.creditApproval === true }
|
|
178
|
+
}));
|
|
179
|
+
if (row.id !== id || row.quote.id !== input.reassessment.quote.id || (row.state !== 'evaluating' && row.state !== 'completed')) {
|
|
180
|
+
throw unconfirmed('reassessment');
|
|
181
|
+
}
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
184
|
+
/** Cancel a reviewed reassessment after the human's approval. Root refuses an admitted one; nothing is undone. */
|
|
185
|
+
export async function cancelReassessment(scope, input) {
|
|
186
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
187
|
+
const id = reassessmentId(input.reassessmentId, 'reassessment id');
|
|
188
|
+
const row = reassessmentRow(scope, assignmentId)(await api(`${base(scope, assignmentId)}/${id}`, {
|
|
189
|
+
...scoped(scope, input.signal), method: 'DELETE', body: { cancel_consent: true }
|
|
190
|
+
}));
|
|
191
|
+
if (row.id !== id || row.state !== 'cancelled')
|
|
192
|
+
throw unconfirmed('reassessment');
|
|
193
|
+
return row;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* One page of a reassessment's own criterion results (25 per page), each on its
|
|
197
|
+
* own scale and never pooled with the original's. Empty until it is admitted,
|
|
198
|
+
* and empty for one that root stopped after admission.
|
|
199
|
+
*/
|
|
200
|
+
export async function listReassessmentCriteria(scope, input) {
|
|
201
|
+
const assignmentId = reassessmentId(input.assignmentId, 'assignment id');
|
|
202
|
+
const id = reassessmentId(input.reassessmentId, 'reassessment id');
|
|
203
|
+
const page = pageNumber(input.page);
|
|
204
|
+
const raw = await api(`${base(scope, assignmentId)}/${id}/criteria?page=${page}&page_size=${PAGE}`, scoped(scope, input.signal));
|
|
205
|
+
return pageOf(raw, page, criterionRow, 'reassessment criteria');
|
|
206
|
+
}
|