@smthrs/control 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 +194 -0
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/cjs/ApprovalAuthority.d.ts +73 -0
- package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
- package/dist/cjs/ApprovalAuthority.js +62 -0
- package/dist/cjs/ApprovalAuthority.js.map +7 -0
- package/dist/cjs/Cancellation.d.ts +107 -0
- package/dist/cjs/Cancellation.d.ts.map +1 -0
- package/dist/cjs/Cancellation.js +72 -0
- package/dist/cjs/Cancellation.js.map +7 -0
- package/dist/cjs/Channels.d.ts +170 -0
- package/dist/cjs/Channels.d.ts.map +1 -0
- package/dist/cjs/Channels.js +278 -0
- package/dist/cjs/Channels.js.map +7 -0
- package/dist/cjs/Control.d.ts +202 -0
- package/dist/cjs/Control.d.ts.map +1 -0
- package/dist/cjs/Control.js +47 -0
- package/dist/cjs/Control.js.map +7 -0
- package/dist/cjs/ControlClient.d.ts +52 -0
- package/dist/cjs/ControlClient.d.ts.map +1 -0
- package/dist/cjs/ControlClient.js +191 -0
- package/dist/cjs/ControlClient.js.map +7 -0
- package/dist/cjs/ControlError.d.ts +318 -0
- package/dist/cjs/ControlError.d.ts.map +1 -0
- package/dist/cjs/ControlError.js +249 -0
- package/dist/cjs/ControlError.js.map +7 -0
- package/dist/cjs/ControlExecutor.d.ts +372 -0
- package/dist/cjs/ControlExecutor.d.ts.map +1 -0
- package/dist/cjs/ControlExecutor.js +123 -0
- package/dist/cjs/ControlExecutor.js.map +7 -0
- package/dist/cjs/ControlFacts.d.ts +454 -0
- package/dist/cjs/ControlFacts.d.ts.map +1 -0
- package/dist/cjs/ControlFacts.js +261 -0
- package/dist/cjs/ControlFacts.js.map +7 -0
- package/dist/cjs/ControlLive.d.ts +23 -0
- package/dist/cjs/ControlLive.d.ts.map +1 -0
- package/dist/cjs/ControlLive.js +1293 -0
- package/dist/cjs/ControlLive.js.map +7 -0
- package/dist/cjs/ControlRpcs.d.ts +1204 -0
- package/dist/cjs/ControlRpcs.d.ts.map +1 -0
- package/dist/cjs/ControlRpcs.js +247 -0
- package/dist/cjs/ControlRpcs.js.map +7 -0
- package/dist/cjs/ControlRuntime.d.ts +635 -0
- package/dist/cjs/ControlRuntime.d.ts.map +1 -0
- package/dist/cjs/ControlRuntime.js +744 -0
- package/dist/cjs/ControlRuntime.js.map +7 -0
- package/dist/cjs/ControlSchema.d.ts +2642 -0
- package/dist/cjs/ControlSchema.d.ts.map +1 -0
- package/dist/cjs/ControlSchema.js +634 -0
- package/dist/cjs/ControlSchema.js.map +7 -0
- package/dist/cjs/ControlServer.d.ts +51 -0
- package/dist/cjs/ControlServer.d.ts.map +1 -0
- package/dist/cjs/ControlServer.js +121 -0
- package/dist/cjs/ControlServer.js.map +7 -0
- package/dist/cjs/Credential.d.ts +136 -0
- package/dist/cjs/Credential.d.ts.map +1 -0
- package/dist/cjs/Credential.js +168 -0
- package/dist/cjs/Credential.js.map +7 -0
- package/dist/cjs/CredentialCipher.d.ts +90 -0
- package/dist/cjs/CredentialCipher.d.ts.map +1 -0
- package/dist/cjs/CredentialCipher.js +45 -0
- package/dist/cjs/CredentialCipher.js.map +7 -0
- package/dist/cjs/CredentialStore.d.ts +97 -0
- package/dist/cjs/CredentialStore.d.ts.map +1 -0
- package/dist/cjs/CredentialStore.js +81 -0
- package/dist/cjs/CredentialStore.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +112 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +45 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Health.d.ts +333 -0
- package/dist/cjs/Health.d.ts.map +1 -0
- package/dist/cjs/Health.js +311 -0
- package/dist/cjs/Health.js.map +7 -0
- package/dist/cjs/JevSessionChecker.d.ts +57 -0
- package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
- package/dist/cjs/JevSessionChecker.js +113 -0
- package/dist/cjs/JevSessionChecker.js.map +7 -0
- package/dist/cjs/Lineage.d.ts +131 -0
- package/dist/cjs/Lineage.d.ts.map +1 -0
- package/dist/cjs/Lineage.js +81 -0
- package/dist/cjs/Lineage.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +34 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +60 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/Monitor.d.ts +282 -0
- package/dist/cjs/Monitor.d.ts.map +1 -0
- package/dist/cjs/Monitor.js +283 -0
- package/dist/cjs/Monitor.js.map +7 -0
- package/dist/cjs/ScopedToken.d.ts +193 -0
- package/dist/cjs/ScopedToken.d.ts.map +1 -0
- package/dist/cjs/ScopedToken.js +135 -0
- package/dist/cjs/ScopedToken.js.map +7 -0
- package/dist/cjs/SqlControlRuntime.d.ts +161 -0
- package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
- package/dist/cjs/SqlControlRuntime.js +1522 -0
- package/dist/cjs/SqlControlRuntime.js.map +7 -0
- package/dist/cjs/SqlCredentialStore.d.ts +43 -0
- package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
- package/dist/cjs/SqlCredentialStore.js +113 -0
- package/dist/cjs/SqlCredentialStore.js.map +7 -0
- package/dist/cjs/Steering.d.ts +69 -0
- package/dist/cjs/Steering.d.ts.map +1 -0
- package/dist/cjs/Steering.js +49 -0
- package/dist/cjs/Steering.js.map +7 -0
- package/dist/cjs/SystemFlows.d.ts +223 -0
- package/dist/cjs/SystemFlows.d.ts.map +1 -0
- package/dist/cjs/SystemFlows.js +195 -0
- package/dist/cjs/SystemFlows.js.map +7 -0
- package/dist/cjs/WebCryptoCipher.d.ts +49 -0
- package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
- package/dist/cjs/WebCryptoCipher.js +129 -0
- package/dist/cjs/WebCryptoCipher.js.map +7 -0
- package/dist/cjs/WebhookChannel.d.ts +113 -0
- package/dist/cjs/WebhookChannel.d.ts.map +1 -0
- package/dist/cjs/WebhookChannel.js +98 -0
- package/dist/cjs/WebhookChannel.js.map +7 -0
- package/dist/cjs/index.d.ts +160 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +91 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
- package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/MutationBoundary.js +50 -0
- package/dist/cjs/internal/MutationBoundary.js.map +7 -0
- package/dist/cjs/internal/activeFibers.d.ts +12 -0
- package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
- package/dist/cjs/internal/activeFibers.js +30 -0
- package/dist/cjs/internal/activeFibers.js.map +7 -0
- package/dist/cjs/internal/issues.d.ts +28 -0
- package/dist/cjs/internal/issues.d.ts.map +1 -0
- package/dist/cjs/internal/issues.js +34 -0
- package/dist/cjs/internal/issues.js.map +7 -0
- package/dist/cjs/internal/planning.d.ts +347 -0
- package/dist/cjs/internal/planning.d.ts.map +1 -0
- package/dist/cjs/internal/planning.js +137 -0
- package/dist/cjs/internal/planning.js.map +7 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
- package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_control_tables.js +115 -0
- package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_run_keys.js +44 -0
- package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_signal_commands.js +50 -0
- package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
- package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_signal_principals.js +45 -0
- package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_run_principals.js +49 -0
- package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_resume_consent.js +46 -0
- package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestControl.d.ts +19 -0
- package/dist/cjs/test/TestControl.d.ts.map +1 -0
- package/dist/cjs/test/TestControl.js +62 -0
- package/dist/cjs/test/TestControl.js.map +7 -0
- package/dist/esm/ApprovalAuthority.d.ts +73 -0
- package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
- package/dist/esm/ApprovalAuthority.js +72 -0
- package/dist/esm/ApprovalAuthority.js.map +1 -0
- package/dist/esm/Cancellation.d.ts +107 -0
- package/dist/esm/Cancellation.d.ts.map +1 -0
- package/dist/esm/Cancellation.js +116 -0
- package/dist/esm/Cancellation.js.map +1 -0
- package/dist/esm/Channels.d.ts +170 -0
- package/dist/esm/Channels.d.ts.map +1 -0
- package/dist/esm/Channels.js +312 -0
- package/dist/esm/Channels.js.map +1 -0
- package/dist/esm/Control.d.ts +202 -0
- package/dist/esm/Control.d.ts.map +1 -0
- package/dist/esm/Control.js +42 -0
- package/dist/esm/Control.js.map +1 -0
- package/dist/esm/ControlClient.d.ts +52 -0
- package/dist/esm/ControlClient.d.ts.map +1 -0
- package/dist/esm/ControlClient.js +217 -0
- package/dist/esm/ControlClient.js.map +1 -0
- package/dist/esm/ControlError.d.ts +318 -0
- package/dist/esm/ControlError.d.ts.map +1 -0
- package/dist/esm/ControlError.js +359 -0
- package/dist/esm/ControlError.js.map +1 -0
- package/dist/esm/ControlExecutor.d.ts +372 -0
- package/dist/esm/ControlExecutor.d.ts.map +1 -0
- package/dist/esm/ControlExecutor.js +212 -0
- package/dist/esm/ControlExecutor.js.map +1 -0
- package/dist/esm/ControlFacts.d.ts +454 -0
- package/dist/esm/ControlFacts.d.ts.map +1 -0
- package/dist/esm/ControlFacts.js +324 -0
- package/dist/esm/ControlFacts.js.map +1 -0
- package/dist/esm/ControlLive.d.ts +23 -0
- package/dist/esm/ControlLive.d.ts.map +1 -0
- package/dist/esm/ControlLive.js +1589 -0
- package/dist/esm/ControlLive.js.map +1 -0
- package/dist/esm/ControlRpcs.d.ts +1204 -0
- package/dist/esm/ControlRpcs.d.ts.map +1 -0
- package/dist/esm/ControlRpcs.js +299 -0
- package/dist/esm/ControlRpcs.js.map +1 -0
- package/dist/esm/ControlRuntime.d.ts +635 -0
- package/dist/esm/ControlRuntime.d.ts.map +1 -0
- package/dist/esm/ControlRuntime.js +808 -0
- package/dist/esm/ControlRuntime.js.map +1 -0
- package/dist/esm/ControlSchema.d.ts +2642 -0
- package/dist/esm/ControlSchema.d.ts.map +1 -0
- package/dist/esm/ControlSchema.js +1030 -0
- package/dist/esm/ControlSchema.js.map +1 -0
- package/dist/esm/ControlServer.d.ts +51 -0
- package/dist/esm/ControlServer.d.ts.map +1 -0
- package/dist/esm/ControlServer.js +145 -0
- package/dist/esm/ControlServer.js.map +1 -0
- package/dist/esm/Credential.d.ts +136 -0
- package/dist/esm/Credential.d.ts.map +1 -0
- package/dist/esm/Credential.js +190 -0
- package/dist/esm/Credential.js.map +1 -0
- package/dist/esm/CredentialCipher.d.ts +90 -0
- package/dist/esm/CredentialCipher.d.ts.map +1 -0
- package/dist/esm/CredentialCipher.js +56 -0
- package/dist/esm/CredentialCipher.js.map +1 -0
- package/dist/esm/CredentialStore.d.ts +97 -0
- package/dist/esm/CredentialStore.d.ts.map +1 -0
- package/dist/esm/CredentialStore.js +101 -0
- package/dist/esm/CredentialStore.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +112 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +76 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Health.d.ts +333 -0
- package/dist/esm/Health.d.ts.map +1 -0
- package/dist/esm/Health.js +400 -0
- package/dist/esm/Health.js.map +1 -0
- package/dist/esm/JevSessionChecker.d.ts +57 -0
- package/dist/esm/JevSessionChecker.d.ts.map +1 -0
- package/dist/esm/JevSessionChecker.js +108 -0
- package/dist/esm/JevSessionChecker.js.map +1 -0
- package/dist/esm/Lineage.d.ts +131 -0
- package/dist/esm/Lineage.d.ts.map +1 -0
- package/dist/esm/Lineage.js +174 -0
- package/dist/esm/Lineage.js.map +1 -0
- package/dist/esm/Migrations.d.ts +34 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +53 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/Monitor.d.ts +282 -0
- package/dist/esm/Monitor.d.ts.map +1 -0
- package/dist/esm/Monitor.js +415 -0
- package/dist/esm/Monitor.js.map +1 -0
- package/dist/esm/ScopedToken.d.ts +193 -0
- package/dist/esm/ScopedToken.d.ts.map +1 -0
- package/dist/esm/ScopedToken.js +224 -0
- package/dist/esm/ScopedToken.js.map +1 -0
- package/dist/esm/SqlControlRuntime.d.ts +161 -0
- package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
- package/dist/esm/SqlControlRuntime.js +1756 -0
- package/dist/esm/SqlControlRuntime.js.map +1 -0
- package/dist/esm/SqlCredentialStore.d.ts +43 -0
- package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
- package/dist/esm/SqlCredentialStore.js +97 -0
- package/dist/esm/SqlCredentialStore.js.map +1 -0
- package/dist/esm/Steering.d.ts +69 -0
- package/dist/esm/Steering.d.ts.map +1 -0
- package/dist/esm/Steering.js +89 -0
- package/dist/esm/Steering.js.map +1 -0
- package/dist/esm/SystemFlows.d.ts +223 -0
- package/dist/esm/SystemFlows.d.ts.map +1 -0
- package/dist/esm/SystemFlows.js +198 -0
- package/dist/esm/SystemFlows.js.map +1 -0
- package/dist/esm/WebCryptoCipher.d.ts +49 -0
- package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
- package/dist/esm/WebCryptoCipher.js +123 -0
- package/dist/esm/WebCryptoCipher.js.map +1 -0
- package/dist/esm/WebhookChannel.d.ts +113 -0
- package/dist/esm/WebhookChannel.d.ts.map +1 -0
- package/dist/esm/WebhookChannel.js +109 -0
- package/dist/esm/WebhookChannel.js.map +1 -0
- package/dist/esm/index.d.ts +160 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +160 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/MutationBoundary.d.ts +27 -0
- package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/esm/internal/MutationBoundary.js +40 -0
- package/dist/esm/internal/MutationBoundary.js.map +1 -0
- package/dist/esm/internal/activeFibers.d.ts +12 -0
- package/dist/esm/internal/activeFibers.d.ts.map +1 -0
- package/dist/esm/internal/activeFibers.js +17 -0
- package/dist/esm/internal/activeFibers.js.map +1 -0
- package/dist/esm/internal/issues.d.ts +28 -0
- package/dist/esm/internal/issues.d.ts.map +1 -0
- package/dist/esm/internal/issues.js +35 -0
- package/dist/esm/internal/issues.js.map +1 -0
- package/dist/esm/internal/planning.d.ts +347 -0
- package/dist/esm/internal/planning.d.ts.map +1 -0
- package/dist/esm/internal/planning.js +199 -0
- package/dist/esm/internal/planning.js.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.js +38 -0
- package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
- package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
- package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/esm/migrations/0001_control_tables.js +96 -0
- package/dist/esm/migrations/0001_control_tables.js.map +1 -0
- package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
- package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/esm/migrations/0002_run_keys.js +22 -0
- package/dist/esm/migrations/0002_run_keys.js.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.js +26 -0
- package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.js +25 -0
- package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.js +27 -0
- package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
- package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
- package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0006_run_principals.js +30 -0
- package/dist/esm/migrations/0006_run_principals.js.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.js +27 -0
- package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
- package/dist/esm/test/TestControl.d.ts +19 -0
- package/dist/esm/test/TestControl.d.ts.map +1 -0
- package/dist/esm/test/TestControl.js +30 -0
- package/dist/esm/test/TestControl.js.map +1 -0
- package/docs/README.md +189 -0
- package/docs/api.md +982 -0
- package/docs/concepts/authority.md +109 -0
- package/docs/concepts/cancellation.md +129 -0
- package/docs/concepts/lineage.md +132 -0
- package/docs/concepts/ownership.md +139 -0
- package/docs/concepts/projections.md +203 -0
- package/docs/concepts/receipts.md +128 -0
- package/docs/guides/approvals.md +284 -0
- package/docs/guides/cancel-and-resume.md +162 -0
- package/docs/guides/durable-storage.md +147 -0
- package/docs/guides/implement-an-executor.md +173 -0
- package/docs/guides/ingest-a-webhook.md +177 -0
- package/docs/guides/list-runs.md +160 -0
- package/docs/guides/monitor-runs.md +176 -0
- package/docs/guides/observe-health.md +147 -0
- package/docs/guides/postgres-tests.md +7 -0
- package/docs/guides/serve-over-rpc.md +220 -0
- package/docs/guides/signal-a-run.md +53 -0
- package/docs/guides/steer-a-run.md +138 -0
- package/docs/guides/store-credentials.md +164 -0
- package/docs/guides/testing.md +139 -0
- package/docs/guides/watch-a-run.md +154 -0
- package/docs/installation.md +106 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +208 -0
- package/package.json +405 -3
- package/src/ApprovalAuthority.ts +114 -0
- package/src/Cancellation.ts +172 -0
- package/src/Channels.ts +493 -0
- package/src/Control.ts +337 -0
- package/src/ControlClient.ts +319 -0
- package/src/ControlError.ts +378 -0
- package/src/ControlExecutor.ts +490 -0
- package/src/ControlFacts.ts +383 -0
- package/src/ControlLive.ts +2127 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1601 -0
- package/src/ControlSchema.ts +1380 -0
- package/src/ControlServer.ts +182 -0
- package/src/Credential.ts +310 -0
- package/src/CredentialCipher.ts +110 -0
- package/src/CredentialStore.ts +152 -0
- package/src/DispatchReader.ts +122 -0
- package/src/Health.ts +591 -0
- package/src/JevSessionChecker.ts +127 -0
- package/src/Lineage.ts +203 -0
- package/src/Migrations.ts +56 -0
- package/src/Monitor.ts +600 -0
- package/src/ScopedToken.ts +306 -0
- package/src/SqlControlRuntime.ts +2478 -0
- package/src/SqlCredentialStore.ts +148 -0
- package/src/Steering.ts +96 -0
- package/src/SystemFlows.ts +225 -0
- package/src/WebCryptoCipher.ts +169 -0
- package/src/WebhookChannel.ts +166 -0
- package/src/index.ts +188 -0
- package/src/internal/MutationBoundary.ts +46 -0
- package/src/internal/activeFibers.ts +22 -0
- package/src/internal/issues.ts +40 -0
- package/src/internal/planning.ts +262 -0
- package/src/internal/sqlSchemaErrors.ts +37 -0
- package/src/migrations/0001_control_tables.ts +99 -0
- package/src/migrations/0002_run_keys.ts +23 -0
- package/src/migrations/0003_signal_commands.ts +27 -0
- package/src/migrations/0004_approval_decisions.ts +25 -0
- package/src/migrations/0005_signal_principals.ts +27 -0
- package/src/migrations/0006_run_principals.ts +31 -0
- package/src/migrations/0007_resume_consent.ts +28 -0
- package/src/test/TestControl.ts +47 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Cancel a run, and restart one"
|
|
3
|
+
description: "Stop a run you may not own and restart one nobody is driving: what each receipt means, why both verbs read terminality first, and how a cancel reaches a run in another process."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`cancel` and `resume` are the two lifecycle verbs, and they share a shape:
|
|
9
|
+
a run id, a caller-stated `reason`, and an idempotency key. Both record who
|
|
10
|
+
asked, and both read the run's terminality before anything else.
|
|
11
|
+
|
|
12
|
+
## Cancel a run
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Control } from "@smthrs/control/Control"
|
|
16
|
+
import * as Effect from "effect/Effect"
|
|
17
|
+
|
|
18
|
+
const cancel = Effect.gen(function*() {
|
|
19
|
+
const control = yield* Control
|
|
20
|
+
return yield* control.cancel({
|
|
21
|
+
runId: "run-17",
|
|
22
|
+
reason: "budget",
|
|
23
|
+
idempotencyKey: "cancel:run-17"
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `reason` is free text and it is recorded on the
|
|
29
|
+
`control.run.cancel-requested` entry the mutation writes, then projected back
|
|
30
|
+
onto `RunSummary.cancellation`. An operator reading a cancelled run a week
|
|
31
|
+
later asks "why", and a control plane that never carried the answer cannot
|
|
32
|
+
produce one afterwards.
|
|
33
|
+
|
|
34
|
+
### What comes back
|
|
35
|
+
|
|
36
|
+
| Receipt | Meaning |
|
|
37
|
+
| -------------------------------- | ---------------------------------------------------------- |
|
|
38
|
+
| `Terminal` with the run's status | The run settled, either before this call or because of it. |
|
|
39
|
+
| `Accepted` | The request is durable and a live peer will act on it. |
|
|
40
|
+
|
|
41
|
+
A cancel that this process could interrupt answers `Terminal` naming
|
|
42
|
+
`cancelled`, because the run really did settle in this call. A cancel against a
|
|
43
|
+
run a live peer is holding answers `Accepted`: the request is on the engine
|
|
44
|
+
row, and the owner stops the run at its next cancel poll.
|
|
45
|
+
|
|
46
|
+
`cancel` deliberately does not replay its recorded receipt. Its answer is a
|
|
47
|
+
statement about a run, and the run moves on. See
|
|
48
|
+
[Receipts and idempotency](../concepts/receipts.md).
|
|
49
|
+
|
|
50
|
+
### How a cancel reaches another process
|
|
51
|
+
|
|
52
|
+
Fibers are process-local, so an interrupt only stops a run this process is
|
|
53
|
+
driving. The durable half travels through the executor:
|
|
54
|
+
|
|
55
|
+
1. `ControlExecutor.requestCancel` writes `cancel_requested_at_ms` on the
|
|
56
|
+
engine row, inside the mutation's transaction. An engine that refuses rolls
|
|
57
|
+
the whole cancel back, because a control row that says `cancelled` over an
|
|
58
|
+
engine row that is still running is the one state an operator cannot
|
|
59
|
+
recover from.
|
|
60
|
+
2. The attribution entry is written, unless the executor answered
|
|
61
|
+
`already-requested`, which means the column was already set and the record
|
|
62
|
+
already exists.
|
|
63
|
+
3. The local fiber is interrupted. A parked run has no owner, so the cancelling
|
|
64
|
+
process claims the park itself in order to end it.
|
|
65
|
+
4. `ControlExecutor.settleCancelledPark` runs _after_ the mutation commits, so
|
|
66
|
+
the parked execution is finished before the process that asked goes away.
|
|
67
|
+
Driving a run re-enters the engine, whose writes would wait on the writer
|
|
68
|
+
the transaction holds.
|
|
69
|
+
|
|
70
|
+
An executor that answers with a `CancelTerminal` reports that the engine row
|
|
71
|
+
had already settled. Nothing was cancelled, so no attribution is written, and
|
|
72
|
+
the control row is reconciled onto the engine's own status instead.
|
|
73
|
+
|
|
74
|
+
## Restart a run
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const resumed = yield * control.resume({
|
|
78
|
+
runId: "run-17",
|
|
79
|
+
reason: "operator retry",
|
|
80
|
+
idempotencyKey: "resume:run-17"
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`run` with a `Resume` input is the same operation:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
yield * control.run({ _tag: "Resume", runId: "run-17", idempotencyKey: "resume:run-17" })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
One resume, one implementation. The two spellings exist because RPC clients and
|
|
91
|
+
the CLI reach for different ones.
|
|
92
|
+
|
|
93
|
+
### What comes back
|
|
94
|
+
|
|
95
|
+
| Receipt | Meaning |
|
|
96
|
+
| ----------------------- | --------------------------------------------------------------------------- |
|
|
97
|
+
| `Terminal` | The run had already settled. Nothing to restart. |
|
|
98
|
+
| `Accepted` | The run was claimed, or the restart was recorded for the host that owns it. |
|
|
99
|
+
| `Accepted` + `handedTo` | A live host parked the run. The restart was handed to that host. |
|
|
100
|
+
| `AlreadyApplied` | An earlier call under this key already restarted it. |
|
|
101
|
+
|
|
102
|
+
`ClaimLost` is the failure, and it names a real peer: a run at `running` or at
|
|
103
|
+
the `accepted` a claim writes is being held by a live process, so there is
|
|
104
|
+
nothing to restart and pretending otherwise would hide the peer.
|
|
105
|
+
|
|
106
|
+
A run the _engine_ created, a child, a fork, or a later trampoline round, keeps
|
|
107
|
+
its own driver. Both public resume spellings use `scope: "launched"` and journal
|
|
108
|
+
`control.run.resume`; they leave engine-created rows unclaimed to preserve
|
|
109
|
+
the continuation state. The caller or a journal subscriber must drive the
|
|
110
|
+
execution, even when the plane claims a control-launched run. A run the caller
|
|
111
|
+
claims creates no `pendingResumes` entry. An `Accepted` receipt does not
|
|
112
|
+
establish that execution started.
|
|
113
|
+
|
|
114
|
+
### A run a live host parked
|
|
115
|
+
|
|
116
|
+
A detached host that parks a run, for example over children released when its
|
|
117
|
+
lease lapsed during a stall, stays alive and keeps the run. Claiming that run
|
|
118
|
+
would take its execution from the host. `resume` hands the restart to the host
|
|
119
|
+
instead:
|
|
120
|
+
|
|
121
|
+
1. The runtime refuses the claim with `ClaimLost` naming the host in
|
|
122
|
+
`parkedBy`.
|
|
123
|
+
2. `resume` journals `control.run.resume` with `handedTo` set to that host,
|
|
124
|
+
then records a durable delegation whose `consent` is that entry's journal
|
|
125
|
+
sequence.
|
|
126
|
+
3. The receipt is `Accepted` with `handedTo`. The caller holds nothing to
|
|
127
|
+
drive.
|
|
128
|
+
4. The host polls `pendingResumes` every second and takes the delegation up.
|
|
129
|
+
Because it carries consent, the host records the per-release retry
|
|
130
|
+
permission under that sequence, journals `control.run.claimed`, and
|
|
131
|
+
re-drives the run as an operator resume.
|
|
132
|
+
|
|
133
|
+
An approval or a wake is background intent and never carries consent, and a
|
|
134
|
+
later approval delegation keeps consent that no host has taken up yet. The
|
|
135
|
+
permission covers the releases that exist when the host takes the delegation
|
|
136
|
+
up. A later lease lapse is a new release, so it needs a new resume. A run whose
|
|
137
|
+
parking host has exited is claimed by the caller as before.
|
|
138
|
+
|
|
139
|
+
## What the CLI does
|
|
140
|
+
|
|
141
|
+
[`smthrs runs cancel`](/cli/runs) and `smthrs runs cancel-all` both reach
|
|
142
|
+
`cancel`; [`smthrs runs resume`](/cli/runs) reaches `resume`. Both record the
|
|
143
|
+
principal the CLI authenticated, so `RunSummary.cancellation.principal` names a
|
|
144
|
+
person rather than a process.
|
|
145
|
+
|
|
146
|
+
`smthrs runs resume` keys each request by the run's latest park, so resuming a
|
|
147
|
+
second park is a new request rather than a replay of the first. A run it claims
|
|
148
|
+
is driven in the foreground until it settles. A run handed to its live host is
|
|
149
|
+
not: the CLI waits up to 15 seconds for the host's `control.run.claimed` and for
|
|
150
|
+
the run to leave its `released` park, then prints the receipt with the run's
|
|
151
|
+
`status` and `waitingReason`. If the host does not take it up in that time, the
|
|
152
|
+
command fails with `resume_not_taken_up`, naming the host's process. The
|
|
153
|
+
request stays recorded for that host.
|
|
154
|
+
|
|
155
|
+
## Where to go next
|
|
156
|
+
|
|
157
|
+
- [Cancellation attribution](../concepts/cancellation.md): what the answer to
|
|
158
|
+
"who cancelled this" is built from.
|
|
159
|
+
- [Ownership, fences, and claims](../concepts/ownership.md): why `ClaimLost` is
|
|
160
|
+
the right refusal.
|
|
161
|
+
- [Connect an execution engine](./implement-an-executor.md): the four methods
|
|
162
|
+
these verbs call.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Store control state in a database"
|
|
3
|
+
description: "Compose SqlControlRuntime over a SQL database and the fenced run store: the layer stack, the migration order, the owner identity every claim is stamped with, and what sharing a database with the engine buys."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 7
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`ControlRuntime.layerMemory` models the production seams in a `Map`, and
|
|
9
|
+
nothing it decides survives the process. `SqlControlRuntime` is the durable
|
|
10
|
+
adapter: the same contract, over a SQL database and the fenced run store from
|
|
11
|
+
[`@smthrs/run-store`](/api/run-store).
|
|
12
|
+
|
|
13
|
+
## Compose the layer
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import * as ControlLive from "@smthrs/control/ControlLive"
|
|
17
|
+
import * as SqlControlRuntime from "@smthrs/control/SqlControlRuntime"
|
|
18
|
+
import * as DurableWriter from "@smthrs/database/DurableWriter"
|
|
19
|
+
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
|
|
20
|
+
import { NotificationQueue } from "@smthrs/notifications"
|
|
21
|
+
import { Registry } from "@smthrs/registry"
|
|
22
|
+
import { Migrations as RunStoreMigrations, RunStore } from "@smthrs/run-store"
|
|
23
|
+
import * as Layer from "effect/Layer"
|
|
24
|
+
|
|
25
|
+
const storage = RunStore.layer.pipe(
|
|
26
|
+
Layer.provideMerge(RunStoreMigrations.layer),
|
|
27
|
+
Layer.provideMerge(DurableWriter.layer()),
|
|
28
|
+
Layer.provideMerge(NodeDatabase.layer({ filename: "control.sqlite" }))
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const controlPlane = ControlLive.layer.pipe(
|
|
32
|
+
Layer.provideMerge(
|
|
33
|
+
Layer.mergeAll(
|
|
34
|
+
SqlControlRuntime.layer({ owner: { hostId: "gateway", pid: process.pid, nonce: "boot" } })
|
|
35
|
+
.pipe(Layer.orDie),
|
|
36
|
+
NotificationQueue.layer,
|
|
37
|
+
Registry.layerNoop()
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`SqlControlRuntime.layer` requires `Crypto`, `DurableWriter`, `SqlClient`, and
|
|
44
|
+
`RunStore`, and fails with `PersistenceError` if its migration cannot run.
|
|
45
|
+
`layerWithStore` is the same layer with `RunStore.layer` already provided, for
|
|
46
|
+
a composition that has no other use for the store.
|
|
47
|
+
|
|
48
|
+
Build the storage once. `Layer.provideMerge` builds what it provides privately,
|
|
49
|
+
so composing it twice hands the control plane its own empty copy of the rows it
|
|
50
|
+
is supposed to be reading.
|
|
51
|
+
|
|
52
|
+
`SqlControlRuntime` stores the decoded plan input and the plan card summary as
|
|
53
|
+
raw plaintext JSON. The durable adapter does not redact or encrypt those
|
|
54
|
+
columns because the input must be replayed exactly. Never put a credential in
|
|
55
|
+
plan input: store it through `Credential`, pass only a `CredentialRef`, and
|
|
56
|
+
resolve that reference at the adapter boundary that needs the secret.
|
|
57
|
+
|
|
58
|
+
The Node CLI creates its `.flows/` directory with mode `0700` and, on POSIX,
|
|
59
|
+
repairs existing directory permissions to `0700` and SQLite database, WAL,
|
|
60
|
+
and shared-memory file permissions to `0600` when opening the control store.
|
|
61
|
+
Windows does not use this POSIX chmod policy. If you embed the durable adapter
|
|
62
|
+
with your own database layer, configure equivalent storage access controls.
|
|
63
|
+
|
|
64
|
+
### Options
|
|
65
|
+
|
|
66
|
+
| Option | Meaning |
|
|
67
|
+
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `flows` | The catalog this plane may plan, as `DurableFlow` entries. Defaults to the plannable reserved system flows. |
|
|
69
|
+
| `owner` | The process identity every claim is stamped with. Omitted, one synthetic identity is minted for this runtime only, so separately constructed runtimes cannot cross each other's fences. |
|
|
70
|
+
| `principal` | The fallback identity stamped on a mutation whose caller named none. |
|
|
71
|
+
|
|
72
|
+
Supply a real `owner` whenever the host can report its process identity, so
|
|
73
|
+
liveness probes can reason about the operating-system process.
|
|
74
|
+
|
|
75
|
+
## Run the migrations
|
|
76
|
+
|
|
77
|
+
The package reserves a namespaced migration block, and a host composes it
|
|
78
|
+
beside the journal and run-store sets before opening a shared control database:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import * as ControlMigrations from "@smthrs/control/Migrations"
|
|
82
|
+
import * as DatabaseMigrations from "@smthrs/database/Migrations"
|
|
83
|
+
import * as JournalMigrations from "@smthrs/journal/Migrations"
|
|
84
|
+
import * as RunStoreMigrations from "@smthrs/run-store/Migrations"
|
|
85
|
+
|
|
86
|
+
const migrations = DatabaseMigrations.run([
|
|
87
|
+
JournalMigrations.set,
|
|
88
|
+
RunStoreMigrations.set,
|
|
89
|
+
ControlMigrations.set
|
|
90
|
+
])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`ControlMigrations.layer` runs the control set alone before exposing the
|
|
94
|
+
database to control services, and `ControlMigrations.run` is the effect behind
|
|
95
|
+
it.
|
|
96
|
+
|
|
97
|
+
Each adapter also bootstraps its own tables idempotently, through
|
|
98
|
+
`SqlControlRuntime.migrate` and `SqlCredentialStore.migrate`, so standalone
|
|
99
|
+
construction works. Prefer the composed set: a standalone runtime that recorded
|
|
100
|
+
control's high-offset migration first would make the later journal and
|
|
101
|
+
run-store sets look skipped.
|
|
102
|
+
|
|
103
|
+
The tables the set creates are `control_plans`, `control_plan_keys`,
|
|
104
|
+
`control_tokens`, `control_grants`, `control_mutations`, `control_runs`,
|
|
105
|
+
`control_run_resumes`, `control_run_messages`, `control_sequences`, and
|
|
106
|
+
`control_credentials`.
|
|
107
|
+
|
|
108
|
+
## What the durable runtime adds
|
|
109
|
+
|
|
110
|
+
Several `RunSummary` fields exist only here, because they are read from the
|
|
111
|
+
engine's own columns and journal entries:
|
|
112
|
+
|
|
113
|
+
| Field | Read from |
|
|
114
|
+
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `waitingReason` | `flows_runs.waiting_reason` |
|
|
116
|
+
| `parentRunId`, `lineageId`, `roundOrdinal`, `origin` | the run row's columns and the `flows_run_parents` spawn edges |
|
|
117
|
+
| `cancellation` | `cancel_requested_at_ms`, `flows.engine.interrupted`, and the plane's own attributed requests |
|
|
118
|
+
|
|
119
|
+
They are read through the `SqlClient` the runtime was built over. A composition
|
|
120
|
+
that wants them must give the control runtime and the engine **one** database.
|
|
121
|
+
The [`smthrs` CLI](/api/cli) does not: it keeps `.flows/control.db` and
|
|
122
|
+
`.flows/engine.db` as two files, so one run has two rows and these projections
|
|
123
|
+
are empty there. Cancellation still converges, because the request travels
|
|
124
|
+
through the [executor port](./implement-an-executor.md) and the owning driver
|
|
125
|
+
settles from it.
|
|
126
|
+
|
|
127
|
+
The listing covers every row in `flows_runs`, not only the runs this plane
|
|
128
|
+
launched. A run whose `state_json` is not a control summary, an engine-created
|
|
129
|
+
run, is projected from the row's own columns with the engine's `flowName` as
|
|
130
|
+
its `flowId`.
|
|
131
|
+
|
|
132
|
+
## Reading one run stays cheap
|
|
133
|
+
|
|
134
|
+
A listing folds the whole database, because every row is going to be answered
|
|
135
|
+
for anyway. Reading one run, which is what every mutation does before it
|
|
136
|
+
writes, reads that run and its ancestor chain and nothing else. The cost of
|
|
137
|
+
steering or cancelling a run therefore does not grow with the size of the
|
|
138
|
+
database.
|
|
139
|
+
|
|
140
|
+
## Where to go next
|
|
141
|
+
|
|
142
|
+
- [Ownership, fences, and claims](../concepts/ownership.md): what a fence is
|
|
143
|
+
and what the status mapping means.
|
|
144
|
+
- [Connect an execution engine](./implement-an-executor.md): the other half of
|
|
145
|
+
a real deployment.
|
|
146
|
+
- [Store and resolve a credential](./store-credentials.md): the durable
|
|
147
|
+
credential table lives in this same set.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Connect an execution engine"
|
|
3
|
+
description: "Implement ControlExecutor so plan launches start real runs and cancels, signals, and resumes reach the engine: the five methods, the answers each one may give, and why a launch forks."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 8
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`ControlExecutor` is the seam between authority and execution. The plane hands
|
|
9
|
+
work over and learns only what the executor did with it.
|
|
10
|
+
|
|
11
|
+
Without it, the plane records facts nobody reads: a cancel that answers
|
|
12
|
+
`ClaimLost` to every process but the owner, a signal a parked run never sees,
|
|
13
|
+
and a resume nothing subscribes to.
|
|
14
|
+
|
|
15
|
+
## The five methods
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface Service {
|
|
19
|
+
readonly launch: (input: Launch) => Effect.Effect<Acceptance, LaunchFailed>
|
|
20
|
+
readonly requestCancel: (input: CancelRequest) => Effect.Effect<CancelRecord, PersistenceError>
|
|
21
|
+
readonly deliverSignal: (input: Signal) => Effect.Effect<SignalDelivery, PersistenceError>
|
|
22
|
+
readonly resumeRun: (input: ResumeRequest) => Effect.Effect<ResumeUptake, PersistenceError>
|
|
23
|
+
readonly settleCancelledPark: (input: CancelRequest) => Effect.Effect<void, PersistenceError>
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each answer is a small closed vocabulary, and every value in it is a real
|
|
28
|
+
deployment:
|
|
29
|
+
|
|
30
|
+
| Method | Answer | Means |
|
|
31
|
+
| --------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
32
|
+
| `launch` | `accepted` | This executor took the launch. The plane writes `running`. |
|
|
33
|
+
| | `pending` | It queued the launch. The plane releases the row as `control.run.pending`. |
|
|
34
|
+
| | fails `LaunchFailed` | Nothing will ever drive this run. The plane settles the row as `failed`. |
|
|
35
|
+
| `requestCancel` | `recorded` | This call set `cancel_requested_at_ms` on the engine row. |
|
|
36
|
+
| | `already-requested` | The column was already set, so the attribution record already exists. |
|
|
37
|
+
| | `unknown` | This executor's engine has no row for the run. |
|
|
38
|
+
| | `{ _tag: "Terminal", status }` | The engine row has already settled. |
|
|
39
|
+
| `deliverSignal` | `delivered`, `no-match`, `refused`, `unknown` | See [Deliver a signal](./signal-a-run.md). |
|
|
40
|
+
| `resumeRun` | `resuming` | This executor hosts the run, took the fence, and is re-driving it. |
|
|
41
|
+
| | `unknown` | It drives no execution for this run. |
|
|
42
|
+
| `settleCancelledPark` | | Finishes a parked execution whose cancellation is already durable. |
|
|
43
|
+
|
|
44
|
+
`already-requested` is not a detail. The write is first-writer-wins and every
|
|
45
|
+
repeat of `cancel` re-runs the whole mutation, so answering `recorded` to all
|
|
46
|
+
of them journals one `control.run.cancel-requested` per ask for a single
|
|
47
|
+
cancellation.
|
|
48
|
+
|
|
49
|
+
`settleCancelledPark` exists because a park has no owner, so nothing is driving
|
|
50
|
+
the run and nothing reads the request `requestCancel` wrote. The engine's
|
|
51
|
+
parked-run sweep does, once per heartbeat, but a short-lived `smthrs runs cancel`
|
|
52
|
+
process writes the request at the very end of its life and exits first. The
|
|
53
|
+
plane calls this _after_ the cancel mutation commits, never inside it: driving
|
|
54
|
+
a run re-enters the engine, whose writes would wait on the writer the
|
|
55
|
+
transaction holds.
|
|
56
|
+
|
|
57
|
+
## Start from the noop
|
|
58
|
+
|
|
59
|
+
`ControlExecutor.makeNoop` answers the honest absence for every method, so an
|
|
60
|
+
implementation overrides only what it supports:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import * as ControlExecutor from "@smthrs/control/ControlExecutor"
|
|
64
|
+
import * as Effect from "effect/Effect"
|
|
65
|
+
|
|
66
|
+
const executor = ControlExecutor.makeNoop({
|
|
67
|
+
launch: ({ plan, run }) =>
|
|
68
|
+
Effect.sync(() => {
|
|
69
|
+
queue.push({ flowId: plan.card.flowId, runId: run.runId })
|
|
70
|
+
return "pending" as const
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`ControlExecutor.layer(executor)` and `ControlExecutor.layerNoop(overrides)`
|
|
76
|
+
provide it.
|
|
77
|
+
|
|
78
|
+
## Start the run the plane minted
|
|
79
|
+
|
|
80
|
+
`launch` receives the stored plan and the run row the plane has committed, and
|
|
81
|
+
it must start _that_ run: `run.runId` is the execution id, so the events the
|
|
82
|
+
engine journals and the row the plane projects name one run.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import * as ControlExecutor from "@smthrs/control/ControlExecutor"
|
|
86
|
+
import * as ControlRuntime from "@smthrs/control/ControlRuntime"
|
|
87
|
+
import type * as ControlSchema from "@smthrs/control/ControlSchema"
|
|
88
|
+
import type { FlowRuntime } from "@smthrs/flow"
|
|
89
|
+
import { Executable, Registry } from "@smthrs/registry"
|
|
90
|
+
import { RunStore } from "@smthrs/run-store"
|
|
91
|
+
import type * as Crypto from "effect/Crypto"
|
|
92
|
+
import * as Effect from "effect/Effect"
|
|
93
|
+
import type * as FileSystem from "effect/FileSystem"
|
|
94
|
+
import * as Layer from "effect/Layer"
|
|
95
|
+
import type * as Path from "effect/Path"
|
|
96
|
+
|
|
97
|
+
/** How a discovered descriptor is loaded, and what it may delegate to. */
|
|
98
|
+
const bridge: Executable.Options = { delegates: [] }
|
|
99
|
+
|
|
100
|
+
const executorLayer = Layer.effect(ControlExecutor.ControlExecutor)(
|
|
101
|
+
Effect.gen(function*() {
|
|
102
|
+
const plane = yield* ControlRuntime.ControlRuntime
|
|
103
|
+
const services = yield* Effect.context<
|
|
104
|
+
| Crypto.Crypto
|
|
105
|
+
| FileSystem.FileSystem
|
|
106
|
+
| FlowRuntime.FlowRuntime
|
|
107
|
+
| Path.Path
|
|
108
|
+
| Registry.Registry
|
|
109
|
+
| RunStore.RunStore
|
|
110
|
+
>()
|
|
111
|
+
|
|
112
|
+
return ControlExecutor.makeNoop({
|
|
113
|
+
launch: ({ plan, run }) =>
|
|
114
|
+
Effect.gen(function*() {
|
|
115
|
+
const executable = yield* Executable.fromRegistry(plan.card.flowId, bridge)
|
|
116
|
+
Effect.runForkWith(services)(
|
|
117
|
+
executable.flow.execute(
|
|
118
|
+
{ input: plan.decodedInput },
|
|
119
|
+
{ executionId: run.runId, discard: true }
|
|
120
|
+
).pipe(Effect.andThen(mirror(plane, run.runId)))
|
|
121
|
+
)
|
|
122
|
+
return "accepted" as const
|
|
123
|
+
}).pipe(Effect.provide(services), Effect.orDie)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It forks so launch acceptance does not wait for execution to finish. Admission
|
|
130
|
+
is already committed before `launch` runs. After the executor answers
|
|
131
|
+
`accepted`, the plane marks a still-accepted run as running; an outcome the
|
|
132
|
+
executor has already recorded, including a parked or completed run, is preserved.
|
|
133
|
+
|
|
134
|
+
## Mirror the engine's status back
|
|
135
|
+
|
|
136
|
+
The plane cannot see into the engine's database, so an executor that walked
|
|
137
|
+
away after starting a run would leave every run reading `running` forever.
|
|
138
|
+
Reading the engine's own row back and writing the plane's vocabulary onto the
|
|
139
|
+
plane's row is the whole of that duty, and it is the `mirror` the launch above
|
|
140
|
+
chains onto:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
/** The engine's run vocabulary, in the plane's. */
|
|
144
|
+
const planeStatus = (status: RunStore.RunStatus): ControlSchema.RunStatus =>
|
|
145
|
+
status === "suspended" ? "parked" : status === "pending" ? "accepted" : status
|
|
146
|
+
|
|
147
|
+
const mirror = (plane: ControlRuntime.Service, runId: string) =>
|
|
148
|
+
Effect.gen(function*() {
|
|
149
|
+
const runs = yield* RunStore.RunStore
|
|
150
|
+
const row = yield* runs.get(runId)
|
|
151
|
+
const id = runId as ControlSchema.RunId
|
|
152
|
+
const fence = yield* plane.claimFence(id)
|
|
153
|
+
yield* plane.writeStatus(id, fence, planeStatus(row.status))
|
|
154
|
+
}).pipe(Effect.orDie)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The engine calls a parked run `suspended`; an operator calls it `parked`. The
|
|
158
|
+
two vocabularies are not the same set, so the mapping is explicit: every status
|
|
159
|
+
the engine reports has to land on a member of `ControlSchema.RunStatus`, which
|
|
160
|
+
is the only vocabulary the plane's row accepts.
|
|
161
|
+
|
|
162
|
+
The complete, runnable bridge, with discovery, two databases, and one shared
|
|
163
|
+
journal, is
|
|
164
|
+
[`examples/src/24-control-plane-and-gateway.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/24-control-plane-and-gateway.ts).
|
|
165
|
+
|
|
166
|
+
## Where to go next
|
|
167
|
+
|
|
168
|
+
- [Ownership, fences, and claims](../concepts/ownership.md): what
|
|
169
|
+
`claimFence` and `writeStatus` are doing.
|
|
170
|
+
- [Cancel a run, and restart one](./cancel-and-resume.md): the sequence
|
|
171
|
+
`requestCancel` and `settleCancelledPark` sit inside.
|
|
172
|
+
- [Store control state in a database](./durable-storage.md): the other half of
|
|
173
|
+
a real deployment.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Accept a webhook as a control request"
|
|
3
|
+
description: "Turn a verified external request into one control mutation: the verify-then-decode order, the durable idempotency a redelivery replays, the headers that may enter identity, and the body ceiling."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 11
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A channel turns an external request into a control mutation, once. It verifies
|
|
9
|
+
opaque bytes before it decodes them, and it acquires no execution path of its
|
|
10
|
+
own: everything it does, it does through `Control`.
|
|
11
|
+
|
|
12
|
+
## Build a webhook channel
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import * as WebhookChannel from "@smthrs/control/WebhookChannel"
|
|
16
|
+
import * as Effect from "effect/Effect"
|
|
17
|
+
import * as Redacted from "effect/Redacted"
|
|
18
|
+
import * as Schema from "effect/Schema"
|
|
19
|
+
|
|
20
|
+
const Push = Schema.Struct({ ref: Schema.String })
|
|
21
|
+
|
|
22
|
+
const github = WebhookChannel.make({
|
|
23
|
+
name: "github",
|
|
24
|
+
schema: Push,
|
|
25
|
+
credential: Redacted.make({ id: "github-webhook", name: "GitHub webhook" }),
|
|
26
|
+
fingerprintHeaders: ["x-github-event"],
|
|
27
|
+
verify: (raw, credential) => verifySignature(raw, credential),
|
|
28
|
+
map: (payload) =>
|
|
29
|
+
Effect.succeed({
|
|
30
|
+
_tag: "Start" as const,
|
|
31
|
+
flowId: "ops/Deploy",
|
|
32
|
+
input: { build: payload.ref }
|
|
33
|
+
}),
|
|
34
|
+
project: (run) => ({ cursor: run.status, operation: "post", message: { text: run.runId } })
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`map` returns one of two results:
|
|
39
|
+
|
|
40
|
+
| Result | Becomes |
|
|
41
|
+
| ----------------------------------- | ---------------------------------------- |
|
|
42
|
+
| `{ _tag: "Start", flowId, input }` | `control.plan` followed by `control.run` |
|
|
43
|
+
| `{ _tag: "Signal", runId, signal }` | `control.signal` |
|
|
44
|
+
|
|
45
|
+
`decode` and `map` must be deterministic and free of side effects. A retry may
|
|
46
|
+
evaluate either of them again.
|
|
47
|
+
|
|
48
|
+
The credential is a redacted `CredentialRef`, never a secret. Resolving it
|
|
49
|
+
belongs at the host adapter boundary, so a webhook's persisted record never
|
|
50
|
+
holds key material. See [Store and resolve a credential](./store-credentials.md).
|
|
51
|
+
|
|
52
|
+
## Register and mount it
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import * as Channels from "@smthrs/control/Channels"
|
|
56
|
+
|
|
57
|
+
const program = Effect.gen(function*() {
|
|
58
|
+
const channels = yield* Channels.Channels
|
|
59
|
+
yield* channels.register(github)
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`register` accepts `Channel<A>` directly, including typed webhook payloads.
|
|
64
|
+
`lookup` returns a `RegisteredChannel`: the declared schema and transport
|
|
65
|
+
metadata remain available, while `decodeAndMap` keeps the hidden payload type
|
|
66
|
+
inside the adapter. Use `ingest` for verified dispatch through `Control`.
|
|
67
|
+
|
|
68
|
+
`Channels.layer` builds the coordinator over `ControlRuntime`'s durable
|
|
69
|
+
mutation store, so inbound idempotency survives a coordinator restart. Only
|
|
70
|
+
registration and outbound projection cursors are process-local.
|
|
71
|
+
`Channels.layerMemory` is process-local throughout and exists for adapter unit
|
|
72
|
+
tests.
|
|
73
|
+
|
|
74
|
+
`WebhookChannel.handler` reads an abstract Effect HTTP request and dispatches
|
|
75
|
+
it, so any Effect HTTP host can mount it:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
HttpRouter.post("/webhooks/github", WebhookChannel.handler("github", deliveryId))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Take `deliveryId` from the platform's own delivery header. That is what makes a
|
|
82
|
+
redelivery the same mutation instead of a second one.
|
|
83
|
+
|
|
84
|
+
## The order verification happens in
|
|
85
|
+
|
|
86
|
+
`ingest` runs one request at a time, and in this order:
|
|
87
|
+
|
|
88
|
+
1. Copy the request: the bytes, and only the enumerable own string headers,
|
|
89
|
+
lower-cased, with a duplicate case-insensitive name refused.
|
|
90
|
+
2. Compute the body fingerprint.
|
|
91
|
+
3. **Verify**, with the verifier's own copy of the body. Signature verification
|
|
92
|
+
is the amplification guard, so it happens before decode and before any
|
|
93
|
+
`Control` access. The verifier receiving its own copy means even a verifier
|
|
94
|
+
that edits bytes cannot change what the decoder sees after approval.
|
|
95
|
+
4. Look the durable idempotency record up. A match replays the stored receipt.
|
|
96
|
+
5. Decode, map, and dispatch through `Control`.
|
|
97
|
+
6. Record the receipt, unless it was a `Conflict` or `Parked`. A parked start
|
|
98
|
+
leaves the ingress key unsettled: approve its stored plan, then redeliver
|
|
99
|
+
with the same delivery id to retry the launch. Once accepted, later
|
|
100
|
+
redeliveries return `AlreadyApplied` for the same run.
|
|
101
|
+
|
|
102
|
+
The receipt handed back carries the platform's own delivery id as its
|
|
103
|
+
`receiptId`, so a caller correlating against its own logs sees the id it sent.
|
|
104
|
+
|
|
105
|
+
## What enters durable identity
|
|
106
|
+
|
|
107
|
+
The fingerprint is the SHA-256 of the body plus only the header names the
|
|
108
|
+
adapter declared in `fingerprintHeaders`, matched case-insensitively and sorted.
|
|
109
|
+
|
|
110
|
+
Declare a header there only when its value changes the decoded command, as
|
|
111
|
+
`x-github-event` does. Signature, authorization, cookie, token, and credential
|
|
112
|
+
headers must not be declared: rotating an excluded credential header leaves the
|
|
113
|
+
delivery identical, which is what you want.
|
|
114
|
+
|
|
115
|
+
Reusing one delivery id with different declared semantics answers `Conflict`.
|
|
116
|
+
|
|
117
|
+
## The body ceiling
|
|
118
|
+
|
|
119
|
+
A webhook is the one control-plane ingress a caller reaches with an arbitrary
|
|
120
|
+
payload, so `handler` bounds the body twice:
|
|
121
|
+
|
|
122
|
+
- A `content-length` over the limit is refused before the body is read at all,
|
|
123
|
+
so a declared flood costs nothing.
|
|
124
|
+
- Each streamed chunk is measured before it is retained. Reading stops and the
|
|
125
|
+
stream is cancelled at the first chunk exceeding the limit, even if the
|
|
126
|
+
caller understates or omits the length. Verification has not run at this point.
|
|
127
|
+
|
|
128
|
+
Both refusals are `InvalidInput` naming the two byte counts and no body
|
|
129
|
+
content. The default is `WebhookChannel.maximumBodyBytes`, 1 MiB, and one mount
|
|
130
|
+
lowers it:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
WebhookChannel.handler("github", deliveryId, { maximumBodyBytes: 256 * 1024 })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The default is deliberately smaller than the 4 MiB mutation identity budget: a
|
|
137
|
+
body that cannot become a durable mutation is refused at the door rather than
|
|
138
|
+
copied, decoded, and refused later.
|
|
139
|
+
|
|
140
|
+
Malformed JSON returns `InvalidInput` with the fixed issue `invalid webhook
|
|
141
|
+
JSON`. Parser messages and payload fragments are excluded from the error.
|
|
142
|
+
|
|
143
|
+
## Project a run back out
|
|
144
|
+
|
|
145
|
+
`project` is side-effect free. It turns a `RunSummary` and the previous
|
|
146
|
+
delivery record into a `DeliveryProjection`, and the transport adapter performs
|
|
147
|
+
the network call after the projection is journaled:
|
|
148
|
+
|
|
149
|
+
| `operation` | Meaning |
|
|
150
|
+
| ----------- | ------------------------------------- |
|
|
151
|
+
| `post` | Send a new message. |
|
|
152
|
+
| `edit` | Update the message `messageId` names. |
|
|
153
|
+
| `noop` | Nothing changed worth sending. |
|
|
154
|
+
|
|
155
|
+
The coordinator keeps delivery identities for live runs, including parked runs
|
|
156
|
+
and runs waiting for approval, so later projections can edit the same message.
|
|
157
|
+
Completed, failed, and cancelled runs share a FIFO window of 1,024 delivery
|
|
158
|
+
records across channels. Repeated terminal projections do not extend that
|
|
159
|
+
window. A run projected as live again leaves the terminal window.
|
|
160
|
+
|
|
161
|
+
A `noop` does not create or replace a delivery record. Unchanged cursor and
|
|
162
|
+
message identities reuse the previous record. Terminal status still moves an
|
|
163
|
+
existing record into the retention window even when the projection is a noop.
|
|
164
|
+
|
|
165
|
+
Outbound records are process-local. After terminal eviction or coordinator
|
|
166
|
+
restart, the adapter receives no previous delivery and may post a new message.
|
|
167
|
+
Hosts needing edits beyond this window must keep remote message identities in
|
|
168
|
+
their own durable transport storage.
|
|
169
|
+
|
|
170
|
+
## Where to go next
|
|
171
|
+
|
|
172
|
+
- [Store and resolve a credential](./store-credentials.md): where the verifier's
|
|
173
|
+
secret comes from.
|
|
174
|
+
- [Receipts and idempotency](../concepts/receipts.md): the store behind a
|
|
175
|
+
replayed redelivery.
|
|
176
|
+
- [Gate work behind an approval](./approvals.md): an ingested `Start` still
|
|
177
|
+
parks until somebody approves it.
|