@smthrs/harness 0.0.0-stage → 1.0.0-rc.4
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/CHANGELOG.md +387 -0
- package/LICENSE +21 -0
- package/README.md +138 -2
- package/dist/cjs/AgentEvent.d.ts +3092 -0
- package/dist/cjs/AgentEvent.d.ts.map +1 -0
- package/dist/cjs/AgentEvent.js +1116 -0
- package/dist/cjs/AgentEvent.js.map +7 -0
- package/dist/cjs/CallLedger.d.ts +378 -0
- package/dist/cjs/CallLedger.d.ts.map +1 -0
- package/dist/cjs/CallLedger.js +240 -0
- package/dist/cjs/CallLedger.js.map +7 -0
- package/dist/cjs/Cell.d.ts +774 -0
- package/dist/cjs/Cell.d.ts.map +1 -0
- package/dist/cjs/Cell.js +431 -0
- package/dist/cjs/Cell.js.map +7 -0
- package/dist/cjs/CellCalls.d.ts +115 -0
- package/dist/cjs/CellCalls.d.ts.map +1 -0
- package/dist/cjs/CellCalls.js +98 -0
- package/dist/cjs/CellCalls.js.map +7 -0
- package/dist/cjs/CellHistory.d.ts +102 -0
- package/dist/cjs/CellHistory.d.ts.map +1 -0
- package/dist/cjs/CellHistory.js +54 -0
- package/dist/cjs/CellHistory.js.map +7 -0
- package/dist/cjs/CellTurn.d.ts +1071 -0
- package/dist/cjs/CellTurn.d.ts.map +1 -0
- package/dist/cjs/CellTurn.js +2618 -0
- package/dist/cjs/CellTurn.js.map +7 -0
- package/dist/cjs/CellValidation.d.ts +94 -0
- package/dist/cjs/CellValidation.d.ts.map +1 -0
- package/dist/cjs/CellValidation.js +218 -0
- package/dist/cjs/CellValidation.js.map +7 -0
- package/dist/cjs/Compaction.d.ts +158 -0
- package/dist/cjs/Compaction.d.ts.map +1 -0
- package/dist/cjs/Compaction.js +189 -0
- package/dist/cjs/Compaction.js.map +7 -0
- package/dist/cjs/CompletionClaim.d.ts +801 -0
- package/dist/cjs/CompletionClaim.d.ts.map +1 -0
- package/dist/cjs/CompletionClaim.js +303 -0
- package/dist/cjs/CompletionClaim.js.map +7 -0
- package/dist/cjs/ContextWindow.d.ts +435 -0
- package/dist/cjs/ContextWindow.d.ts.map +1 -0
- package/dist/cjs/ContextWindow.js +319 -0
- package/dist/cjs/ContextWindow.js.map +7 -0
- package/dist/cjs/EngineLike.d.ts +546 -0
- package/dist/cjs/EngineLike.d.ts.map +1 -0
- package/dist/cjs/EngineLike.js +114 -0
- package/dist/cjs/EngineLike.js.map +7 -0
- package/dist/cjs/ExternalTranscript.d.ts +349 -0
- package/dist/cjs/ExternalTranscript.d.ts.map +1 -0
- package/dist/cjs/ExternalTranscript.js +826 -0
- package/dist/cjs/ExternalTranscript.js.map +7 -0
- package/dist/cjs/FailedCall.d.ts +132 -0
- package/dist/cjs/FailedCall.d.ts.map +1 -0
- package/dist/cjs/FailedCall.js +53 -0
- package/dist/cjs/FailedCall.js.map +7 -0
- package/dist/cjs/FlowBinding.d.ts +289 -0
- package/dist/cjs/FlowBinding.d.ts.map +1 -0
- package/dist/cjs/FlowBinding.js +251 -0
- package/dist/cjs/FlowBinding.js.map +7 -0
- package/dist/cjs/HarnessError.d.ts +57 -0
- package/dist/cjs/HarnessError.d.ts.map +1 -0
- package/dist/cjs/HarnessError.js +75 -0
- package/dist/cjs/HarnessError.js.map +7 -0
- package/dist/cjs/Judgement.d.ts +289 -0
- package/dist/cjs/Judgement.d.ts.map +1 -0
- package/dist/cjs/Judgement.js +240 -0
- package/dist/cjs/Judgement.js.map +7 -0
- package/dist/cjs/Monitor.d.ts +374 -0
- package/dist/cjs/Monitor.d.ts.map +1 -0
- package/dist/cjs/Monitor.js +233 -0
- package/dist/cjs/Monitor.js.map +7 -0
- package/dist/cjs/NarrowedCheck.d.ts +523 -0
- package/dist/cjs/NarrowedCheck.d.ts.map +1 -0
- package/dist/cjs/NarrowedCheck.js +263 -0
- package/dist/cjs/NarrowedCheck.js.map +7 -0
- package/dist/cjs/Notifications.d.ts +42 -0
- package/dist/cjs/Notifications.d.ts.map +1 -0
- package/dist/cjs/Notifications.js +177 -0
- package/dist/cjs/Notifications.js.map +7 -0
- package/dist/cjs/Plan.d.ts +127 -0
- package/dist/cjs/Plan.d.ts.map +1 -0
- package/dist/cjs/Plan.js +77 -0
- package/dist/cjs/Plan.js.map +7 -0
- package/dist/cjs/QuickJSSandbox.d.ts +151 -0
- package/dist/cjs/QuickJSSandbox.d.ts.map +1 -0
- package/dist/cjs/QuickJSSandbox.js +987 -0
- package/dist/cjs/QuickJSSandbox.js.map +7 -0
- package/dist/cjs/Relevance.d.ts +213 -0
- package/dist/cjs/Relevance.d.ts.map +1 -0
- package/dist/cjs/Relevance.js +183 -0
- package/dist/cjs/Relevance.js.map +7 -0
- package/dist/cjs/Sandbox.d.ts +637 -0
- package/dist/cjs/Sandbox.d.ts.map +1 -0
- package/dist/cjs/Sandbox.js +260 -0
- package/dist/cjs/Sandbox.js.map +7 -0
- package/dist/cjs/Steering.d.ts +464 -0
- package/dist/cjs/Steering.d.ts.map +1 -0
- package/dist/cjs/Steering.js +153 -0
- package/dist/cjs/Steering.js.map +7 -0
- package/dist/cjs/StructuredOutput.d.ts +252 -0
- package/dist/cjs/StructuredOutput.d.ts.map +1 -0
- package/dist/cjs/StructuredOutput.js +266 -0
- package/dist/cjs/StructuredOutput.js.map +7 -0
- package/dist/cjs/Sufficiency.d.ts +195 -0
- package/dist/cjs/Sufficiency.d.ts.map +1 -0
- package/dist/cjs/Sufficiency.js +110 -0
- package/dist/cjs/Sufficiency.js.map +7 -0
- package/dist/cjs/Supervisor.d.ts +719 -0
- package/dist/cjs/Supervisor.d.ts.map +1 -0
- package/dist/cjs/Supervisor.js +313 -0
- package/dist/cjs/Supervisor.js.map +7 -0
- package/dist/cjs/Tokens.d.ts +86 -0
- package/dist/cjs/Tokens.d.ts.map +1 -0
- package/dist/cjs/Tokens.js +72 -0
- package/dist/cjs/Tokens.js.map +7 -0
- package/dist/cjs/Transcript.d.ts +171 -0
- package/dist/cjs/Transcript.d.ts.map +1 -0
- package/dist/cjs/Transcript.js +342 -0
- package/dist/cjs/Transcript.js.map +7 -0
- package/dist/cjs/TruncatedOutput.d.ts +186 -0
- package/dist/cjs/TruncatedOutput.d.ts.map +1 -0
- package/dist/cjs/TruncatedOutput.js +143 -0
- package/dist/cjs/TruncatedOutput.js.map +7 -0
- package/dist/cjs/UnmovedTree.d.ts +113 -0
- package/dist/cjs/UnmovedTree.d.ts.map +1 -0
- package/dist/cjs/UnmovedTree.js +38 -0
- package/dist/cjs/UnmovedTree.js.map +7 -0
- package/dist/cjs/UnresolvedFailure.d.ts +196 -0
- package/dist/cjs/UnresolvedFailure.d.ts.map +1 -0
- package/dist/cjs/UnresolvedFailure.js +77 -0
- package/dist/cjs/UnresolvedFailure.js.map +7 -0
- package/dist/cjs/VacuousVerification.d.ts +237 -0
- package/dist/cjs/VacuousVerification.d.ts.map +1 -0
- package/dist/cjs/VacuousVerification.js +91 -0
- package/dist/cjs/VacuousVerification.js.map +7 -0
- package/dist/cjs/VariablesPanel.d.ts +117 -0
- package/dist/cjs/VariablesPanel.d.ts.map +1 -0
- package/dist/cjs/VariablesPanel.js +108 -0
- package/dist/cjs/VariablesPanel.js.map +7 -0
- package/dist/cjs/index.d.ts +172 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +99 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/bytes.d.ts +36 -0
- package/dist/cjs/internal/bytes.d.ts.map +1 -0
- package/dist/cjs/internal/bytes.js +57 -0
- package/dist/cjs/internal/bytes.js.map +7 -0
- package/dist/cjs/internal/cellPrompt.d.ts +122 -0
- package/dist/cjs/internal/cellPrompt.d.ts.map +1 -0
- package/dist/cjs/internal/cellPrompt.js +146 -0
- package/dist/cjs/internal/cellPrompt.js.map +7 -0
- package/dist/cjs/internal/compactable.d.ts +44 -0
- package/dist/cjs/internal/compactable.d.ts.map +1 -0
- package/dist/cjs/internal/compactable.js +56 -0
- package/dist/cjs/internal/compactable.js.map +7 -0
- package/dist/cjs/internal/compactionMarks.d.ts +278 -0
- package/dist/cjs/internal/compactionMarks.d.ts.map +1 -0
- package/dist/cjs/internal/compactionMarks.js +212 -0
- package/dist/cjs/internal/compactionMarks.js.map +7 -0
- package/dist/cjs/internal/demandText.d.ts +95 -0
- package/dist/cjs/internal/demandText.d.ts.map +1 -0
- package/dist/cjs/internal/demandText.js +70 -0
- package/dist/cjs/internal/demandText.js.map +7 -0
- package/dist/cjs/internal/elide.d.ts +109 -0
- package/dist/cjs/internal/elide.d.ts.map +1 -0
- package/dist/cjs/internal/elide.js +59 -0
- package/dist/cjs/internal/elide.js.map +7 -0
- package/dist/cjs/internal/frame.d.ts +463 -0
- package/dist/cjs/internal/frame.d.ts.map +1 -0
- package/dist/cjs/internal/frame.js +514 -0
- package/dist/cjs/internal/frame.js.map +7 -0
- package/dist/cjs/internal/nonNegativeSafeInt.d.ts +20 -0
- package/dist/cjs/internal/nonNegativeSafeInt.d.ts.map +1 -0
- package/dist/cjs/internal/nonNegativeSafeInt.js +29 -0
- package/dist/cjs/internal/nonNegativeSafeInt.js.map +7 -0
- package/dist/cjs/internal/paidUsage.d.ts +52 -0
- package/dist/cjs/internal/paidUsage.d.ts.map +1 -0
- package/dist/cjs/internal/paidUsage.js +70 -0
- package/dist/cjs/internal/paidUsage.js.map +7 -0
- package/dist/cjs/internal/printChannel.d.ts +233 -0
- package/dist/cjs/internal/printChannel.d.ts.map +1 -0
- package/dist/cjs/internal/printChannel.js +165 -0
- package/dist/cjs/internal/printChannel.js.map +7 -0
- package/dist/cjs/internal/printsObservation.d.ts +26 -0
- package/dist/cjs/internal/printsObservation.d.ts.map +1 -0
- package/dist/cjs/internal/printsObservation.js +27 -0
- package/dist/cjs/internal/printsObservation.js.map +7 -0
- package/dist/cjs/internal/refusal.d.ts +40 -0
- package/dist/cjs/internal/refusal.d.ts.map +1 -0
- package/dist/cjs/internal/refusal.js +45 -0
- package/dist/cjs/internal/refusal.js.map +7 -0
- package/dist/cjs/internal/supervision.d.ts +174 -0
- package/dist/cjs/internal/supervision.d.ts.map +1 -0
- package/dist/cjs/internal/supervision.js +402 -0
- package/dist/cjs/internal/supervision.js.map +7 -0
- package/dist/cjs/internal/unfinishedWork.d.ts +99 -0
- package/dist/cjs/internal/unfinishedWork.d.ts.map +1 -0
- package/dist/cjs/internal/unfinishedWork.js +80 -0
- package/dist/cjs/internal/unfinishedWork.js.map +7 -0
- package/dist/cjs/internal/unobservedCall.d.ts +112 -0
- package/dist/cjs/internal/unobservedCall.d.ts.map +1 -0
- package/dist/cjs/internal/unobservedCall.js +345 -0
- package/dist/cjs/internal/unobservedCall.js.map +7 -0
- package/dist/cjs/internal/untrustedData.d.ts +14 -0
- package/dist/cjs/internal/untrustedData.d.ts.map +1 -0
- package/dist/cjs/internal/untrustedData.js +30 -0
- package/dist/cjs/internal/untrustedData.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/AgentEvent.d.ts +3092 -0
- package/dist/esm/AgentEvent.d.ts.map +1 -0
- package/dist/esm/AgentEvent.js +1610 -0
- package/dist/esm/AgentEvent.js.map +1 -0
- package/dist/esm/CallLedger.d.ts +378 -0
- package/dist/esm/CallLedger.d.ts.map +1 -0
- package/dist/esm/CallLedger.js +506 -0
- package/dist/esm/CallLedger.js.map +1 -0
- package/dist/esm/Cell.d.ts +774 -0
- package/dist/esm/Cell.d.ts.map +1 -0
- package/dist/esm/Cell.js +772 -0
- package/dist/esm/Cell.js.map +1 -0
- package/dist/esm/CellCalls.d.ts +115 -0
- package/dist/esm/CellCalls.d.ts.map +1 -0
- package/dist/esm/CellCalls.js +97 -0
- package/dist/esm/CellCalls.js.map +1 -0
- package/dist/esm/CellHistory.d.ts +102 -0
- package/dist/esm/CellHistory.d.ts.map +1 -0
- package/dist/esm/CellHistory.js +91 -0
- package/dist/esm/CellHistory.js.map +1 -0
- package/dist/esm/CellTurn.d.ts +1071 -0
- package/dist/esm/CellTurn.d.ts.map +1 -0
- package/dist/esm/CellTurn.js +3410 -0
- package/dist/esm/CellTurn.js.map +1 -0
- package/dist/esm/CellValidation.d.ts +94 -0
- package/dist/esm/CellValidation.d.ts.map +1 -0
- package/dist/esm/CellValidation.js +335 -0
- package/dist/esm/CellValidation.js.map +1 -0
- package/dist/esm/Compaction.d.ts +158 -0
- package/dist/esm/Compaction.d.ts.map +1 -0
- package/dist/esm/Compaction.js +216 -0
- package/dist/esm/Compaction.js.map +1 -0
- package/dist/esm/CompletionClaim.d.ts +801 -0
- package/dist/esm/CompletionClaim.d.ts.map +1 -0
- package/dist/esm/CompletionClaim.js +833 -0
- package/dist/esm/CompletionClaim.js.map +1 -0
- package/dist/esm/ContextWindow.d.ts +435 -0
- package/dist/esm/ContextWindow.d.ts.map +1 -0
- package/dist/esm/ContextWindow.js +427 -0
- package/dist/esm/ContextWindow.js.map +1 -0
- package/dist/esm/EngineLike.d.ts +546 -0
- package/dist/esm/EngineLike.d.ts.map +1 -0
- package/dist/esm/EngineLike.js +218 -0
- package/dist/esm/EngineLike.js.map +1 -0
- package/dist/esm/ExternalTranscript.d.ts +349 -0
- package/dist/esm/ExternalTranscript.d.ts.map +1 -0
- package/dist/esm/ExternalTranscript.js +985 -0
- package/dist/esm/ExternalTranscript.js.map +1 -0
- package/dist/esm/FailedCall.d.ts +132 -0
- package/dist/esm/FailedCall.d.ts.map +1 -0
- package/dist/esm/FailedCall.js +131 -0
- package/dist/esm/FailedCall.js.map +1 -0
- package/dist/esm/FlowBinding.d.ts +289 -0
- package/dist/esm/FlowBinding.d.ts.map +1 -0
- package/dist/esm/FlowBinding.js +376 -0
- package/dist/esm/FlowBinding.js.map +1 -0
- package/dist/esm/HarnessError.d.ts +57 -0
- package/dist/esm/HarnessError.d.ts.map +1 -0
- package/dist/esm/HarnessError.js +85 -0
- package/dist/esm/HarnessError.js.map +1 -0
- package/dist/esm/Judgement.d.ts +289 -0
- package/dist/esm/Judgement.d.ts.map +1 -0
- package/dist/esm/Judgement.js +305 -0
- package/dist/esm/Judgement.js.map +1 -0
- package/dist/esm/Monitor.d.ts +374 -0
- package/dist/esm/Monitor.d.ts.map +1 -0
- package/dist/esm/Monitor.js +370 -0
- package/dist/esm/Monitor.js.map +1 -0
- package/dist/esm/NarrowedCheck.d.ts +523 -0
- package/dist/esm/NarrowedCheck.d.ts.map +1 -0
- package/dist/esm/NarrowedCheck.js +612 -0
- package/dist/esm/NarrowedCheck.js.map +1 -0
- package/dist/esm/Notifications.d.ts +42 -0
- package/dist/esm/Notifications.d.ts.map +1 -0
- package/dist/esm/Notifications.js +215 -0
- package/dist/esm/Notifications.js.map +1 -0
- package/dist/esm/Plan.d.ts +127 -0
- package/dist/esm/Plan.d.ts.map +1 -0
- package/dist/esm/Plan.js +97 -0
- package/dist/esm/Plan.js.map +1 -0
- package/dist/esm/QuickJSSandbox.d.ts +151 -0
- package/dist/esm/QuickJSSandbox.d.ts.map +1 -0
- package/dist/esm/QuickJSSandbox.js +1364 -0
- package/dist/esm/QuickJSSandbox.js.map +1 -0
- package/dist/esm/Relevance.d.ts +213 -0
- package/dist/esm/Relevance.d.ts.map +1 -0
- package/dist/esm/Relevance.js +254 -0
- package/dist/esm/Relevance.js.map +1 -0
- package/dist/esm/Sandbox.d.ts +637 -0
- package/dist/esm/Sandbox.d.ts.map +1 -0
- package/dist/esm/Sandbox.js +464 -0
- package/dist/esm/Sandbox.js.map +1 -0
- package/dist/esm/Steering.d.ts +464 -0
- package/dist/esm/Steering.d.ts.map +1 -0
- package/dist/esm/Steering.js +193 -0
- package/dist/esm/Steering.js.map +1 -0
- package/dist/esm/StructuredOutput.d.ts +252 -0
- package/dist/esm/StructuredOutput.d.ts.map +1 -0
- package/dist/esm/StructuredOutput.js +430 -0
- package/dist/esm/StructuredOutput.js.map +1 -0
- package/dist/esm/Sufficiency.d.ts +195 -0
- package/dist/esm/Sufficiency.d.ts.map +1 -0
- package/dist/esm/Sufficiency.js +207 -0
- package/dist/esm/Sufficiency.js.map +1 -0
- package/dist/esm/Supervisor.d.ts +719 -0
- package/dist/esm/Supervisor.d.ts.map +1 -0
- package/dist/esm/Supervisor.js +575 -0
- package/dist/esm/Supervisor.js.map +1 -0
- package/dist/esm/Tokens.d.ts +86 -0
- package/dist/esm/Tokens.d.ts.map +1 -0
- package/dist/esm/Tokens.js +92 -0
- package/dist/esm/Tokens.js.map +1 -0
- package/dist/esm/Transcript.d.ts +171 -0
- package/dist/esm/Transcript.d.ts.map +1 -0
- package/dist/esm/Transcript.js +424 -0
- package/dist/esm/Transcript.js.map +1 -0
- package/dist/esm/TruncatedOutput.d.ts +186 -0
- package/dist/esm/TruncatedOutput.d.ts.map +1 -0
- package/dist/esm/TruncatedOutput.js +257 -0
- package/dist/esm/TruncatedOutput.js.map +1 -0
- package/dist/esm/UnmovedTree.d.ts +113 -0
- package/dist/esm/UnmovedTree.d.ts.map +1 -0
- package/dist/esm/UnmovedTree.js +90 -0
- package/dist/esm/UnmovedTree.js.map +1 -0
- package/dist/esm/UnresolvedFailure.d.ts +196 -0
- package/dist/esm/UnresolvedFailure.d.ts.map +1 -0
- package/dist/esm/UnresolvedFailure.js +218 -0
- package/dist/esm/UnresolvedFailure.js.map +1 -0
- package/dist/esm/VacuousVerification.d.ts +237 -0
- package/dist/esm/VacuousVerification.d.ts.map +1 -0
- package/dist/esm/VacuousVerification.js +245 -0
- package/dist/esm/VacuousVerification.js.map +1 -0
- package/dist/esm/VariablesPanel.d.ts +117 -0
- package/dist/esm/VariablesPanel.d.ts.map +1 -0
- package/dist/esm/VariablesPanel.js +142 -0
- package/dist/esm/VariablesPanel.js.map +1 -0
- package/dist/esm/index.d.ts +172 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +172 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/bytes.d.ts +36 -0
- package/dist/esm/internal/bytes.d.ts.map +1 -0
- package/dist/esm/internal/bytes.js +70 -0
- package/dist/esm/internal/bytes.js.map +1 -0
- package/dist/esm/internal/cellPrompt.d.ts +122 -0
- package/dist/esm/internal/cellPrompt.d.ts.map +1 -0
- package/dist/esm/internal/cellPrompt.js +276 -0
- package/dist/esm/internal/cellPrompt.js.map +1 -0
- package/dist/esm/internal/compactable.d.ts +44 -0
- package/dist/esm/internal/compactable.d.ts.map +1 -0
- package/dist/esm/internal/compactable.js +71 -0
- package/dist/esm/internal/compactable.js.map +1 -0
- package/dist/esm/internal/compactionMarks.d.ts +278 -0
- package/dist/esm/internal/compactionMarks.d.ts.map +1 -0
- package/dist/esm/internal/compactionMarks.js +317 -0
- package/dist/esm/internal/compactionMarks.js.map +1 -0
- package/dist/esm/internal/demandText.d.ts +95 -0
- package/dist/esm/internal/demandText.d.ts.map +1 -0
- package/dist/esm/internal/demandText.js +128 -0
- package/dist/esm/internal/demandText.js.map +1 -0
- package/dist/esm/internal/elide.d.ts +109 -0
- package/dist/esm/internal/elide.d.ts.map +1 -0
- package/dist/esm/internal/elide.js +123 -0
- package/dist/esm/internal/elide.js.map +1 -0
- package/dist/esm/internal/frame.d.ts +463 -0
- package/dist/esm/internal/frame.d.ts.map +1 -0
- package/dist/esm/internal/frame.js +861 -0
- package/dist/esm/internal/frame.js.map +1 -0
- package/dist/esm/internal/nonNegativeSafeInt.d.ts +20 -0
- package/dist/esm/internal/nonNegativeSafeInt.d.ts.map +1 -0
- package/dist/esm/internal/nonNegativeSafeInt.js +20 -0
- package/dist/esm/internal/nonNegativeSafeInt.js.map +1 -0
- package/dist/esm/internal/paidUsage.d.ts +52 -0
- package/dist/esm/internal/paidUsage.d.ts.map +1 -0
- package/dist/esm/internal/paidUsage.js +72 -0
- package/dist/esm/internal/paidUsage.js.map +1 -0
- package/dist/esm/internal/printChannel.d.ts +233 -0
- package/dist/esm/internal/printChannel.d.ts.map +1 -0
- package/dist/esm/internal/printChannel.js +376 -0
- package/dist/esm/internal/printChannel.js.map +1 -0
- package/dist/esm/internal/printsObservation.d.ts +26 -0
- package/dist/esm/internal/printsObservation.d.ts.map +1 -0
- package/dist/esm/internal/printsObservation.js +29 -0
- package/dist/esm/internal/printsObservation.js.map +1 -0
- package/dist/esm/internal/refusal.d.ts +40 -0
- package/dist/esm/internal/refusal.d.ts.map +1 -0
- package/dist/esm/internal/refusal.js +47 -0
- package/dist/esm/internal/refusal.js.map +1 -0
- package/dist/esm/internal/supervision.d.ts +174 -0
- package/dist/esm/internal/supervision.d.ts.map +1 -0
- package/dist/esm/internal/supervision.js +485 -0
- package/dist/esm/internal/supervision.js.map +1 -0
- package/dist/esm/internal/unfinishedWork.d.ts +99 -0
- package/dist/esm/internal/unfinishedWork.d.ts.map +1 -0
- package/dist/esm/internal/unfinishedWork.js +85 -0
- package/dist/esm/internal/unfinishedWork.js.map +1 -0
- package/dist/esm/internal/unobservedCall.d.ts +112 -0
- package/dist/esm/internal/unobservedCall.d.ts.map +1 -0
- package/dist/esm/internal/unobservedCall.js +501 -0
- package/dist/esm/internal/unobservedCall.js.map +1 -0
- package/dist/esm/internal/untrustedData.d.ts +14 -0
- package/dist/esm/internal/untrustedData.d.ts.map +1 -0
- package/dist/esm/internal/untrustedData.js +15 -0
- package/dist/esm/internal/untrustedData.js.map +1 -0
- package/docs/README.md +129 -0
- package/docs/api.md +1869 -0
- package/docs/concepts.md +238 -0
- package/docs/external-transcripts.md +245 -0
- package/docs/guides/bind-flows.md +167 -0
- package/docs/guides/drive-the-loop.md +265 -0
- package/docs/guides/run-cells.md +262 -0
- package/docs/guides/workerd.md +144 -0
- package/docs/installation.md +69 -0
- package/docs/quickstart.md +121 -0
- package/docs/reference.md +954 -0
- package/docs/troubleshooting.md +177 -0
- package/package.json +463 -3
- package/src/AgentEvent.ts +1772 -0
- package/src/CallLedger.ts +560 -0
- package/src/Cell.ts +926 -0
- package/src/CellCalls.ts +198 -0
- package/src/CellHistory.ts +128 -0
- package/src/CellTurn.ts +4450 -0
- package/src/CellValidation.ts +382 -0
- package/src/Compaction.ts +330 -0
- package/src/CompletionClaim.ts +1013 -0
- package/src/ContextWindow.ts +669 -0
- package/src/EngineLike.ts +614 -0
- package/src/ExternalTranscript.ts +1142 -0
- package/src/FailedCall.ts +163 -0
- package/src/FlowBinding.ts +603 -0
- package/src/HarnessError.ts +98 -0
- package/src/Judgement.ts +526 -0
- package/src/Monitor.ts +579 -0
- package/src/NarrowedCheck.ts +694 -0
- package/src/Notifications.ts +262 -0
- package/src/Plan.ts +113 -0
- package/src/QuickJSSandbox.ts +1557 -0
- package/src/Relevance.ts +352 -0
- package/src/Sandbox.ts +908 -0
- package/src/Steering.ts +405 -0
- package/src/StructuredOutput.ts +484 -0
- package/src/Sufficiency.ts +247 -0
- package/src/Supervisor.ts +798 -0
- package/src/Tokens.ts +108 -0
- package/src/Transcript.ts +513 -0
- package/src/TruncatedOutput.ts +297 -0
- package/src/UnmovedTree.ts +121 -0
- package/src/UnresolvedFailure.ts +240 -0
- package/src/VacuousVerification.ts +280 -0
- package/src/VariablesPanel.ts +165 -0
- package/src/index.ts +204 -0
- package/src/internal/bytes.ts +70 -0
- package/src/internal/cellPrompt.ts +337 -0
- package/src/internal/compactable.ts +76 -0
- package/src/internal/compactionMarks.ts +454 -0
- package/src/internal/demandText.ts +145 -0
- package/src/internal/elide.ts +130 -0
- package/src/internal/frame.ts +1178 -0
- package/src/internal/nonNegativeSafeInt.ts +24 -0
- package/src/internal/paidUsage.ts +90 -0
- package/src/internal/printChannel.ts +434 -0
- package/src/internal/printsObservation.ts +31 -0
- package/src/internal/refusal.ts +53 -0
- package/src/internal/supervision.ts +652 -0
- package/src/internal/unfinishedWork.ts +123 -0
- package/src/internal/unobservedCall.ts +536 -0
- package/src/internal/untrustedData.ts +19 -0
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The QuickJS-WASM sandbox binding.
|
|
3
|
+
*
|
|
4
|
+
* This is the production `Sandbox`: the cell runs inside a QuickJS interpreter
|
|
5
|
+
* compiled to WebAssembly, which is a genuinely separate JavaScript realm with
|
|
6
|
+
* no reference to the host's globals, prototypes, or module loader. The same
|
|
7
|
+
* single-file variant runs unmodified on Node and in a browser, so a browser
|
|
8
|
+
* host provides this layer and calls the identical harness.
|
|
9
|
+
*
|
|
10
|
+
* What the cell can reach is exactly `ctx`: `ctx.call` bridges to the host's
|
|
11
|
+
* durable flow boundary, `ctx.flows` is a frozen catalog projection. The
|
|
12
|
+
* prelude removes `Date` and `Math.random` from the realm, because a replayed
|
|
13
|
+
* cell must reach the same calls in the same order, and `Proxy`, because a
|
|
14
|
+
* value that answers reflection from a handler cannot be weighed against the
|
|
15
|
+
* run's memory ceiling. There is no filesystem, no network, no process, and no
|
|
16
|
+
* module loader to reach in the first place.
|
|
17
|
+
*
|
|
18
|
+
* Teardown is scope finalization and cancellation is fiber interruption: an
|
|
19
|
+
* interrupted frame disposes the runtime, which is the only thing holding the
|
|
20
|
+
* cell alive.
|
|
21
|
+
*
|
|
22
|
+
* Which QuickJS build is compiled is a seam. The default compiles the
|
|
23
|
+
* single-file build from bytes, which is what Node and a browser want. A host
|
|
24
|
+
* whose runtime forbids compiling WebAssembly from bytes at runtime, such as
|
|
25
|
+
* Cloudflare's workerd, provides {@link Variant} instead and names a build
|
|
26
|
+
* whose module came from its toolchain.
|
|
27
|
+
*
|
|
28
|
+
* @since 0.1.0
|
|
29
|
+
*/
|
|
30
|
+
import singlefile from "@jitl/quickjs-singlefile-browser-release-sync";
|
|
31
|
+
import { Context, Duration, Effect, Layer, Option, Schema } from "effect";
|
|
32
|
+
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten-core";
|
|
33
|
+
import * as Cell from "./Cell.js";
|
|
34
|
+
import * as bytes from "./internal/bytes.js";
|
|
35
|
+
import * as elide from "./internal/elide.js";
|
|
36
|
+
import * as printChannel from "./internal/printChannel.js";
|
|
37
|
+
import { refusal } from "./internal/refusal.js";
|
|
38
|
+
import * as Sandbox from "./Sandbox.js";
|
|
39
|
+
import * as VariablesPanel from "./VariablesPanel.js";
|
|
40
|
+
/**
|
|
41
|
+
* Every intrinsic the host's own realm-side code reads, bound before a cell runs.
|
|
42
|
+
*
|
|
43
|
+
* A global script may rebind `Object`, and under a per-cell realm that cost the
|
|
44
|
+
* cell that did it and nothing else. Under a realm that outlives the cell it
|
|
45
|
+
* costs the run: `encode` reaches `Object.keys` at call time, so one top-level
|
|
46
|
+
* `const Object = {}` in frame 3 makes every `ctx.call` and every
|
|
47
|
+
* `console.log` from frame 4 on die on `TypeError: not a function`, with no
|
|
48
|
+
* name in the failure to connect it to the declaration that caused it. The same
|
|
49
|
+
* holds for `JSON`, `Array`, `Number`, `String`, `TypeError` and `Promise`.
|
|
50
|
+
*
|
|
51
|
+
* Binding them here makes a cell that shadows an intrinsic cost exactly what a
|
|
52
|
+
* REPL should charge for it — its own later code — and nothing the harness runs
|
|
53
|
+
* on the cell's behalf. Nothing is refused, because nothing needs to be.
|
|
54
|
+
*/
|
|
55
|
+
const preludeIntrinsics = ` var keysOf = Object.keys
|
|
56
|
+
var freezeValue = Object.freeze
|
|
57
|
+
var prototypeOf = Object.getPrototypeOf
|
|
58
|
+
var objectPrototype = Object.prototype
|
|
59
|
+
var define = Object.defineProperty
|
|
60
|
+
var isArray = Array.isArray
|
|
61
|
+
var stringify = JSON.stringify
|
|
62
|
+
var parse = JSON.parse
|
|
63
|
+
var finite = Number.isFinite
|
|
64
|
+
var render = String
|
|
65
|
+
var Fault = TypeError
|
|
66
|
+
var Deferred = Promise
|
|
67
|
+
var argumentsOf = Array.prototype.slice`;
|
|
68
|
+
/**
|
|
69
|
+
* The realm-side helpers both preludes install.
|
|
70
|
+
*
|
|
71
|
+
* `encode` carries the label of what it is encoding so one validator serves
|
|
72
|
+
* `ctx.call`, `ctx.done`, `ctx.park` and `ctx.justify` without any of them
|
|
73
|
+
* having to describe its own refusal.
|
|
74
|
+
*/
|
|
75
|
+
const preludeHelpers = `${preludeIntrinsics}
|
|
76
|
+
var freeze = function (value) {
|
|
77
|
+
if (value !== null && typeof value === "object") {
|
|
78
|
+
keysOf(value).forEach(function (key) { freeze(value[key]) })
|
|
79
|
+
freezeValue(value)
|
|
80
|
+
}
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
var encode = function (label, input) {
|
|
84
|
+
var seen = []
|
|
85
|
+
var visit = function (value) {
|
|
86
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return
|
|
87
|
+
if (typeof value === "number" && finite(value)) return
|
|
88
|
+
if (typeof value !== "object") throw new Fault(label + " must be JSON-serializable")
|
|
89
|
+
if (seen.indexOf(value) >= 0) throw new Fault(label + " must be JSON-serializable")
|
|
90
|
+
var prototype = prototypeOf(value)
|
|
91
|
+
if (!isArray(value) && prototype !== objectPrototype && prototype !== null) {
|
|
92
|
+
throw new Fault(label + " must be JSON-serializable")
|
|
93
|
+
}
|
|
94
|
+
seen.push(value)
|
|
95
|
+
keysOf(value).forEach(function (key) { visit(value[key]) })
|
|
96
|
+
seen.pop()
|
|
97
|
+
}
|
|
98
|
+
visit(input)
|
|
99
|
+
return stringify(input)
|
|
100
|
+
}
|
|
101
|
+
var settleEnvelope = function (settled) {
|
|
102
|
+
// A failed call RESOLVES with the failure envelope; only teardown throws.
|
|
103
|
+
// See Cell.callFailure for why.
|
|
104
|
+
if (settled.ok) return settled.value
|
|
105
|
+
if (settled.aborted) throw new Error(settled.failure.error.message)
|
|
106
|
+
return settled.failure
|
|
107
|
+
}
|
|
108
|
+
var dispatch = function (flow, input, at) { return bridge(flow, input, at).then(settleEnvelope) }
|
|
109
|
+
// The at option is whatever the cell wrote, so it is encoded defensively
|
|
110
|
+
// rather than strictly: a value JSON cannot hold travels as null and the
|
|
111
|
+
// boundary answers it as an ordinary invalid_input, instead of throwing out
|
|
112
|
+
// of a cell that has already paid for the calls before this line.
|
|
113
|
+
var encodeAt = function (value) {
|
|
114
|
+
try { return encode("ctx.call at", value === undefined ? null : value) } catch (error) { return "null" }
|
|
115
|
+
}`;
|
|
116
|
+
/**
|
|
117
|
+
* The handle `ctx.base` is bound to, rendered into both preludes as data.
|
|
118
|
+
*
|
|
119
|
+
* It is a constant rather than something the host mints, because the run's
|
|
120
|
+
* opening tree is not a thing anybody has to decide to keep: the host either
|
|
121
|
+
* recorded one or it did not, and a call against this id says which by
|
|
122
|
+
* succeeding or by answering `checkpoint_unavailable`.
|
|
123
|
+
*/
|
|
124
|
+
const baseHandle = Cell.checkpoint(Cell.baseCheckpoint);
|
|
125
|
+
/**
|
|
126
|
+
* The name a queued mint carries, so an overrun reads as what it was.
|
|
127
|
+
*
|
|
128
|
+
* Nothing resolves it: a checkpoint is not a flow, it is not in the catalog,
|
|
129
|
+
* and no capability gates it. See `Sandbox` `Minter`.
|
|
130
|
+
*/
|
|
131
|
+
const checkpointFlow = "checkpoint";
|
|
132
|
+
/** Reads the `at` the realm encoded; the empty string is "the cell passed none". */
|
|
133
|
+
const decodedAt = (encoded) => encoded === "" ? undefined : Schema.decodeUnknownSync(Schema.Json)(JSON.parse(encoded));
|
|
134
|
+
/**
|
|
135
|
+
* The `ctx.call`, `ctx.checkpoint` and `ctx.base` members. `guard` is the one
|
|
136
|
+
* line the REPL mode adds: nothing in the filing mode can end a run part-way
|
|
137
|
+
* through a cell, because there the run ends by returning.
|
|
138
|
+
*/
|
|
139
|
+
const preludeCall = (guard) => ` call: function (flow, input, options) {
|
|
140
|
+
if (typeof flow !== "string") return Deferred.reject(new Fault("ctx.call expects a flow name as its first argument"))
|
|
141
|
+
${guard} var encoded = encode("ctx.call input", input === undefined ? null : input)
|
|
142
|
+
var at = options === null || typeof options !== "object" ? undefined : options.at
|
|
143
|
+
if (at === undefined) return dispatch(flow, encoded, "")
|
|
144
|
+
// A handle the cell never awaited is a promise, and that spelling is the
|
|
145
|
+
// one the ruling wrote: \`const cp = ctx.checkpoint()\`. The pin lands where
|
|
146
|
+
// that line is, because the queue settles in issue order, so awaiting the
|
|
147
|
+
// handle later cannot move the tree it names. Both spellings are accepted.
|
|
148
|
+
if (at !== null && typeof at === "object" && typeof at.then === "function") {
|
|
149
|
+
return at.then(function (resolved) { return dispatch(flow, encoded, encodeAt(resolved)) })
|
|
150
|
+
}
|
|
151
|
+
return dispatch(flow, encoded, encodeAt(at))
|
|
152
|
+
},
|
|
153
|
+
checkpoint: function () {
|
|
154
|
+
${guard} return pin().then(settleEnvelope)
|
|
155
|
+
},
|
|
156
|
+
base: freeze(parse(${JSON.stringify(JSON.stringify(baseHandle))})),`;
|
|
157
|
+
/**
|
|
158
|
+
* What a `ctx.call` issued after `ctx.done` or `ctx.park` resolves with.
|
|
159
|
+
*
|
|
160
|
+
* A completion takes effect where it is called, so the calls a cell would have
|
|
161
|
+
* made after it do not run. They fail the way every other refused call fails —
|
|
162
|
+
* soft, with a code and a hint — because rule 3 promises a cell that a call
|
|
163
|
+
* resolves rather than throws, and a completion is not the place to break that
|
|
164
|
+
* promise: the guard shape the contract teaches puts `ctx.done` in the middle of
|
|
165
|
+
* a cell, and a throw there would discard the rest of a frame that had already
|
|
166
|
+
* finished the run.
|
|
167
|
+
*/
|
|
168
|
+
const sealedCall = Cell.callFailure(refusal("run_completed", "This run was already completed: an earlier line of this cell called ctx.done or ctx.park, which takes effect where it is called, so no further flow call is dispatched."));
|
|
169
|
+
/**
|
|
170
|
+
* The prelude a persistent realm is opened with.
|
|
171
|
+
*
|
|
172
|
+
* It removes the two sources of nondeterminism QuickJS ships with, `Date` and
|
|
173
|
+
* `Math.random`. The raw host bridge is captured in a closure and then deleted
|
|
174
|
+
* from the global object, so a cell cannot reach the unwrapped boundary and
|
|
175
|
+
* hand it unencoded values.
|
|
176
|
+
*
|
|
177
|
+
* It differs from the per-cell prelude the filing surface used in exactly the
|
|
178
|
+
* two ways the persistent realm
|
|
179
|
+
* differs: `ctx.state` is gone, because the realm is the memory, and three new
|
|
180
|
+
* members plus `console` are installed, because a script cannot `return`.
|
|
181
|
+
*
|
|
182
|
+
* `console.log` renders each argument on the host side — a string as itself,
|
|
183
|
+
* structures through the print channel — so a structured value reaches the next model
|
|
184
|
+
* turn as the value it is rather than as `[object Object]`. A value JSON cannot
|
|
185
|
+
* walk at all, a cycle above all, is the one case where that promise cannot be
|
|
186
|
+
* kept, so it is named instead: the kind, the reason, and the fact that the
|
|
187
|
+
* value is still bound. `String(value)` there would print the exact bytes this
|
|
188
|
+
* channel exists to abolish.
|
|
189
|
+
*
|
|
190
|
+
* Both are installed as non-writable, non-configurable own properties, which the
|
|
191
|
+
* per-cell prelude never needed to do. There, a cell that declared `ctx` shadowed
|
|
192
|
+
* the name inside its own async wrapper and the next cell was handed a fresh
|
|
193
|
+
* realm. Here the wrapper is gone and `CellValidation.normalize` rewrites a
|
|
194
|
+
* top-level `const` to `var`, so the same declaration would assign over the run's
|
|
195
|
+
* only host binding and every later cell would die on
|
|
196
|
+
* `TypeError: not a function` — with `ctx` inside the panel's baseline, so
|
|
197
|
+
* nothing would even name what went missing. A `var` declaration over a
|
|
198
|
+
* non-writable global is a silent no-op in sloppy mode, which is a failure the
|
|
199
|
+
* run survives. `CellValidation` refuses the declaration in-frame as well, so
|
|
200
|
+
* the model is told rather than left to wonder.
|
|
201
|
+
*/
|
|
202
|
+
const replPrelude = (catalog) => `(function () {
|
|
203
|
+
var bridge = globalThis.__call
|
|
204
|
+
var pin = globalThis.__checkpoint
|
|
205
|
+
var print = globalThis.__print
|
|
206
|
+
var intent = globalThis.__intent
|
|
207
|
+
${preludeHelpers}
|
|
208
|
+
delete globalThis.__call
|
|
209
|
+
delete globalThis.__checkpoint
|
|
210
|
+
delete globalThis.__print
|
|
211
|
+
delete globalThis.__intent
|
|
212
|
+
delete globalThis.Date
|
|
213
|
+
delete Math.random
|
|
214
|
+
// Removed for the same reason as the two above: it makes the realm answer a
|
|
215
|
+
// question dishonestly. Every reading the panel probe takes goes through the
|
|
216
|
+
// intrinsics captured before any cell ran, and every property comes off its
|
|
217
|
+
// own descriptor, so a cell can neither rebind reflection nor hide weight
|
|
218
|
+
// behind a getter. Neither defence survives a value that IS the trap: a proxy
|
|
219
|
+
// answers \`getOwnPropertyNames\` from its handler, so one wrapping a
|
|
220
|
+
// megabyte-wide target weighs as an empty object while the realm holds the
|
|
221
|
+
// target alive, and the run's only ceiling over string data reads zero for it.
|
|
222
|
+
// No reading of a proxy can be trusted — any number it reports is a number the
|
|
223
|
+
// handler chose — and nothing else in this realm can construct one, so the
|
|
224
|
+
// constructor goes rather than the accounting. \`Reflect\` stays: it offers no
|
|
225
|
+
// way to make a proxy and the probe does not read through it.
|
|
226
|
+
delete globalThis.Proxy
|
|
227
|
+
// The seal: once this frame has said how the run ends, it has ended, and the
|
|
228
|
+
// calls a cell would have made after that line are not dispatched. It lives
|
|
229
|
+
// here rather than on the host because ctx.call has to answer synchronously,
|
|
230
|
+
// and it is cleared per frame by the function this prelude returns — a park
|
|
231
|
+
// whose reason is refused is asked again inside the same frame, and a realm
|
|
232
|
+
// still sealed from the refused attempt would answer that retry with nothing.
|
|
233
|
+
var catalog = freeze(${catalog})
|
|
234
|
+
var sealed = null
|
|
235
|
+
var sealedEnvelope = freeze(parse(${JSON.stringify(JSON.stringify(sealedCall))}))
|
|
236
|
+
var unprintable = function (value, error) {
|
|
237
|
+
var kind = isArray(value) ? "array" : typeof value
|
|
238
|
+
var why = error !== null && typeof error === "object" && error.message ? error.message : render(error)
|
|
239
|
+
return "[unprintable " + kind + ": " + why + " — the value is still bound, so print the part of it you need]"
|
|
240
|
+
}
|
|
241
|
+
var show = function (values) {
|
|
242
|
+
var parts = []
|
|
243
|
+
for (var index = 0; index < values.length; index++) {
|
|
244
|
+
var value = values[index]
|
|
245
|
+
if (typeof value === "string") parts.push({ text: value })
|
|
246
|
+
else if (value === undefined) parts.push({ text: "undefined" })
|
|
247
|
+
else {
|
|
248
|
+
var encoded = null
|
|
249
|
+
var refused = false
|
|
250
|
+
try { encoded = stringify(value) } catch (error) { refused = true; parts.push({ text: unprintable(value, error) }) }
|
|
251
|
+
// JSON answers \`undefined\` rather than throwing for a value it has no
|
|
252
|
+
// notation for at all — a function, a symbol — and those read best as
|
|
253
|
+
// themselves. It throws only for a value it cannot walk, and that is
|
|
254
|
+
// the one case where naming the reason beats printing "[object Object]".
|
|
255
|
+
if (!refused) parts.push(encoded === undefined ? { text: render(value) } : { json: parse(encoded) })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
print(stringify(parts))
|
|
259
|
+
}
|
|
260
|
+
var line = function () { show(argumentsOf.call(arguments)) }
|
|
261
|
+
var host = function (name, value) {
|
|
262
|
+
define(globalThis, name, { value: value, writable: false, enumerable: true, configurable: false })
|
|
263
|
+
}
|
|
264
|
+
host("console", freezeValue({ log: line, info: line, warn: line, error: line }))
|
|
265
|
+
host("ctx", freezeValue({
|
|
266
|
+
${preludeCall(` if (sealed !== null) return Deferred.resolve(sealed)\n`)}
|
|
267
|
+
get flows() { return catalog },
|
|
268
|
+
done: function (output) {
|
|
269
|
+
if (sealed !== null) return
|
|
270
|
+
if (arguments.length === 0) {
|
|
271
|
+
throw new Fault("ctx.done(output) takes the run's answer; call it with the value the task asked for")
|
|
272
|
+
}
|
|
273
|
+
var encoded = encode("ctx.done output", output === undefined ? null : output)
|
|
274
|
+
sealed = sealedEnvelope
|
|
275
|
+
intent("done", encoded)
|
|
276
|
+
},
|
|
277
|
+
park: function (reason, message) {
|
|
278
|
+
if (sealed !== null) return
|
|
279
|
+
var encoded = encode("ctx.park message", { reason: reason === undefined ? null : reason, message: message === undefined ? "" : message })
|
|
280
|
+
sealed = sealedEnvelope
|
|
281
|
+
intent("park", encoded)
|
|
282
|
+
},
|
|
283
|
+
justify: function (text) { intent("justify", encode("ctx.justify text", text === undefined ? "" : text)) }
|
|
284
|
+
}))
|
|
285
|
+
// Handed back to the host, which calls it as each frame opens. See the seal
|
|
286
|
+
// above for why the clearing is per frame rather than per run.
|
|
287
|
+
return function (nextCatalog) {
|
|
288
|
+
sealed = null
|
|
289
|
+
if (nextCatalog !== undefined) catalog = freeze(parse(nextCatalog))
|
|
290
|
+
}
|
|
291
|
+
})()`;
|
|
292
|
+
const Catalog = Schema.Record(Schema.String, Cell.FlowProjection);
|
|
293
|
+
const catalogOf = (flows) => {
|
|
294
|
+
const encoded = Schema.encodeSync(Catalog)(flows);
|
|
295
|
+
const normalized = Object.fromEntries(Object.entries(encoded).map(([name, projection]) => [
|
|
296
|
+
name,
|
|
297
|
+
Object.fromEntries(Object.entries(projection).flatMap(([field, value]) => Option.isOption(value)
|
|
298
|
+
? Option.isSome(value) ? [[field, value.value]] : []
|
|
299
|
+
: [[field, value]]))
|
|
300
|
+
]));
|
|
301
|
+
return JSON.stringify(normalized);
|
|
302
|
+
};
|
|
303
|
+
const raisedFrom = (dumped) => {
|
|
304
|
+
if (typeof dumped === "object" && dumped !== null) {
|
|
305
|
+
const record = dumped;
|
|
306
|
+
return new Cell.Raised({
|
|
307
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
308
|
+
// Never `String(object)`: a cell that threw a structured value is told
|
|
309
|
+
// what it threw. See `Sandbox` `describe`.
|
|
310
|
+
message: typeof record.message === "string" ? record.message : Sandbox.raisedOutcome(dumped).message
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return new Cell.Raised({ name: "Error", message: String(dumped) });
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Builds one owned QuickJS handle from the already-validated JSON result.
|
|
317
|
+
*
|
|
318
|
+
* Materializing the value directly also avoids parsing a multi-megabyte JSON
|
|
319
|
+
* string inside a promise job, which leaves QuickJS 0.32's runtime heap in an
|
|
320
|
+
* uncollectable state during disposal.
|
|
321
|
+
*/
|
|
322
|
+
const handleFromJson = (context, defineDataProperty, value, depth = 0) => {
|
|
323
|
+
if (value === null)
|
|
324
|
+
return context.null;
|
|
325
|
+
switch (typeof value) {
|
|
326
|
+
case "boolean":
|
|
327
|
+
return value ? context.true : context.false;
|
|
328
|
+
case "number":
|
|
329
|
+
return context.newNumber(value);
|
|
330
|
+
case "string":
|
|
331
|
+
return context.newString(value);
|
|
332
|
+
}
|
|
333
|
+
// Leave enough host stack to release every ancestor handle on refusal.
|
|
334
|
+
if (depth >= 128)
|
|
335
|
+
throw new Error("The flow result exceeds 128 levels of JSON nesting");
|
|
336
|
+
const container = Array.isArray(value) ? context.newArray() : context.newObject();
|
|
337
|
+
try {
|
|
338
|
+
for (const [key, item] of Object.entries(value)) {
|
|
339
|
+
const child = handleFromJson(context, defineDataProperty, item, depth + 1);
|
|
340
|
+
try {
|
|
341
|
+
const property = context.newString(key);
|
|
342
|
+
try {
|
|
343
|
+
context.unwrapResult(context.callFunction(defineDataProperty, context.undefined, container, property, child)).dispose();
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
property.dispose();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
child.dispose();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return container;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
container.dispose();
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Caches only a successful asynchronous load; a rejection may be retried.
|
|
362
|
+
*
|
|
363
|
+
* @category constructors
|
|
364
|
+
* @since 0.1.0
|
|
365
|
+
*/
|
|
366
|
+
export const cacheSuccessful = (load) => {
|
|
367
|
+
let loaded;
|
|
368
|
+
return () => {
|
|
369
|
+
if (loaded === undefined) {
|
|
370
|
+
const pending = load();
|
|
371
|
+
loaded = pending;
|
|
372
|
+
void pending.catch(() => {
|
|
373
|
+
loaded = undefined;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return loaded;
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* The QuickJS build the sandbox compiles.
|
|
381
|
+
*
|
|
382
|
+
* @category services
|
|
383
|
+
* @since 0.1.0
|
|
384
|
+
*/
|
|
385
|
+
export class Variant extends Context.Service()("@smthrs/harness/QuickJSSandbox/Variant") {
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Provides the single-file build, which Node and a browser both compile.
|
|
389
|
+
*
|
|
390
|
+
* @category layers
|
|
391
|
+
* @since 0.1.0
|
|
392
|
+
*/
|
|
393
|
+
export const layerVariantLive = Layer.succeed(Variant)({ variant: singlefile });
|
|
394
|
+
/**
|
|
395
|
+
* Provides a build the host names.
|
|
396
|
+
*
|
|
397
|
+
* A workerd host builds one with `newVariant(baseVariant, { wasmModule })` from
|
|
398
|
+
* a `.wasm` module import, because that runtime instantiates a module the
|
|
399
|
+
* toolchain compiled and refuses to compile one from bytes.
|
|
400
|
+
*
|
|
401
|
+
* @category layers
|
|
402
|
+
* @since 0.1.0
|
|
403
|
+
*/
|
|
404
|
+
export const layerVariant = (variant) => Layer.succeed(Variant)({ variant });
|
|
405
|
+
/**
|
|
406
|
+
* One compiled module per build, so two sandboxes over one variant share it.
|
|
407
|
+
*
|
|
408
|
+
* Keyed weakly: a variant a host builds per request is collectable with the
|
|
409
|
+
* module it produced.
|
|
410
|
+
*/
|
|
411
|
+
const compiled = new WeakMap();
|
|
412
|
+
const loaderFor = (variant) => {
|
|
413
|
+
const known = compiled.get(variant);
|
|
414
|
+
if (known !== undefined)
|
|
415
|
+
return known;
|
|
416
|
+
const loader = cacheSuccessful(() => newQuickJSWASMModuleFromVariant(variant));
|
|
417
|
+
compiled.set(variant, loader);
|
|
418
|
+
return loader;
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* Synchronous monotonic-enough clock required by QuickJS's interrupt callback.
|
|
422
|
+
*
|
|
423
|
+
* @category services
|
|
424
|
+
* @since 0.1.0
|
|
425
|
+
*/
|
|
426
|
+
export class ComputeClock extends Context.Service()("@smthrs/harness/QuickJSSandbox/ComputeClock") {
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Provides the browser-safe host clock behind the QuickJS clock seam.
|
|
430
|
+
*
|
|
431
|
+
* @category layers
|
|
432
|
+
* @since 0.1.0
|
|
433
|
+
*/
|
|
434
|
+
export const layerClockLive = Layer.succeed(ComputeClock)({ now: () => Date.now() });
|
|
435
|
+
const capabilities = {
|
|
436
|
+
calls: true,
|
|
437
|
+
memoryBytes: true,
|
|
438
|
+
steps: true,
|
|
439
|
+
timeMs: true
|
|
440
|
+
};
|
|
441
|
+
const timeLimitExceeded = (timeMs) => new Cell.Rejected({
|
|
442
|
+
code: "limit_exceeded",
|
|
443
|
+
message: `This cell exceeded its wall-clock limit of ${timeMs} milliseconds`
|
|
444
|
+
});
|
|
445
|
+
/**
|
|
446
|
+
* The reasons `ctx.park` may name, as the transition schema declares them.
|
|
447
|
+
*
|
|
448
|
+
* Checked on the host rather than inside the realm so a wrong one settles the
|
|
449
|
+
* frame as an `invalid_transition` the next frame is asked to fix, instead of as
|
|
450
|
+
* a throw that reads like a bug in the cell's own logic.
|
|
451
|
+
*/
|
|
452
|
+
const parkReasons = ["waiting-input", "waiting-event", "waiting-quota"];
|
|
453
|
+
const parkPayload = Schema.decodeUnknownSync(Schema.Struct({ reason: Schema.Json, message: Schema.Json }));
|
|
454
|
+
const printParts = Schema.decodeUnknownSync(Schema.Array(Schema.Union([
|
|
455
|
+
Schema.Struct({ text: Schema.String }),
|
|
456
|
+
Schema.Struct({ json: Schema.Json })
|
|
457
|
+
])));
|
|
458
|
+
/**
|
|
459
|
+
* Renders one `console.log` call as the statement it contributes.
|
|
460
|
+
*
|
|
461
|
+
* The statement is *not* bounded here beyond what the host is willing to hold:
|
|
462
|
+
* a frame's statements share one budget and the share each one gets is not known
|
|
463
|
+
* until the frame closes, so the reduction happens there. What happens here is
|
|
464
|
+
* the reduction the host needs for itself — a value larger than a whole frame's
|
|
465
|
+
* budget can never be shown whole, so only its two ends are kept, and the size
|
|
466
|
+
* it had is carried beside them so the notice at frame close names the original.
|
|
467
|
+
*
|
|
468
|
+
* The two ends are cut through `elide`, which will not split a surrogate pair.
|
|
469
|
+
* They are joined with nothing between them, so a head ending in the first half
|
|
470
|
+
* of a pair and a tail starting with the second half would fuse into a character
|
|
471
|
+
* the cell never printed.
|
|
472
|
+
*/
|
|
473
|
+
const printed = (encoded) => {
|
|
474
|
+
const parts = printParts(JSON.parse(encoded));
|
|
475
|
+
const rendered = parts.map((part) => "text" in part ? part.text : printChannel.render(part.json));
|
|
476
|
+
// A multi-line argument gets its own lines; short neighbours stay space-joined.
|
|
477
|
+
const whole = rendered.reduce((joined, part, index) => index === 0 ? part : joined + (part.includes("\n") || rendered[index - 1].includes("\n") ? "\n" : " ") + part, "");
|
|
478
|
+
const wholeBytes = bytes.size(whole);
|
|
479
|
+
if (wholeBytes <= Sandbox.printFrameBytes)
|
|
480
|
+
return { text: whole, bytes: wholeBytes };
|
|
481
|
+
const edge = Math.floor(Sandbox.printFrameBytes / 2);
|
|
482
|
+
return {
|
|
483
|
+
text: `${elide.headSlice(whole, edge)}${elide.tailSlice(whole, edge)}`,
|
|
484
|
+
bytes: wholeBytes
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* The transition a REPL frame settled, or the reason it settled none.
|
|
489
|
+
*/
|
|
490
|
+
const replOutcome = (recorded, justification) => {
|
|
491
|
+
if (recorded !== undefined && recorded.kind === "park") {
|
|
492
|
+
const reason = parkReasons.find((candidate) => candidate === recorded.reason);
|
|
493
|
+
if (reason === undefined) {
|
|
494
|
+
return new Cell.Rejected({
|
|
495
|
+
code: "invalid_transition",
|
|
496
|
+
message: `ctx.park was called with reason ${JSON.stringify(recorded.reason)}. Call it as ctx.park(reason, message) with reason one of "waiting-input", "waiting-event" or "waiting-quota".`
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return new Cell.Settled({
|
|
500
|
+
transition: Sandbox.replTransition({ _tag: "Park", reason, message: recorded.message }, justification)
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return new Cell.Settled({
|
|
504
|
+
transition: Sandbox.replTransition(recorded === undefined ? undefined : { _tag: "Done", output: recorded.output, value: recorded.value }, justification)
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* How many values one probe walks before it stops counting.
|
|
509
|
+
*
|
|
510
|
+
* The walk exists to find the string bytes a realm is holding, and strings are
|
|
511
|
+
* few and large where they matter — a file a cell read, a suite's output. The
|
|
512
|
+
* bound is what keeps a realm holding a million small objects from paying for a
|
|
513
|
+
* traversal of all of them, and it doubles as the cycle guard: a graph that
|
|
514
|
+
* points at itself spends the budget instead of the stack.
|
|
515
|
+
*/
|
|
516
|
+
const weighNodes = 200_000;
|
|
517
|
+
/**
|
|
518
|
+
* How deep one probe descends before it stops counting.
|
|
519
|
+
*
|
|
520
|
+
* A cell can bind a linked list, and the walk runs inside the realm on the
|
|
521
|
+
* realm's own stack.
|
|
522
|
+
*/
|
|
523
|
+
const weighDepth = 32;
|
|
524
|
+
/**
|
|
525
|
+
* Builds the in-realm probe that reads the variables panel and the realm's weight.
|
|
526
|
+
*
|
|
527
|
+
* The source is evaluated **once**, when the realm opens, and answers the
|
|
528
|
+
* function every later frame calls. That is the whole reason it is a factory:
|
|
529
|
+
* the intrinsics the walk needs are read here, before any cell has run, and held
|
|
530
|
+
* in the closure, so a cell that binds `Object` at its top level loses its own
|
|
531
|
+
* reflection and not the panel that would have named the binding for it. Called
|
|
532
|
+
* fresh each frame instead, the probe would read whatever `Object` a cell had
|
|
533
|
+
* left behind, and the frame after the shadowing would report an empty realm.
|
|
534
|
+
*
|
|
535
|
+
* Captured intrinsics stop a cell rebinding reflection, and reading every named
|
|
536
|
+
* property off its own descriptor stops a getter running. Neither stops a value
|
|
537
|
+
* that IS the trap, which is why `replPrelude` deletes `Proxy`: a proxy answers
|
|
538
|
+
* `getOwnPropertyNames` from its handler, so every number this walk could report
|
|
539
|
+
* for one is a number the cell chose. The constructor goes rather than the
|
|
540
|
+
* accounting, because there is no reading of a proxy the realm can be trusted to
|
|
541
|
+
* take.
|
|
542
|
+
*
|
|
543
|
+
* The estimate covers array indices, enumerable own string-keyed properties,
|
|
544
|
+
* Map keys and values, and Set values. It cannot see closure state, weak
|
|
545
|
+
* collection entries, or other storage unreachable by those paths. Functions
|
|
546
|
+
* have a fixed weight of 8 regardless of their scope. This supplements the
|
|
547
|
+
* native allocator limit; it is not a hard bound on all retained memory.
|
|
548
|
+
*
|
|
549
|
+
* The returned function declares nothing on the global object and adds no name
|
|
550
|
+
* of its own to the set it reports, because the host holds it as a handle rather
|
|
551
|
+
* than as a global. Every value's *panel* line is measured cheaply — a string's
|
|
552
|
+
* length, an array's length, an object's key count, a function's arity, a number
|
|
553
|
+
* or boolean by value — because a panel that serialized every global would cost
|
|
554
|
+
* the heap every frame, and the whole point of a realm is that the value is
|
|
555
|
+
* still there under the name the panel prints.
|
|
556
|
+
*
|
|
557
|
+
* `bytes` is a second, separate reading and it exists because QuickJS's own
|
|
558
|
+
* ceiling cannot supply it. Measured on the shipped variant: `str_count` and
|
|
559
|
+
* `str_size` stay at zero for every construction a cell can reach — `repeat`,
|
|
560
|
+
* `join`, and a string handed in by the host bridge alike — so
|
|
561
|
+
* `runtime.setMemoryLimit` refuses one allocation larger than the whole ceiling
|
|
562
|
+
* and never sees accumulation. A realm under a 128 MiB ceiling held 400 MiB of
|
|
563
|
+
* live strings across forty frames and raised nothing while the host's resident
|
|
564
|
+
* set grew by 385 MB. Under a per-cell realm that hole was bounded by one cell;
|
|
565
|
+
* under a per-run realm it compounds for the life of the run, so the run budget
|
|
566
|
+
* is measured here instead. See {@link openRealm}.
|
|
567
|
+
*/
|
|
568
|
+
const panelProbe = (baseline) => `(function (ownNames, descriptorOf, keysOf, isArray, stringify, String, mapSize, mapEntries, mapNext, setSize, setValues, setNext, skip) {
|
|
569
|
+
return function () {
|
|
570
|
+
var names = ownNames(globalThis)
|
|
571
|
+
var out = []
|
|
572
|
+
var total = 0
|
|
573
|
+
var budget = ${weighNodes}
|
|
574
|
+
var partial = false
|
|
575
|
+
var partialRoot = null
|
|
576
|
+
var partialBytes = 0
|
|
577
|
+
// The ancestors of the value being weighed, and the whole of this walk's
|
|
578
|
+
// repeat detection. A cycle is the one repeat that MUST be cut, because it
|
|
579
|
+
// never ends, and an ancestor list cuts it at the cost of the depth ceiling
|
|
580
|
+
// rather than of the whole graph. Two siblings that share one object are
|
|
581
|
+
// counted twice, which over-states such a realm: a ceiling that errs toward
|
|
582
|
+
// refusing is the safe direction, and the refusal is spent on the frame it
|
|
583
|
+
// lands in, so the next cell runs and can restructure. A Set or a WeakSet
|
|
584
|
+
// would dedupe them properly and cannot be used here: allocating either
|
|
585
|
+
// inside a realm that is at its heap ceiling, which is the realm this probe
|
|
586
|
+
// exists to weigh, leaves the runtime unable to assert itself empty at
|
|
587
|
+
// teardown and aborts the host process.
|
|
588
|
+
var path = []
|
|
589
|
+
var weigh = function (value, depth) {
|
|
590
|
+
var kind = typeof value
|
|
591
|
+
if (depth > ${weighDepth}) {
|
|
592
|
+
partial = true
|
|
593
|
+
return 0
|
|
594
|
+
}
|
|
595
|
+
if (budget <= 0) {
|
|
596
|
+
partial = true
|
|
597
|
+
return 0
|
|
598
|
+
}
|
|
599
|
+
budget = budget - 1
|
|
600
|
+
// Code units, not UTF-8 bytes, and deliberately: this walk answers "how
|
|
601
|
+
// much of the host's memory is this realm holding", and QuickJS stores a
|
|
602
|
+
// string as one or two bytes per code unit, so a unit count is the honest
|
|
603
|
+
// reading of the thing being bounded. It is also the only O(1) one, and
|
|
604
|
+
// that is load-bearing: the probe runs at frame close under the cell's own
|
|
605
|
+
// interrupt budget, so a scan of a two-hundred-megabyte realm is killed
|
|
606
|
+
// part way and a realm that cannot be weighed at all reads as weighing
|
|
607
|
+
// nothing. UTF-8 bytes are the unit of what the model is SHOWN, which is
|
|
608
|
+
// the print channel and the call ledger, not of what the realm holds.
|
|
609
|
+
if (kind === "string") return value.length
|
|
610
|
+
if (value === null || kind !== "object") return 8
|
|
611
|
+
for (var up = 0; up < path.length; up++) if (path[up] === value) return 8
|
|
612
|
+
path.push(value)
|
|
613
|
+
var sum = 8
|
|
614
|
+
// An array is walked by index so keysOf never allocates a second array as
|
|
615
|
+
// long as the first. Each transient descriptor dies with its iteration,
|
|
616
|
+
// and the same node budget that bounds the walk bounds that pressure.
|
|
617
|
+
// Reading through the descriptor is load-bearing: an index accessor is
|
|
618
|
+
// cell code, not data the memory probe may execute.
|
|
619
|
+
if (isArray(value)) {
|
|
620
|
+
var count = value.length
|
|
621
|
+
for (var item = 0; item < count && !partial; item++) {
|
|
622
|
+
var property = descriptorOf(value, String(item))
|
|
623
|
+
if (property === undefined || !("value" in property)) sum = sum + 8
|
|
624
|
+
else sum = sum + weigh(property.value, depth + 1)
|
|
625
|
+
}
|
|
626
|
+
path.pop()
|
|
627
|
+
return sum
|
|
628
|
+
}
|
|
629
|
+
// Collection entries live in internal slots, not own properties. Use
|
|
630
|
+
// captured native methods for both branding and iteration: instanceof,
|
|
631
|
+
// Symbol.iterator, and methods read from the value are cell-controlled.
|
|
632
|
+
// Catch only a failed brand check, never an incomplete entry traversal.
|
|
633
|
+
var isMap = false
|
|
634
|
+
var isSet = false
|
|
635
|
+
try {
|
|
636
|
+
mapSize(value)
|
|
637
|
+
isMap = true
|
|
638
|
+
} catch (error) {
|
|
639
|
+
try { setSize(value); isSet = true } catch (error) {}
|
|
640
|
+
}
|
|
641
|
+
if (isMap || isSet) {
|
|
642
|
+
// Iterator allocation can exhaust the heap too. Let that failure reach
|
|
643
|
+
// the outer catch so the reading is partial rather than empty.
|
|
644
|
+
var iterator = isMap ? mapEntries(value) : setValues(value)
|
|
645
|
+
while (!partial) {
|
|
646
|
+
var entry = isMap ? mapNext(iterator) : setNext(iterator)
|
|
647
|
+
if (entry.done) break
|
|
648
|
+
if (isMap) {
|
|
649
|
+
sum = sum + weigh(entry.value[0], depth + 1)
|
|
650
|
+
sum = sum + weigh(entry.value[1], depth + 1)
|
|
651
|
+
} else sum = sum + weigh(entry.value, depth + 1)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
var keys = keysOf(value)
|
|
655
|
+
for (var key = 0; key < keys.length && !partial; key++) {
|
|
656
|
+
var property = descriptorOf(value, keys[key])
|
|
657
|
+
sum = sum + keys[key].length
|
|
658
|
+
if (property === undefined || !("value" in property)) sum = sum + 8
|
|
659
|
+
else sum = sum + weigh(property.value, depth + 1)
|
|
660
|
+
}
|
|
661
|
+
path.pop()
|
|
662
|
+
return sum
|
|
663
|
+
}
|
|
664
|
+
var dataProperty = function (value, name, fallbackValue) {
|
|
665
|
+
var descriptor = descriptorOf(value, name)
|
|
666
|
+
return descriptor !== undefined && "value" in descriptor ? descriptor.value : fallbackValue
|
|
667
|
+
}
|
|
668
|
+
for (var index = 0; index < names.length; index++) {
|
|
669
|
+
var name = names[index]
|
|
670
|
+
if (skip.indexOf(name) >= 0) continue
|
|
671
|
+
var before = total
|
|
672
|
+
try {
|
|
673
|
+
var descriptor = descriptorOf(globalThis, name)
|
|
674
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
675
|
+
total = total + 8
|
|
676
|
+
out.push({ name: name, type: descriptor === undefined ? "unreadable" : "accessor", size: "", bytes: 8 })
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
var value = descriptor.value
|
|
680
|
+
var kind = typeof value
|
|
681
|
+
var bytes = weigh(value, 0)
|
|
682
|
+
total = total + bytes
|
|
683
|
+
if (value === null) out.push({ name: name, type: "null", size: "", bytes: bytes })
|
|
684
|
+
else if (kind === "undefined") out.push({ name: name, type: "unset", size: "", bytes: bytes })
|
|
685
|
+
else if (kind === "string") out.push({ name: name, type: "string", size: value.length + " chars", bytes: bytes })
|
|
686
|
+
else if (kind === "function") out.push({ name: name, type: "function", size: "arity " + dataProperty(value, "length", 0), bytes: bytes })
|
|
687
|
+
else if (isArray(value)) out.push({ name: name, type: "array", size: dataProperty(value, "length", 0) + " items", bytes: bytes })
|
|
688
|
+
else if (kind === "object") out.push({ name: name, type: "object", size: keysOf(value).length + " keys", bytes: bytes })
|
|
689
|
+
else out.push({ name: name, type: kind, size: String(value), bytes: bytes })
|
|
690
|
+
if (partial) {
|
|
691
|
+
partialRoot = name
|
|
692
|
+
partialBytes = bytes
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
// A name the probe cannot read at all: a proxy that refuses reflection,
|
|
697
|
+
// or a realm so large that walking it is itself what runs out of heap.
|
|
698
|
+
// Either way this reading is a floor and not a total, so it is reported
|
|
699
|
+
// as partial rather than weighed at nothing. No accessor is invoked to
|
|
700
|
+
// get here: every property read above goes through its own descriptor.
|
|
701
|
+
total = before + 8
|
|
702
|
+
out.push({ name: name, type: "unreadable", size: "", bytes: 8 })
|
|
703
|
+
partial = true
|
|
704
|
+
partialRoot = name
|
|
705
|
+
partialBytes = 8
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return stringify(partialRoot === null
|
|
710
|
+
? { names: out, bytes: total, partial: false }
|
|
711
|
+
: { names: out, bytes: total, partial: true, partialRoot: partialRoot, partialBytes: partialBytes })
|
|
712
|
+
}
|
|
713
|
+
})(
|
|
714
|
+
Object.getOwnPropertyNames,
|
|
715
|
+
Object.getOwnPropertyDescriptor,
|
|
716
|
+
Object.keys,
|
|
717
|
+
Array.isArray,
|
|
718
|
+
JSON.stringify,
|
|
719
|
+
String,
|
|
720
|
+
Function.prototype.call.bind(Object.getOwnPropertyDescriptor(Map.prototype, "size").get),
|
|
721
|
+
Function.prototype.call.bind(Map.prototype.entries),
|
|
722
|
+
Function.prototype.call.bind(Object.getPrototypeOf(new Map().entries()).next),
|
|
723
|
+
Function.prototype.call.bind(Object.getOwnPropertyDescriptor(Set.prototype, "size").get),
|
|
724
|
+
Function.prototype.call.bind(Set.prototype.values),
|
|
725
|
+
Function.prototype.call.bind(Object.getPrototypeOf(new Set().values()).next),
|
|
726
|
+
JSON.parse(${baseline})
|
|
727
|
+
)`;
|
|
728
|
+
/** One name as the probe reports it, before the panel drops the weight. */
|
|
729
|
+
const Weighed = Schema.Struct({
|
|
730
|
+
name: Schema.String,
|
|
731
|
+
type: Schema.String,
|
|
732
|
+
size: Schema.String,
|
|
733
|
+
bytes: Schema.Number
|
|
734
|
+
});
|
|
735
|
+
const ProbeReading = {
|
|
736
|
+
names: Schema.Array(Weighed),
|
|
737
|
+
bytes: Schema.Number
|
|
738
|
+
};
|
|
739
|
+
const decodeProbe = Schema.decodeUnknownSync(Schema.Union([
|
|
740
|
+
Schema.Struct({ ...ProbeReading, partial: Schema.Literal(false) }),
|
|
741
|
+
Schema.Struct({
|
|
742
|
+
...ProbeReading,
|
|
743
|
+
partial: Schema.Literal(true),
|
|
744
|
+
partialRoot: Schema.String,
|
|
745
|
+
partialBytes: Schema.Number
|
|
746
|
+
})
|
|
747
|
+
]));
|
|
748
|
+
/**
|
|
749
|
+
* States the run's memory ceiling in the terms the realm can act on.
|
|
750
|
+
*
|
|
751
|
+
* Written for the model, because the model is who reads it: the total, the
|
|
752
|
+
* ceiling, and the three names to reassign. A `var`-created global is
|
|
753
|
+
* non-configurable, so `delete` cannot remove one and assignment is the whole of
|
|
754
|
+
* the recovery — which is also why the refusal has to be spent where it lands.
|
|
755
|
+
* Freeing is done by a cell, and a cell that is refused cannot free anything, so
|
|
756
|
+
* a ceiling that stayed shut would ask for the one act it had just made
|
|
757
|
+
* impossible. See {@link openRealm}.
|
|
758
|
+
*/
|
|
759
|
+
const overBudget = (held, ceiling, weighed) => {
|
|
760
|
+
const heaviest = [...weighed]
|
|
761
|
+
.sort((left, right) => right.bytes - left.bytes)
|
|
762
|
+
.slice(0, 3)
|
|
763
|
+
.map((entry) => `${entry.name} (${entry.bytes})`);
|
|
764
|
+
return new Cell.Rejected({
|
|
765
|
+
code: "limit_exceeded",
|
|
766
|
+
message: `The names this realm holds weigh ${held} bytes, over this run's ceiling of ${ceiling}. ` +
|
|
767
|
+
`Nothing ran this frame, and your next cell does run: spend it freeing the largest by assigning over them — ${heaviest.join(", ")} — ` +
|
|
768
|
+
"because a name a cell bound can be reassigned but never deleted. Every other name is still bound, " +
|
|
769
|
+
"and a realm still over the ceiling after that cell is refused again."
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* States a reading the probe could not finish, in the same terms as one it could.
|
|
774
|
+
*
|
|
775
|
+
* A walk that ran out of node budget, out of depth, or out of heap stopped
|
|
776
|
+
* somewhere inside one name, so what it counted is a floor and not a total. The
|
|
777
|
+
* honest reading of a floor is "at least this much", and a ceiling that cannot
|
|
778
|
+
* be shown to be respected is a ceiling that is spent: the frame is refused
|
|
779
|
+
* exactly as an over-budget one is, and for the same recovery. Failing the other
|
|
780
|
+
* way is what let a realm holding eleven megabytes of string sit under a four
|
|
781
|
+
* megabyte ceiling, because the budget ran out before the string was reached.
|
|
782
|
+
*
|
|
783
|
+
* The name is rendered the way {@link overBudget} renders one, so a model that
|
|
784
|
+
* has read one refusal can act on the other without learning a second shape.
|
|
785
|
+
*/
|
|
786
|
+
const tooLargeToWeigh = (held, ceiling, root, bytes) => new Cell.Rejected({
|
|
787
|
+
code: "limit_exceeded",
|
|
788
|
+
message: `This realm is too large to measure: weighing it stopped inside ${root} (${bytes}) before it finished, ` +
|
|
789
|
+
`so the ${held} bytes counted are a floor and not a total, and this run's ceiling of ${ceiling} is treated as spent. ` +
|
|
790
|
+
"Nothing ran this frame, and your next cell does run: spend it freeing that name by assigning over it, " +
|
|
791
|
+
"because a name a cell bound can be reassigned but never deleted. Every other name is still bound, " +
|
|
792
|
+
"and a realm still too large to weigh after that cell is refused again."
|
|
793
|
+
});
|
|
794
|
+
/**
|
|
795
|
+
* Opens one QuickJS realm that lives for a whole run.
|
|
796
|
+
*
|
|
797
|
+
* The acquire/release is the pair the per-cell binding uses, moved up to the
|
|
798
|
+
* caller's scope: teardown is still scope finalization and cancellation is still
|
|
799
|
+
* fiber interruption, so nothing threads an abort signal. What changes is
|
|
800
|
+
* lifetime — every cell is evaluated against the same context, so a top-level
|
|
801
|
+
* declaration made in frame 3 is still bound in frame 9.
|
|
802
|
+
*
|
|
803
|
+
* Each cell is evaluated as `evalCode(text, "cell-<frame>.js", 128)`:
|
|
804
|
+
* `JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC`. That combination, measured on the
|
|
805
|
+
* shipped variant, does three things at once — top-level `await` compiles, the
|
|
806
|
+
* result is a promise the existing drive loop already knows how to poll, and
|
|
807
|
+
* top-level declarations land in the realm rather than in an async wrapper that
|
|
808
|
+
* dies with the frame. The raw numeric flag is passed because
|
|
809
|
+
* `quickjs-emscripten-core`'s options object does not spell the async flag but
|
|
810
|
+
* hands a numeric `options` straight through.
|
|
811
|
+
*
|
|
812
|
+
* The per-frame budgets survive the move unchanged, because they are counters
|
|
813
|
+
* the interrupt handler reads rather than properties of the runtime: `timeMs`
|
|
814
|
+
* and `steps` reset at each frame's start and the compute clock keeps refunding
|
|
815
|
+
* host-call duration.
|
|
816
|
+
*
|
|
817
|
+
* `memoryBytes` becomes a **run** budget, and it is enforced in two places
|
|
818
|
+
* because one of them cannot see half the heap. `runtime.setMemoryLimit` covers
|
|
819
|
+
* the object graph and refuses any single allocation larger than the whole
|
|
820
|
+
* ceiling; it does not count string data at all on the shipped variant, so
|
|
821
|
+
* accumulation across frames escapes it entirely. The panel probe supplies that
|
|
822
|
+
* half: it weighs what the realm's own names hold and a frame that opens over
|
|
823
|
+
* the ceiling is refused before it runs, with the heaviest names stated, so the
|
|
824
|
+
* next cell frees by assignment and the realm survives.
|
|
825
|
+
*
|
|
826
|
+
* The refusal is spent where it lands, and that is the load-bearing half of it.
|
|
827
|
+
* Freeing is done by a cell, so a ceiling that stayed shut once it had fired
|
|
828
|
+
* would refuse the freeing cell too, and the run would spend every remaining
|
|
829
|
+
* frame being told to do the one thing it was being prevented from doing. So
|
|
830
|
+
* the reading is cleared with the refusal: the next frame runs, the probe weighs
|
|
831
|
+
* the realm again at its close, and a realm still over the ceiling is refused
|
|
832
|
+
* again. The bound is therefore a pair of frames — a cell may allocate past the
|
|
833
|
+
* ceiling and is told at the next frame, and a run that never frees alternates
|
|
834
|
+
* between refusal and cell rather than growing every frame — which is what "the
|
|
835
|
+
* names a run accumulates" can honestly mean when the harness never drops a
|
|
836
|
+
* value behind the model's back.
|
|
837
|
+
*/
|
|
838
|
+
const openRealm = (module, options, clock) => Effect.gen(function* () {
|
|
839
|
+
const realmLimits = Sandbox.withDefaults(capabilities, options.limits);
|
|
840
|
+
/* v8 ignore next -- `withDefaults` fills `timeMs` from `defaultLimits` whenever the `timeMs` capability is declared, and this binding declares it, so the coalesce never reaches its fallback; it only discharges the optional type on `Sandbox.Limits` */
|
|
841
|
+
let timeMs = realmLimits.timeMs ?? Sandbox.defaultLimits.timeMs;
|
|
842
|
+
/* v8 ignore next -- `withDefaults` fills `memoryBytes` whenever the `memoryBytes` capability is declared, and this binding declares it, so the coalesce only discharges the optional type */
|
|
843
|
+
const memoryBytes = realmLimits.memoryBytes ?? Sandbox.defaultLimits.memoryBytes;
|
|
844
|
+
let stepBudget = realmLimits.steps;
|
|
845
|
+
let clockBase = clock.now();
|
|
846
|
+
let steps = 0;
|
|
847
|
+
let exhausted;
|
|
848
|
+
let pending = [];
|
|
849
|
+
let closing = true;
|
|
850
|
+
let ordinal = 0;
|
|
851
|
+
let boundary = { terminal: "timeout", dispatched: -1, settled: -1 };
|
|
852
|
+
let lines = [];
|
|
853
|
+
let retained = 0;
|
|
854
|
+
let unread = 0;
|
|
855
|
+
let recorded;
|
|
856
|
+
let justification;
|
|
857
|
+
const acquired = yield* Effect.acquireRelease(Effect.sync(() => {
|
|
858
|
+
const runtime = module.newRuntime();
|
|
859
|
+
/* v8 ignore else -- `withDefaults` fills `memoryBytes` whenever the `memoryBytes` capability is declared, and this binding declares it, so the heap ceiling is always set */
|
|
860
|
+
if (realmLimits.memoryBytes !== undefined) {
|
|
861
|
+
runtime.setMemoryLimit(realmLimits.memoryBytes);
|
|
862
|
+
}
|
|
863
|
+
runtime.setInterruptHandler(() => {
|
|
864
|
+
if (clock.now() - clockBase >= timeMs) {
|
|
865
|
+
exhausted = exhausted ?? timeLimitExceeded(timeMs);
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
if (stepBudget !== undefined && ++steps > stepBudget) {
|
|
869
|
+
exhausted = exhausted ?? new Cell.Rejected({
|
|
870
|
+
code: "limit_exceeded",
|
|
871
|
+
message: `This cell exceeded its limit of ${stepBudget} interpreter steps`
|
|
872
|
+
});
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
876
|
+
});
|
|
877
|
+
const context = runtime.newContext();
|
|
878
|
+
return { runtime, context };
|
|
879
|
+
}), ({ context, runtime }) => Effect.sync(() => {
|
|
880
|
+
context.dispose();
|
|
881
|
+
runtime.dispose();
|
|
882
|
+
}));
|
|
883
|
+
const { context, runtime } = acquired;
|
|
884
|
+
// Its own scoped resource rather than part of the acquire above, and that
|
|
885
|
+
// is the whole point: a helper the realm cannot evaluate used to be caught
|
|
886
|
+
// by hand, which meant a hand-written teardown and a branch no test could
|
|
887
|
+
// reach once a zero budget stopped being accepted. Registered as a second
|
|
888
|
+
// resource, the failure is ordinary: the scope closes, the finalizer above
|
|
889
|
+
// disposes the context and the runtime, and nothing leaks. Finalizers run
|
|
890
|
+
// in reverse, so this handle is still released before them.
|
|
891
|
+
const defineDataProperty = yield* Effect.acquireRelease(Effect.sync(() => context.unwrapResult(context.evalCode(`(function (defineProperty) {
|
|
892
|
+
return function (object, key, value) {
|
|
893
|
+
defineProperty(object, key, {
|
|
894
|
+
value: value,
|
|
895
|
+
writable: true,
|
|
896
|
+
enumerable: true,
|
|
897
|
+
configurable: true
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
})(Object.defineProperty)`))), (handle) => Effect.sync(() => handle.dispose()));
|
|
901
|
+
const install = (name, implementation) => {
|
|
902
|
+
const handle = context.newFunction(name, implementation);
|
|
903
|
+
context.setProp(context.global, name, handle);
|
|
904
|
+
handle.dispose();
|
|
905
|
+
};
|
|
906
|
+
const heapUsed = () => {
|
|
907
|
+
const snapshot = runtime.computeMemoryUsage();
|
|
908
|
+
try {
|
|
909
|
+
const usage = context.dump(snapshot);
|
|
910
|
+
return usage.memory_used_size;
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
snapshot.dispose();
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
const abort = (message) => {
|
|
917
|
+
closing = true;
|
|
918
|
+
for (const call of pending.splice(0))
|
|
919
|
+
call.abort(message);
|
|
920
|
+
};
|
|
921
|
+
const queue = (kind, flow, input, at) => {
|
|
922
|
+
if (closing)
|
|
923
|
+
throw new Error("The cell is closing; no further host calls are accepted");
|
|
924
|
+
const deferred = context.newPromise();
|
|
925
|
+
const reply = (payload) => {
|
|
926
|
+
const remaining = Math.max(0, memoryBytes - heapUsed());
|
|
927
|
+
// JSON byte length misses the storage for values, properties and array
|
|
928
|
+
// slots. Reserve scratch space for the bridge and conservatively price
|
|
929
|
+
// each allocation before any handle can poison the realm on failure.
|
|
930
|
+
let payloadBytes = 64 * 1024;
|
|
931
|
+
const values = [payload];
|
|
932
|
+
while (values.length > 0 && payloadBytes < remaining) {
|
|
933
|
+
const value = values.pop();
|
|
934
|
+
payloadBytes += 64;
|
|
935
|
+
if (typeof value === "string")
|
|
936
|
+
payloadBytes += bytes.size(value) * 2;
|
|
937
|
+
else if (value !== null && typeof value === "object") {
|
|
938
|
+
for (const [key, child] of Object.entries(value)) {
|
|
939
|
+
payloadBytes += 128 + bytes.size(key) * 2;
|
|
940
|
+
values.push(child);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (payloadBytes >= remaining) {
|
|
945
|
+
exhausted = exhausted ?? new Cell.Rejected({
|
|
946
|
+
code: "limit_exceeded",
|
|
947
|
+
reason: "heap",
|
|
948
|
+
message: `The flow result is estimated to need ${payloadBytes} bytes of QuickJS heap, but only ${remaining} bytes remain ` +
|
|
949
|
+
`under this run's ceiling of ${memoryBytes}. The result was not materialized.`
|
|
950
|
+
});
|
|
951
|
+
deferred.resolve(context.undefined);
|
|
952
|
+
deferred.dispose();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
let handle;
|
|
956
|
+
try {
|
|
957
|
+
handle = handleFromJson(context, defineDataProperty, payload);
|
|
958
|
+
deferred.resolve(handle);
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
// Deep JSON can exhaust the host stack while building handles even
|
|
962
|
+
// when its allocation fits. Settle the bridge after cleanup so the
|
|
963
|
+
// rejected frame remains recordable and realm disposal stays safe.
|
|
964
|
+
exhausted = new Cell.Rejected({
|
|
965
|
+
code: "limit_exceeded",
|
|
966
|
+
reason: "heap",
|
|
967
|
+
message: "Materializing the flow result exceeded the realm's limits."
|
|
968
|
+
});
|
|
969
|
+
deferred.resolve(context.undefined);
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
handle?.dispose();
|
|
973
|
+
deferred.dispose();
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
pending.push({
|
|
977
|
+
ordinal: ordinal++,
|
|
978
|
+
flow,
|
|
979
|
+
input,
|
|
980
|
+
kind,
|
|
981
|
+
...(at === undefined ? {} : { at }),
|
|
982
|
+
settle: (result) => reply(result.outcome === "success"
|
|
983
|
+
? { ok: true, value: result.value ?? null }
|
|
984
|
+
: { ok: false, aborted: false, failure: Cell.callFailure(result) }),
|
|
985
|
+
abort: (message) => reply({
|
|
986
|
+
ok: false,
|
|
987
|
+
aborted: true,
|
|
988
|
+
failure: Cell.callFailure(refusal(undefined, message))
|
|
989
|
+
})
|
|
990
|
+
});
|
|
991
|
+
return deferred.handle;
|
|
992
|
+
};
|
|
993
|
+
install("__call", (flowHandle, inputHandle, atHandle) => queue("call", context.getString(flowHandle), Schema.decodeUnknownSync(Schema.Json)(JSON.parse(context.getString(inputHandle))), decodedAt(context.getString(atHandle))));
|
|
994
|
+
install("__checkpoint", () => queue("checkpoint", checkpointFlow, null, undefined));
|
|
995
|
+
install("__print", (partsHandle) => {
|
|
996
|
+
// What the model reads is bounded at frame close; what the host holds
|
|
997
|
+
// while the cell is still running is bounded here, and it has to be a
|
|
998
|
+
// different number because the two are answers to different questions.
|
|
999
|
+
// A cell that prints in a loop hands over one payload per statement, and
|
|
1000
|
+
// every one of them is copied out of the WASM heap, parsed and decoded
|
|
1001
|
+
// before anything can decide it is surplus. Measured on this variant, a
|
|
1002
|
+
// print loop inside the default step budget took the host's resident set
|
|
1003
|
+
// from 288 MB to 746 MB, and two hundred prints of one 3 MiB string took
|
|
1004
|
+
// it to 1.4 GB — while the model, both times, was shown 16 KiB. Past the
|
|
1005
|
+
// retention ceiling the payload is not read at all: the handle belongs to
|
|
1006
|
+
// the caller, so ignoring it costs nothing, and the count of what was
|
|
1007
|
+
// ignored is stated at frame close rather than dropped in silence.
|
|
1008
|
+
if (retained >= Sandbox.printRetainedBytes) {
|
|
1009
|
+
unread = unread + 1;
|
|
1010
|
+
return context.undefined;
|
|
1011
|
+
}
|
|
1012
|
+
const line = printed(context.getString(partsHandle));
|
|
1013
|
+
retained = retained + bytes.size(line.text) + 1;
|
|
1014
|
+
lines.push(line);
|
|
1015
|
+
return context.undefined;
|
|
1016
|
+
});
|
|
1017
|
+
install("__intent", (kindHandle, payloadHandle) => {
|
|
1018
|
+
const kind = context.getString(kindHandle);
|
|
1019
|
+
const payload = Schema.decodeUnknownSync(Schema.Json)(JSON.parse(context.getString(payloadHandle)));
|
|
1020
|
+
if (kind === "done")
|
|
1021
|
+
recorded = { kind: "done", output: Cell.renderText(payload), value: payload };
|
|
1022
|
+
else if (kind === "park") {
|
|
1023
|
+
const park = parkPayload(payload);
|
|
1024
|
+
recorded = { kind: "park", reason: park.reason, message: Cell.renderText(park.message) };
|
|
1025
|
+
}
|
|
1026
|
+
else
|
|
1027
|
+
justification = Cell.renderText(payload);
|
|
1028
|
+
return context.undefined;
|
|
1029
|
+
});
|
|
1030
|
+
const installed = context.evalCode(replPrelude(catalogOf(options.flows)));
|
|
1031
|
+
if (installed.error !== undefined) {
|
|
1032
|
+
const failure = context.dump(installed.error);
|
|
1033
|
+
installed.error.dispose();
|
|
1034
|
+
return yield* new Sandbox.SandboxError({
|
|
1035
|
+
code: "runtime_failed",
|
|
1036
|
+
message: "The sandbox prelude failed to install",
|
|
1037
|
+
cause: failure
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
// What the prelude evaluated to: the function that clears the completion
|
|
1041
|
+
// seal. The host holds it as a handle rather than as a global, so no cell
|
|
1042
|
+
// can reach it and no cell can shadow it.
|
|
1043
|
+
const openFrame = installed.value;
|
|
1044
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => openFrame.dispose()));
|
|
1045
|
+
// The names a fresh realm already holds, snapshotted after the prelude so
|
|
1046
|
+
// `ctx` and `console` are part of the baseline rather than part of the
|
|
1047
|
+
// panel. Doubly encoded so no name — `__proto__` included — can reach the
|
|
1048
|
+
// probe's source as anything but data.
|
|
1049
|
+
const snapshot = context.unwrapResult(context.evalCode("JSON.stringify(Object.getOwnPropertyNames(globalThis))"));
|
|
1050
|
+
const baseline = context.getString(snapshot);
|
|
1051
|
+
snapshot.dispose();
|
|
1052
|
+
// Built once, called every frame. The handle lives on this scope beside the
|
|
1053
|
+
// context that made it, which is what keeps the probe's intrinsics the ones
|
|
1054
|
+
// a fresh realm had rather than the ones a cell left behind.
|
|
1055
|
+
const probe = context.unwrapResult(context.evalCode(panelProbe(JSON.stringify(baseline))));
|
|
1056
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => probe.dispose()));
|
|
1057
|
+
let bindings = [];
|
|
1058
|
+
let weighed = [];
|
|
1059
|
+
let held = 0;
|
|
1060
|
+
let unweighed;
|
|
1061
|
+
const evaluateFrame = (evaluation, limits) => {
|
|
1062
|
+
/* v8 ignore next -- the validated frame inherits the opening totalMs default */
|
|
1063
|
+
const totalMs = limits.totalMs ?? Sandbox.defaultLimits.totalMs;
|
|
1064
|
+
const frameStarted = clock.now();
|
|
1065
|
+
let pausedAtForFrame;
|
|
1066
|
+
let pausedMs = 0;
|
|
1067
|
+
return Effect.gen(function* () {
|
|
1068
|
+
/* v8 ignore next -- the validated frame inherits the opening timeMs default */
|
|
1069
|
+
timeMs = limits.timeMs ?? Sandbox.defaultLimits.timeMs;
|
|
1070
|
+
stepBudget = limits.steps;
|
|
1071
|
+
// Per-frame budgets and per-frame buffers, reset before anything can
|
|
1072
|
+
// settle the frame. They are counters, not properties of the runtime, so
|
|
1073
|
+
// a realm that outlives one cell still charges each cell its own — and
|
|
1074
|
+
// resetting them first is what keeps a frame that ends before its cell
|
|
1075
|
+
// runs from being handed the previous frame's print buffer.
|
|
1076
|
+
clockBase = clock.now();
|
|
1077
|
+
steps = 0;
|
|
1078
|
+
exhausted = undefined;
|
|
1079
|
+
pending = [];
|
|
1080
|
+
closing = false;
|
|
1081
|
+
ordinal = 0;
|
|
1082
|
+
boundary = { terminal: "timeout", dispatched: -1, settled: -1 };
|
|
1083
|
+
lines = [];
|
|
1084
|
+
retained = 0;
|
|
1085
|
+
unread = 0;
|
|
1086
|
+
recorded = undefined;
|
|
1087
|
+
justification = undefined;
|
|
1088
|
+
// The realm's own per-frame state: the seal a completion set. A frame
|
|
1089
|
+
// whose transition the harness refused is asked again inside the same
|
|
1090
|
+
// frame, so the retry has to open on an unsealed realm.
|
|
1091
|
+
if (evaluation.flows === undefined) {
|
|
1092
|
+
context.unwrapResult(context.callFunction(openFrame, context.undefined)).dispose();
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
const catalog = context.newString(catalogOf(evaluation.flows));
|
|
1096
|
+
try {
|
|
1097
|
+
context.unwrapResult(context.callFunction(openFrame, context.undefined, catalog)).dispose();
|
|
1098
|
+
}
|
|
1099
|
+
finally {
|
|
1100
|
+
catalog.dispose();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// Whatever the frame produced, the prints are delivered with it and the
|
|
1104
|
+
// panel is read after it, so the answer is assembled in one place. A
|
|
1105
|
+
// frame that printed past the retention ceiling says so as its last
|
|
1106
|
+
// line, because a buffer that simply stopped would read as a cell that
|
|
1107
|
+
// simply stopped printing.
|
|
1108
|
+
const frameOf = (outcome) => ({
|
|
1109
|
+
outcome,
|
|
1110
|
+
...(outcome._tag === "rejected" && outcome.code === "limit_exceeded"
|
|
1111
|
+
? { boundary: { ...boundary, terminal: "settled" } }
|
|
1112
|
+
: {}),
|
|
1113
|
+
prints: printChannel.buffer(lines, unread),
|
|
1114
|
+
bindings
|
|
1115
|
+
});
|
|
1116
|
+
// The run's memory budget, judged against what the realm's own names
|
|
1117
|
+
// weigh rather than against a heap counter that cannot see them. A
|
|
1118
|
+
// frame that opens over the ceiling runs nothing, so nothing is lost
|
|
1119
|
+
// and the recovery is one assignment — and the reading is cleared with
|
|
1120
|
+
// the refusal, because the cell that frees is a cell and has to run.
|
|
1121
|
+
// Left standing, the ceiling would refuse that cell too, and every one
|
|
1122
|
+
// after it, so the run would spend the rest of its frames being told to
|
|
1123
|
+
// do the one thing it was being prevented from doing. Cleared, the next
|
|
1124
|
+
// frame runs, the probe weighs the realm again at its close, and a realm
|
|
1125
|
+
// still over the ceiling is refused again.
|
|
1126
|
+
if (unweighed !== undefined) {
|
|
1127
|
+
const refusal = tooLargeToWeigh(held, memoryBytes, unweighed.root, unweighed.bytes);
|
|
1128
|
+
unweighed = undefined;
|
|
1129
|
+
held = 0;
|
|
1130
|
+
return frameOf(refusal);
|
|
1131
|
+
}
|
|
1132
|
+
if (held > memoryBytes) {
|
|
1133
|
+
const refusal = overBudget(held, memoryBytes, weighed);
|
|
1134
|
+
held = 0;
|
|
1135
|
+
return frameOf(refusal);
|
|
1136
|
+
}
|
|
1137
|
+
// The boundary's own parse when it has one; see `RealmEvaluation.program`.
|
|
1138
|
+
const compiled = evaluation.program ?? Sandbox.compile(evaluation.cell);
|
|
1139
|
+
if (compiled instanceof Cell.Rejected)
|
|
1140
|
+
return frameOf(compiled);
|
|
1141
|
+
const started = context.evalCode(compiled, `cell-${evaluation.frame}.js`, 128);
|
|
1142
|
+
if (started.error !== undefined) {
|
|
1143
|
+
const failure = context.dump(started.error);
|
|
1144
|
+
started.error.dispose();
|
|
1145
|
+
return frameOf(new Cell.Rejected({
|
|
1146
|
+
code: "compile_failed",
|
|
1147
|
+
/* v8 ignore next -- QuickJS reports a compile failure as an Error object, so the `message` arm is what a parser disagreement takes; `String(failure)` only discharges the `unknown` `context.dump` is typed as */
|
|
1148
|
+
message: `The cell did not compile: ${typeof failure === "object" && failure !== null && "message" in failure
|
|
1149
|
+
? String(failure.message)
|
|
1150
|
+
: String(failure)}`
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
const cellHandle = started.value;
|
|
1154
|
+
let settled;
|
|
1155
|
+
const poll = () => {
|
|
1156
|
+
// Teardown alone runs jobs once admission closes, under its own budget.
|
|
1157
|
+
if (closing)
|
|
1158
|
+
return;
|
|
1159
|
+
runtime.executePendingJobs();
|
|
1160
|
+
if (exhausted !== undefined) {
|
|
1161
|
+
settled = exhausted;
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const state = context.getPromiseState(cellHandle);
|
|
1165
|
+
if (state.type === "pending")
|
|
1166
|
+
return;
|
|
1167
|
+
if (state.type === "fulfilled") {
|
|
1168
|
+
// A script's completion value is not a transition and is never read:
|
|
1169
|
+
// a REPL cell says what it wants by calling `ctx.done` or `ctx.park`,
|
|
1170
|
+
// and saying nothing asks for another frame.
|
|
1171
|
+
state.value.dispose();
|
|
1172
|
+
settled = replOutcome(recorded, justification);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const error = context.dump(state.error);
|
|
1176
|
+
state.error.dispose();
|
|
1177
|
+
settled = raisedFrom(error);
|
|
1178
|
+
};
|
|
1179
|
+
const outcome = yield* Sandbox.driveCell({
|
|
1180
|
+
pending,
|
|
1181
|
+
replay: evaluation.replay,
|
|
1182
|
+
progress: (dispatched, settled) => {
|
|
1183
|
+
boundary = { terminal: "timeout", dispatched, settled };
|
|
1184
|
+
},
|
|
1185
|
+
...(evaluation.mint === undefined
|
|
1186
|
+
? {}
|
|
1187
|
+
: {
|
|
1188
|
+
mint: (mint) => Effect.suspend(() => {
|
|
1189
|
+
const pausedAt = clock.now();
|
|
1190
|
+
return evaluation.mint(mint).pipe(Effect.onExit(() => Effect.sync(() => {
|
|
1191
|
+
clockBase += clock.now() - pausedAt;
|
|
1192
|
+
})));
|
|
1193
|
+
})
|
|
1194
|
+
}),
|
|
1195
|
+
...(evaluation.bounded === undefined ? {} : { bounded: evaluation.bounded }),
|
|
1196
|
+
flush: () => poll(),
|
|
1197
|
+
finished: () => {
|
|
1198
|
+
poll();
|
|
1199
|
+
if (settled !== undefined)
|
|
1200
|
+
return settled;
|
|
1201
|
+
if (pending.length > 0)
|
|
1202
|
+
return undefined;
|
|
1203
|
+
return new Cell.Rejected({
|
|
1204
|
+
code: "stalled",
|
|
1205
|
+
message: "The cell awaited something that never settles. Inside a cell the only thing worth awaiting is ctx.call."
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
abort,
|
|
1209
|
+
handler: (call) => Effect.suspend(() => {
|
|
1210
|
+
const pausedAt = clock.now();
|
|
1211
|
+
const pausesFrame = limits.pauseTotalMsFor?.includes(call.flow) === true;
|
|
1212
|
+
if (pausesFrame)
|
|
1213
|
+
pausedAtForFrame = pausedAt;
|
|
1214
|
+
return evaluation.call(call).pipe(Effect.onExit(() => Effect.sync(() => {
|
|
1215
|
+
clockBase += clock.now() - pausedAt;
|
|
1216
|
+
if (pausesFrame) {
|
|
1217
|
+
pausedMs += clock.now() - pausedAt;
|
|
1218
|
+
pausedAtForFrame = undefined;
|
|
1219
|
+
}
|
|
1220
|
+
})));
|
|
1221
|
+
}),
|
|
1222
|
+
limits
|
|
1223
|
+
}).pipe(
|
|
1224
|
+
// Closing is irreversible within this frame. Rejecting a bridge
|
|
1225
|
+
// resumes user catch/finally blocks, so admission must close before
|
|
1226
|
+
// any of those jobs can allocate another host-owned promise handle.
|
|
1227
|
+
Effect.ensuring(Effect.sync(() => {
|
|
1228
|
+
closing = true;
|
|
1229
|
+
try {
|
|
1230
|
+
// A cleanup may keep scheduling promise jobs even without host
|
|
1231
|
+
// calls. Bound that work independently of the compute clock.
|
|
1232
|
+
for (let jobs = 0;; jobs++) {
|
|
1233
|
+
abort("The cell was interrupted");
|
|
1234
|
+
if (jobs >= 1024 || !runtime.hasPendingJob())
|
|
1235
|
+
break;
|
|
1236
|
+
runtime.executePendingJobs(1).dispose();
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
finally {
|
|
1240
|
+
cellHandle.dispose();
|
|
1241
|
+
}
|
|
1242
|
+
})));
|
|
1243
|
+
if (evaluation.replay !== undefined) {
|
|
1244
|
+
// A timed-out attempt never reached the probe below. Keep its realm
|
|
1245
|
+
// accounting too, so the next frame sees the same memory admission.
|
|
1246
|
+
return { outcome: evaluation.replay.outcome, prints: "", bindings, boundary: evaluation.replay.boundary };
|
|
1247
|
+
}
|
|
1248
|
+
// The panel, read from the realm the cell just ran in. It shares the
|
|
1249
|
+
// frame's remaining budget, so a cell that spent all of its own leaves
|
|
1250
|
+
// the previous reading standing rather than an empty one. What a cell
|
|
1251
|
+
// cannot do is take the reading away: the probe holds its own
|
|
1252
|
+
// intrinsics from before any cell ran, so a realm whose `Object` a cell
|
|
1253
|
+
// has rebound is still weighed and still named.
|
|
1254
|
+
const read = context.callFunction(probe, context.undefined);
|
|
1255
|
+
if (read.error === undefined) {
|
|
1256
|
+
const measured = decodeProbe(JSON.parse(context.getString(read.value)));
|
|
1257
|
+
read.value.dispose();
|
|
1258
|
+
weighed = measured.names;
|
|
1259
|
+
held = measured.bytes;
|
|
1260
|
+
unweighed = measured.partial ? { root: measured.partialRoot, bytes: measured.partialBytes } : undefined;
|
|
1261
|
+
bindings = measured.names.map((entry) => new VariablesPanel.Binding({ name: entry.name, type: entry.type, size: entry.size }));
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
read.error.dispose();
|
|
1265
|
+
}
|
|
1266
|
+
return frameOf(outcome);
|
|
1267
|
+
}).pipe(limits.pauseTotalMsFor?.length && evaluation.replay === undefined
|
|
1268
|
+
? Effect.raceFirst(Effect.gen(function* () {
|
|
1269
|
+
for (;;) {
|
|
1270
|
+
const now = clock.now();
|
|
1271
|
+
const elapsed = now - frameStarted - pausedMs -
|
|
1272
|
+
(pausedAtForFrame === undefined ? 0 : now - pausedAtForFrame);
|
|
1273
|
+
const remaining = totalMs - elapsed;
|
|
1274
|
+
if (remaining <= 0) {
|
|
1275
|
+
return {
|
|
1276
|
+
outcome: timeLimitExceeded(totalMs),
|
|
1277
|
+
boundary,
|
|
1278
|
+
prints: "",
|
|
1279
|
+
bindings
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
yield* Effect.sleep(Duration.millis(Math.max(1, remaining)));
|
|
1283
|
+
}
|
|
1284
|
+
}))
|
|
1285
|
+
: Effect.timeoutOrElse({
|
|
1286
|
+
// Replay is stopped by its recorded boundary, not a second clock.
|
|
1287
|
+
duration: evaluation.replay === undefined ? totalMs : Infinity,
|
|
1288
|
+
orElse: () => Effect.succeed({
|
|
1289
|
+
outcome: timeLimitExceeded(totalMs),
|
|
1290
|
+
boundary,
|
|
1291
|
+
prints: "",
|
|
1292
|
+
bindings
|
|
1293
|
+
})
|
|
1294
|
+
}));
|
|
1295
|
+
};
|
|
1296
|
+
const evaluate = (evaluation) => Sandbox.evaluationLimits(realmLimits, evaluation.limits).pipe(Effect.flatMap((limits) => evaluateFrame(evaluation, limits)));
|
|
1297
|
+
return { evaluate };
|
|
1298
|
+
});
|
|
1299
|
+
/**
|
|
1300
|
+
* Loads a QuickJS module through the sandbox's typed failure boundary.
|
|
1301
|
+
*
|
|
1302
|
+
* @category constructors
|
|
1303
|
+
* @since 0.1.0
|
|
1304
|
+
*/
|
|
1305
|
+
export const loadModule = (loader) => Effect.tryPromise({
|
|
1306
|
+
try: loader,
|
|
1307
|
+
catch: (cause) => new Sandbox.SandboxError({
|
|
1308
|
+
code: "runtime_failed",
|
|
1309
|
+
message: "QuickJS WebAssembly module could not be loaded",
|
|
1310
|
+
cause
|
|
1311
|
+
})
|
|
1312
|
+
});
|
|
1313
|
+
/**
|
|
1314
|
+
* Constructs the QuickJS sandbox over the build the host names, compiling the
|
|
1315
|
+
* WebAssembly module once.
|
|
1316
|
+
*
|
|
1317
|
+
* @category constructors
|
|
1318
|
+
* @since 0.1.0
|
|
1319
|
+
*/
|
|
1320
|
+
export const makeWithVariant = Effect
|
|
1321
|
+
.gen(function* () {
|
|
1322
|
+
const clock = yield* ComputeClock;
|
|
1323
|
+
const { variant } = yield* Variant;
|
|
1324
|
+
const module = yield* loadModule(loaderFor(variant));
|
|
1325
|
+
return Sandbox.make({
|
|
1326
|
+
capabilities,
|
|
1327
|
+
openRealm: (options) => openRealm(module, options, clock)
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
/**
|
|
1331
|
+
* Constructs the QuickJS sandbox over the single-file build.
|
|
1332
|
+
*
|
|
1333
|
+
* The build is fixed here rather than left in the requirements, so the clock
|
|
1334
|
+
* stays the only seam this constructor asks its caller to fill. A host that
|
|
1335
|
+
* names its own build uses {@link makeWithVariant}.
|
|
1336
|
+
*
|
|
1337
|
+
* @category constructors
|
|
1338
|
+
* @since 0.1.0
|
|
1339
|
+
* @slop
|
|
1340
|
+
*/
|
|
1341
|
+
export const makeWithClock = makeWithVariant.pipe(Effect.provide(layerVariantLive));
|
|
1342
|
+
/**
|
|
1343
|
+
* Constructs the QuickJS sandbox with the live clock layer.
|
|
1344
|
+
*
|
|
1345
|
+
* @category constructors
|
|
1346
|
+
* @since 0.1.0
|
|
1347
|
+
*/
|
|
1348
|
+
export const make = makeWithClock.pipe(Effect.provide(layerClockLive));
|
|
1349
|
+
/**
|
|
1350
|
+
* Provides the QuickJS sandbox over the build the host names.
|
|
1351
|
+
*
|
|
1352
|
+
* @category layers
|
|
1353
|
+
* @since 0.1.0
|
|
1354
|
+
*/
|
|
1355
|
+
export const layerWithVariant = Layer.effect(Sandbox.Sandbox)(makeWithVariant.pipe(Effect.provide(layerClockLive)));
|
|
1356
|
+
/**
|
|
1357
|
+
* Provides the QuickJS sandbox over the single-file build.
|
|
1358
|
+
*
|
|
1359
|
+
* @category layers
|
|
1360
|
+
* @since 0.1.0
|
|
1361
|
+
* @slop
|
|
1362
|
+
*/
|
|
1363
|
+
export const layer = Layer.effect(Sandbox.Sandbox)(make);
|
|
1364
|
+
//# sourceMappingURL=QuickJSSandbox.js.map
|