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,307 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, rename } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { api } from './auth.js';
|
|
6
|
+
import { privateDirectory } from './credentials.js';
|
|
7
|
+
import { withBootstrapProcessLock } from './bootstrap-process-lock.js';
|
|
8
|
+
import { CliError, releaseClosed } from './http.js';
|
|
9
|
+
import { verifyLocalSourceBasis } from './local-source-basis.js';
|
|
10
|
+
import { selectMany, selectOne } from './prompts.js';
|
|
11
|
+
import { signRunnerRequest } from './runner-identity.js';
|
|
12
|
+
import { selectedWorld } from './world-access.js';
|
|
13
|
+
import { identifier } from './world-contracts.js';
|
|
14
|
+
import { localGrantLookupMatches, localGrantsMatch, localGrantOfferMatches } from '../../contracts/local-grant-contracts.js';
|
|
15
|
+
function canonical(value) {
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return '[' + value.map(canonical).join(',') + ']';
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
const item = value;
|
|
20
|
+
return '{' + Object.keys(item).sort().map(key => JSON.stringify(key) + ':' + canonical(item[key])).join(',') + '}';
|
|
21
|
+
}
|
|
22
|
+
const encoded = JSON.stringify(value);
|
|
23
|
+
if (encoded === undefined)
|
|
24
|
+
throw new CliError('local_grant_invalid', 'The local approval is incomplete.');
|
|
25
|
+
return encoded;
|
|
26
|
+
}
|
|
27
|
+
const digest = (value) => createHash('sha256').update(canonical(value)).digest('hex');
|
|
28
|
+
const auth = (context, signal) => ({ actor: context.owner, workspace: context.workspace, signal });
|
|
29
|
+
const endpoint = (context, key) => `${context.prefix}/local-grants/${identifier(key)}`;
|
|
30
|
+
export async function savedLocalPermission(context, key, signal) {
|
|
31
|
+
const value = await api(endpoint(context, key), auth(context, signal));
|
|
32
|
+
if (!localGrantLookupMatches(value, context.owner, context.workspace, context.world, key))
|
|
33
|
+
throw new CliError('local_grant_receipt_invalid', 'The original permission did not match this account, world and request. No local work was started.');
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export async function localPermissionPage(context, selected, signal) {
|
|
37
|
+
const params = new URLSearchParams({ page: String(selected.page ?? 1) });
|
|
38
|
+
if (selected.device)
|
|
39
|
+
params.set('device_key', identifier(selected.device));
|
|
40
|
+
if (selected.grant)
|
|
41
|
+
params.set('grant_id', identifier(selected.grant));
|
|
42
|
+
if (selected.purpose)
|
|
43
|
+
params.set('purpose', selected.purpose);
|
|
44
|
+
if (selected.available)
|
|
45
|
+
params.set('available', 'true');
|
|
46
|
+
if (selected.query)
|
|
47
|
+
params.set('q', selected.query);
|
|
48
|
+
const value = await api(`${context.prefix}/local-grants?${params}`, auth(context, signal));
|
|
49
|
+
if (!localGrantsMatch(value, context.owner, context.workspace, { ...selected, world: context.world }))
|
|
50
|
+
throw new CliError('local_grant_receipt_invalid', 'The permission history did not match this account, world or selected filters.');
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
async function permissionOffer(context, device, signal) {
|
|
54
|
+
const value = await api(`${context.prefix}/local-runners/${identifier(device)}/grant-offer`, auth(context, signal));
|
|
55
|
+
if (!localGrantOfferMatches(value, context.owner, context.workspace, context.world, device))
|
|
56
|
+
throw new CliError('local_grant_offer_invalid', 'The source, runtime and transfer disclosures did not match this selected world and device. Nothing was approved.');
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const pathFor = async (context, key) => join(await privateDirectory(), `local-grant-${identifier(context.owner)}-${identifier(context.workspace)}-${identifier(context.world)}-${identifier(key)}.json`);
|
|
60
|
+
function integer(value, fallback, maximum, name, minimum = 1) {
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
return fallback;
|
|
63
|
+
if (!/^[1-9][0-9]*$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < minimum || Number(value) > maximum) {
|
|
64
|
+
throw new CliError('local_grant_options_invalid', `Use ${name} between ${minimum} and ${maximum}.`, 2);
|
|
65
|
+
}
|
|
66
|
+
return Number(value);
|
|
67
|
+
}
|
|
68
|
+
async function persist(context, value) {
|
|
69
|
+
const path = await pathFor(context, value.key);
|
|
70
|
+
const temporary = path + '.' + randomUUID() + '.tmp';
|
|
71
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
72
|
+
try {
|
|
73
|
+
await file.writeFile(JSON.stringify(value));
|
|
74
|
+
await file.sync();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await file.close();
|
|
78
|
+
}
|
|
79
|
+
await rename(temporary, path);
|
|
80
|
+
}
|
|
81
|
+
async function load(context, key) {
|
|
82
|
+
const path = await pathFor(context, key);
|
|
83
|
+
const before = await lstat(path);
|
|
84
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > 32768 || before.mode & 0o077
|
|
85
|
+
|| process.getuid && before.uid !== process.getuid())
|
|
86
|
+
throw new CliError('local_grant_journal_unsafe', 'The local permission journal must be private and owned by this OS user.');
|
|
87
|
+
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
88
|
+
try {
|
|
89
|
+
const actual = await file.stat();
|
|
90
|
+
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size)
|
|
91
|
+
throw new Error();
|
|
92
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
93
|
+
if (value.schema !== 'firedrill.local-grant-intent.v1' || value.key !== identifier(key) || value.owner !== context.owner
|
|
94
|
+
|| value.workspace !== context.workspace || value.world !== context.world || value.body?.request_key !== key
|
|
95
|
+
|| value.body.schema !== 'firedrill.local-execution-grant.v1' || value.body.world_id !== context.world
|
|
96
|
+
|| !['request_dispatched', 'retained', 'revoked'].includes(value.stage))
|
|
97
|
+
throw new Error();
|
|
98
|
+
identifier(value.body.device_key);
|
|
99
|
+
identifier(value.body.native_session_id);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new CliError('local_grant_journal_invalid', 'The local permission journal changed. It was not replayed or replaced.');
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await file.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function locked(context, key, signal, work) {
|
|
110
|
+
const directory = join(await privateDirectory(), `local-grant-lock-${context.owner}-${context.workspace}-${context.world}-${identifier(key)}`);
|
|
111
|
+
return withBootstrapProcessLock(directory, signal, work);
|
|
112
|
+
}
|
|
113
|
+
function describe(value) {
|
|
114
|
+
if (value.state === 'not_retained')
|
|
115
|
+
return `Local permission ${value.request_key}: no retained receipt yet. A delayed original request may still arrive. Revoke this exact request to withdraw it.`;
|
|
116
|
+
return `Local permission ${value.request_key}: ${value.state.replaceAll('_', ' ')}.\nNo simulation was started by this permission command.`;
|
|
117
|
+
}
|
|
118
|
+
async function record(context, journal, value) {
|
|
119
|
+
if (!localGrantLookupMatches(value, context.owner, context.workspace, context.world, journal.key))
|
|
120
|
+
throw new CliError('local_grant_receipt_invalid', 'The permission receipt does not match this world.');
|
|
121
|
+
if (value.state === 'not_retained')
|
|
122
|
+
return;
|
|
123
|
+
const frozen = { basis: value.basis, runtime: value.runtime, operations: value.operations, limits: value.limits, consent: value.consent };
|
|
124
|
+
const approved = { basis: journal.body.basis, runtime: journal.body.runtime, operations: journal.body.operations,
|
|
125
|
+
limits: journal.body.limits, consent: journal.body.consent };
|
|
126
|
+
if (value.world_id !== context.world || value.device_key !== journal.body.device_key
|
|
127
|
+
|| value.request_digest !== null && (value.request_digest !== digest(journal.body)
|
|
128
|
+
|| value.native_session_id !== journal.body.native_session_id
|
|
129
|
+
|| canonical(frozen) !== canonical(approved))) {
|
|
130
|
+
throw new CliError('local_grant_receipt_invalid', 'The permission receipt does not match the approved scope.');
|
|
131
|
+
}
|
|
132
|
+
journal.stage = value.state === 'revoked' ? 'revoked' : 'retained';
|
|
133
|
+
await persist(context, journal);
|
|
134
|
+
}
|
|
135
|
+
export async function executionPermission(context, key, device, purpose, signal) {
|
|
136
|
+
const journal = await load(context, identifier(key));
|
|
137
|
+
const receipt = await savedLocalPermission(context, key, signal);
|
|
138
|
+
if (journal.stage === 'revoked' || receipt.state !== 'active' || receipt.request_digest !== digest(journal.body)
|
|
139
|
+
|| receipt.world_id !== context.world || receipt.device_key !== identifier(device) || !receipt.id
|
|
140
|
+
|| journal.body.device_key !== device || !journal.body.operations.includes(purpose)) {
|
|
141
|
+
throw new CliError('local_grant_access_removed', 'The saved permission does not authorize this local job. Review its exact source and runtime before continuing.');
|
|
142
|
+
}
|
|
143
|
+
identifier(receipt.id);
|
|
144
|
+
const source = await verifyLocalSourceBasis(context, device, journal.body.native_session_id, journal.body.basis, signal);
|
|
145
|
+
if (source.project_digest !== journal.body.project_digest) {
|
|
146
|
+
throw new CliError('local_project_changed', 'Open the exact project directory approved for this local permission.');
|
|
147
|
+
}
|
|
148
|
+
return { id: receipt.id, document: journal.body };
|
|
149
|
+
}
|
|
150
|
+
export async function authorizeLocalExecution(context, device, options, output) {
|
|
151
|
+
const offer = await permissionOffer(context, identifier(device), options.signal);
|
|
152
|
+
if (offer.schema !== 'firedrill.local-grant-offer.v1' || offer.world_id !== context.world || offer.device_key !== device
|
|
153
|
+
|| !/^[a-f0-9]{64}$/.test(offer.basis?.source_content_digest) || !Array.isArray(offer.runtimes) || offer.runtimes.length !== 1
|
|
154
|
+
|| !Array.isArray(offer.operations) || offer.operations.some(value => !['validation', 'trial', 'world_session'].includes(value))) {
|
|
155
|
+
throw new CliError('local_grant_offer_unavailable', 'The exact source and pinned local runtime offer is unavailable. No permission was granted.');
|
|
156
|
+
}
|
|
157
|
+
if (options.expectedRuntime && canonical(options.expectedRuntime) !== canonical(offer.runtimes[0])) {
|
|
158
|
+
throw new CliError('local_runtime_changed', 'The selected execution image changed after its isolation check. Review local setup again. No permission was granted.');
|
|
159
|
+
}
|
|
160
|
+
const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
161
|
+
const source = await verifyLocalSourceBasis(context, device, offer.native_session_id, offer.basis, options.signal, interactive);
|
|
162
|
+
let operations;
|
|
163
|
+
if (options.operations) {
|
|
164
|
+
operations = options.operations.split(',');
|
|
165
|
+
if (!operations.length || operations.some(value => !offer.operations.includes(value)) || new Set(operations).size !== operations.length) {
|
|
166
|
+
throw new CliError('local_grant_operations_required', 'Choose a comma-separated subset of validation,trial,world_session with --operations.', 2);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (interactive)
|
|
170
|
+
operations = await selectMany('Allow this device to', offer.operations.map(value => ({ value,
|
|
171
|
+
label: value === 'validation' ? 'Validate this world locally' : value === 'world_session' ? 'Open manual world sessions locally' : 'Run admitted simulation drills locally' })), options.signal);
|
|
172
|
+
else
|
|
173
|
+
throw new CliError('local_grant_operations_required', 'Provide --operations with the required subset of validation,trial,world_session.', 2);
|
|
174
|
+
operations.sort();
|
|
175
|
+
if (!operations.length)
|
|
176
|
+
throw new CliError('local_grant_operations_required', 'No operations were selected. No permission was granted.', 2);
|
|
177
|
+
const limits = { parallelism: integer(options.parallel, 1, 10000, 'parallelism'), cpu_units: integer(options.cpu, 2, operations.includes('world_session') ? 65535 : 65536, 'CPU units'),
|
|
178
|
+
memory_mb: integer(options.memory, 4096, 16777216, 'memory in MiB', 1024) };
|
|
179
|
+
const minutes = integer(options.minutes, 60, 1440, 'minutes');
|
|
180
|
+
const details = offer.disclosures;
|
|
181
|
+
if (!details || !Array.isArray(details.vault_values) || !Array.isArray(details.local_values) || !Number.isSafeInteger(details.real_connections)
|
|
182
|
+
|| !Number.isSafeInteger(details.target_model_connections) || !Number.isSafeInteger(details.catalog_packages)
|
|
183
|
+
|| details.catalog_packages < 0)
|
|
184
|
+
throw new CliError('local_grant_offer_invalid', 'Connection disclosures are unavailable.');
|
|
185
|
+
output('local_permission_review', { world_id: context.world, world_name: offer.world_name, source_id: offer.basis.source_id,
|
|
186
|
+
file_count: source.file_count, component_count: source.component_count, operations, limits, minutes, disclosures: details }, [`World: ${offer.world_name}`, `Source: ${source.file_count} matching files${source.component_count > 1 ? ` across ${source.component_count} selected folders` : ''}`, `Operations: ${operations.join(', ')}`,
|
|
187
|
+
`Ceilings: ${limits.parallelism} concurrent, ${limits.cpu_units} CPU units, ${limits.memory_mb} MiB per execution`,
|
|
188
|
+
`Permission expires after ${minutes} minutes. Actual capacity and run admission are separate.`,
|
|
189
|
+
`${details.real_connections} real-account connections; ${details.vault_values.length} values mapped to this device.`,
|
|
190
|
+
...(details.local_values.length ? [`${details.local_values.length} device-only values remain in this OS credential store.`] : []),
|
|
191
|
+
...(details.catalog_packages ? [`${details.catalog_packages} selected tool package bindings may download private service code and baseline data.`] : []),
|
|
192
|
+
'Cloud coordination and model context processing remain enabled. Local execution is not an offline mode.'].join('\n'));
|
|
193
|
+
const consent = { policy: 'firedrill.local-execution-consent.v1', cloud_context_processing: true, run_metadata: true,
|
|
194
|
+
evidence_upload: options.evidence === true, vault_values_to_device: options.vault === true, real_account_access: options.real === true,
|
|
195
|
+
catalog_packages_to_device: options.catalog === true };
|
|
196
|
+
if (interactive && !options.approve) {
|
|
197
|
+
const choices = [
|
|
198
|
+
{ value: 'evidence_upload', label: operations.includes('world_session') ? 'Upload evidence (required for manual sessions)' : 'Upload run evidence', detail: 'Send selected run transcripts, files and captures to your cloud report.' }
|
|
199
|
+
];
|
|
200
|
+
if (details.vault_values.length)
|
|
201
|
+
choices.push({ value: 'vault_values_to_device', label: 'Use mapped Vault values on this device', detail: 'Only the frozen build/runtime mappings, not your whole Vault.' });
|
|
202
|
+
if (details.real_connections)
|
|
203
|
+
choices.push({ value: 'real_account_access', label: 'Use the selected real accounts', detail: 'Real effects cannot be reset or rewound.' });
|
|
204
|
+
if (details.catalog_packages)
|
|
205
|
+
choices.push({ value: 'catalog_packages_to_device', label: 'Download the selected tool packages', detail: 'Copies their private service code and baseline data to this device for isolated execution.' });
|
|
206
|
+
for (const value of await selectMany('Additional permissions', choices, options.signal))
|
|
207
|
+
consent[value] = true;
|
|
208
|
+
const approved = await selectOne('Grant this local permission?', [{ value: true, label: 'Grant permission', detail: 'Allows the listed operations, cloud context processing and saved run metadata. Does not start work.' },
|
|
209
|
+
{ value: false, label: 'Cancel' }], options.signal);
|
|
210
|
+
if (!approved)
|
|
211
|
+
throw new CliError('canceled', 'No permission was granted.', 130);
|
|
212
|
+
}
|
|
213
|
+
else if (!options.approve)
|
|
214
|
+
throw new CliError('local_grant_consent_required', 'Use --approve-local for explicit local execution, cloud context and run-metadata consent.', 2);
|
|
215
|
+
if (operations.includes('world_session') && !consent.evidence_upload) {
|
|
216
|
+
throw new CliError('local_world_session_permission_required', 'Local manual sessions require evidence upload. No permission was submitted.', 2);
|
|
217
|
+
}
|
|
218
|
+
if (details.vault_values.length && !consent.vault_values_to_device || details.real_connections && !consent.real_account_access
|
|
219
|
+
|| details.catalog_packages && !consent.catalog_packages_to_device) {
|
|
220
|
+
throw new CliError('local_grant_consent_required', 'The selected configuration requires the listed value, tool-download or real-account permission. Change the connection choices or explicitly approve them.', 2);
|
|
221
|
+
}
|
|
222
|
+
// Refresh the read-only offer after local capture/user review. A changed
|
|
223
|
+
// basis or runtime requires a new review, never a silent replacement.
|
|
224
|
+
const current = await permissionOffer(context, device, options.signal);
|
|
225
|
+
if (digest(current.basis) !== digest(offer.basis) || digest(current.runtimes) !== digest(offer.runtimes)
|
|
226
|
+
|| current.native_session_id !== offer.native_session_id)
|
|
227
|
+
throw new CliError('local_grant_basis_changed', 'The world or runtime changed during review. No permission was granted.');
|
|
228
|
+
const issued = Date.parse(current.server_time);
|
|
229
|
+
if (!Number.isSafeInteger(issued))
|
|
230
|
+
throw new CliError('local_grant_offer_invalid', 'The approval service time is unavailable.');
|
|
231
|
+
const key = randomUUID();
|
|
232
|
+
const body = { schema: 'firedrill.local-execution-grant.v1', request_key: key, world_id: context.world, device_key: device,
|
|
233
|
+
native_session_id: identifier(current.native_session_id), basis: offer.basis, runtime: offer.runtimes[0], operations, limits, consent,
|
|
234
|
+
project_digest: source.project_digest, issued_at_ms: issued, expires_at_ms: issued + minutes * 60000 };
|
|
235
|
+
const signed = await signRunnerRequest(context, device, body, options.signal);
|
|
236
|
+
const journal = { schema: 'firedrill.local-grant-intent.v1', owner: context.owner, workspace: context.workspace,
|
|
237
|
+
world: context.world, key, body, stage: 'request_dispatched' };
|
|
238
|
+
await persist(context, journal);
|
|
239
|
+
output('local_permission_request', { request_key: key, world_id: context.world }, `Local permission request ${key}. Inspect with firedrill runner grant status ${key} --world ${context.world}.`);
|
|
240
|
+
return locked(context, key, options.signal, async (active) => {
|
|
241
|
+
options = { ...options, signal: active };
|
|
242
|
+
let receipt;
|
|
243
|
+
try {
|
|
244
|
+
const accepted = await api(endpoint(context, key), { ...auth(context, options.signal), method: 'PUT', body: signed });
|
|
245
|
+
if (!localGrantLookupMatches(accepted, context.owner, context.workspace, context.world, key) || accepted.state === 'not_retained')
|
|
246
|
+
throw new CliError('local_grant_acceptance_uncertain', 'The original local approval response was incomplete.');
|
|
247
|
+
receipt = await savedLocalPermission(context, key, options.signal);
|
|
248
|
+
if (receipt.state === 'not_retained' || receipt.id !== accepted.id || receipt.request_digest !== accepted.request_digest)
|
|
249
|
+
throw new CliError('local_grant_acceptance_uncertain', 'The saved original approval could not be corroborated.');
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
// A release refusal is definite. Any other failed dispatch may have
|
|
253
|
+
// committed: read only; never re-sign or replay a broader/new permission
|
|
254
|
+
// after an uncertain acceptance.
|
|
255
|
+
if (releaseClosed(error))
|
|
256
|
+
throw error;
|
|
257
|
+
receipt = await savedLocalPermission(context, key, options.signal);
|
|
258
|
+
}
|
|
259
|
+
await record(context, journal, receipt);
|
|
260
|
+
output('local_permission_receipt', receipt, describe(receipt));
|
|
261
|
+
if (receipt.state === 'not_retained')
|
|
262
|
+
throw new CliError('local_grant_acceptance_uncertain', 'Approval is not confirmed. Inspect or revoke the exact request; it was not repeated.');
|
|
263
|
+
return receipt;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export async function runnerGrantCommand(action, key, options, output) {
|
|
267
|
+
const context = await selectedWorld({ world: options.world, workspace: options.workspace,
|
|
268
|
+
interactive: !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY, openBrowser: options.openBrowser, signal: options.signal });
|
|
269
|
+
if (action === 'authorize') {
|
|
270
|
+
if (!key)
|
|
271
|
+
throw new CliError('runner_key_required', 'Choose a runner key from firedrill runner list.', 2);
|
|
272
|
+
return authorizeLocalExecution(context, identifier(key), options, output);
|
|
273
|
+
}
|
|
274
|
+
if (action === 'list') {
|
|
275
|
+
const page = integer(options.page, 1, 100000000, 'page');
|
|
276
|
+
const values = await localPermissionPage(context, { page }, options.signal);
|
|
277
|
+
output('local_permission_list', values, values.results.map(value => `${value.request_key} ${value.state} ${(value.operations ?? []).join(', ')}`).join('\n') || 'No local execution permissions for this world.');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!key)
|
|
281
|
+
throw new CliError('local_grant_key_required', 'Use the exact local permission request key.', 2);
|
|
282
|
+
key = identifier(key);
|
|
283
|
+
return locked(context, key, options.signal, async (active) => {
|
|
284
|
+
options = { ...options, signal: active };
|
|
285
|
+
let receipt = await savedLocalPermission(context, key, options.signal);
|
|
286
|
+
let journal = null;
|
|
287
|
+
try {
|
|
288
|
+
journal = await load(context, key);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error.code !== 'ENOENT')
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
if (action === 'revoke') {
|
|
295
|
+
const device = receipt.state === 'not_retained' ? journal?.body.device_key : receipt.device_key;
|
|
296
|
+
if (!device)
|
|
297
|
+
throw new CliError('local_grant_journal_required', 'No retained device or local request was found. Keep the exact original journal before withdrawing an unobserved approval.');
|
|
298
|
+
receipt = await api(endpoint(context, key), { ...auth(context, options.signal), method: 'DELETE', body: { device_key: identifier(device) } });
|
|
299
|
+
if (!localGrantLookupMatches(receipt, context.owner, context.workspace, context.world, key) || receipt.state !== 'revoked'
|
|
300
|
+
|| receipt.device_key !== device)
|
|
301
|
+
throw new CliError('local_grant_revocation_uncertain', 'Permission revocation was not confirmed. Inspect the exact saved request.');
|
|
302
|
+
}
|
|
303
|
+
if (journal)
|
|
304
|
+
await record(context, journal, receipt);
|
|
305
|
+
output('local_permission_receipt', receipt, describe(receipt));
|
|
306
|
+
});
|
|
307
|
+
}
|