@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
package/docs/api.md
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "API reference"
|
|
3
|
+
description: "Every public export of @smthrs/control, module by module: the Control service and its ten operations, the wire schemas, the typed failures, the three ports, the RPC boundary, the projections, and the credential surface."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every module is importable from the root entry point as a namespace and from
|
|
7
|
+
its own subpath:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Control, ControlLive, Monitor } from "@smthrs/control"
|
|
11
|
+
import * as ControlSchema from "@smthrs/control/ControlSchema"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@smthrs/control/internal/*`, `@smthrs/control/migrations/*`, and every nested
|
|
15
|
+
`*/index` are blocked in the export map. `@smthrs/control/package.json` is
|
|
16
|
+
exported, and so is `@smthrs/control/test/TestControl`.
|
|
17
|
+
|
|
18
|
+
Signatures in this reference use the usual shorthand: `Effect<A, E, R>` for
|
|
19
|
+
`Effect.Effect`, `Stream<A, E>` for `Stream.Stream`, `Layer<A, E, R>` for
|
|
20
|
+
`Layer.Layer`, and `Redacted<A>` for `Redacted.Redacted`.
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Control } from "@smthrs/control/Control"
|
|
26
|
+
import * as Effect from "effect/Effect"
|
|
27
|
+
|
|
28
|
+
const program = Effect.gen(function*() {
|
|
29
|
+
const control = yield* Control
|
|
30
|
+
const card = yield* control.plan({ flowId: "quickstart/Deploy", input: { build: "v1.4.0" } })
|
|
31
|
+
yield* control.approve(card.approval)
|
|
32
|
+
return yield* control.run({
|
|
33
|
+
_tag: "Plan",
|
|
34
|
+
planId: card.planId,
|
|
35
|
+
digest: card.digest,
|
|
36
|
+
envelope: card.envelope,
|
|
37
|
+
idempotencyKey: "deploy:v1.4.0"
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Control
|
|
43
|
+
|
|
44
|
+
The transport-independent control vtable. Every implementation in this package
|
|
45
|
+
and every client projects onto this one interface. Each operation includes
|
|
46
|
+
`TransportError` and `Unauthorized` in its error channel for remote transport
|
|
47
|
+
and authentication failures.
|
|
48
|
+
|
|
49
|
+
| Export | Kind | Signature |
|
|
50
|
+
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| `Control` | class | `Context.Service<Control, Service>` at key `/control/Control` |
|
|
52
|
+
| `Service` | interface | The ten operations in the following table |
|
|
53
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
54
|
+
| `layerNoop` | layer | `Layer<Control>`. Every operation fails `Unavailable`, naming the verb as `feature` and the constant `control-runtime-engine-integration` as `ticket`. |
|
|
55
|
+
|
|
56
|
+
### Service
|
|
57
|
+
|
|
58
|
+
| Operation | Signature | Returns |
|
|
59
|
+
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `plan` | `(input: PlanInput) => Effect<PlanCard, FlowNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | The reviewable card, whether or not this call created it. |
|
|
61
|
+
| `run` | `(input: RunInput) => Effect<Receipt, RunNotFound \| PlanNotFound \| PlanDenied \| PlanDigestMismatch \| EnvelopeMismatch \| ClaimLost \| InvalidInput \| LaunchFailed \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Parked` for a plan; a resume answers as `resume` does. |
|
|
62
|
+
| `approve` | `(input: ApprovalInput) => Effect<Receipt, PlanDigestMismatch \| EnvelopeMismatch \| AlreadyResolved \| PlanNotFound \| RunNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`. |
|
|
63
|
+
| `deny` | same as `approve` | same as `approve`. |
|
|
64
|
+
| `steer` | `(input: SteerInput) => Effect<Receipt, NotificationError \| RunNotFound \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`. |
|
|
65
|
+
| `signal` | `(input: SignalInput) => Effect<Receipt, RunNotFound \| NoMatchingWait \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`. |
|
|
66
|
+
| `cancel` | `(input: RunMutationInput) => Effect<Receipt, RunNotFound \| ClaimLost \| InvalidInput \| PersistenceError \| Unavailable \| TransportError \| Unauthorized>` | `Accepted` or `Terminal`. Never replays its recorded receipt. |
|
|
67
|
+
| `resume` | same as `cancel` | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`. |
|
|
68
|
+
| `list` | `(input: ListRequest) => Effect<ListResponse, ControlError>` | A bounded page of flows, runs, triggers, or trigger fires, or exact run-scoped native execution observations. |
|
|
69
|
+
| `watch` | `(filter: WatchFilter) => Stream<ControlEvent, ControlError>` | Committed journal entries, plus the deltas the plane derives. |
|
|
70
|
+
|
|
71
|
+
There is no `pause`. An operator park is written through
|
|
72
|
+
`ControlRuntime.writeStatus(runId, fence, "parked")`.
|
|
73
|
+
|
|
74
|
+
### Inputs
|
|
75
|
+
|
|
76
|
+
| Type | Shape |
|
|
77
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `PlanInput` | `{ flowId: FlowId; input: unknown; idempotencyKey?: IdempotencyKey }`. `input` is `unknown` so the runtime can decode its own flow's schema before anything crosses a transport. |
|
|
79
|
+
| `RunInput` | `ControlSchema.RunInputSchema.Type & { principal?: Principal }`, so either `{ _tag: "Plan", planId, digest, envelope, idempotencyKey }` or `{ _tag: "Resume", runId, idempotencyKey }`. |
|
|
80
|
+
| `ApprovalInput` | `ApprovalPayload & { principal?: Principal }`: `{ target, scope, idempotencyKey }`. |
|
|
81
|
+
| `SteerInput` | `{ runId: RunId; message: SteerMessage; idempotencyKey: IdempotencyKey }`. |
|
|
82
|
+
| `SignalInput` | `{ runId: RunId; signal: SignalPayload; idempotencyKey: IdempotencyKey; principal?: Principal }`. |
|
|
83
|
+
| `RunMutationInput` | `{ runId: RunId; idempotencyKey: IdempotencyKey; reason?: string; principal?: Principal }`. `reason` is recorded on the journal entry the mutation writes. |
|
|
84
|
+
|
|
85
|
+
`ApprovalTarget` is re-exported from `ControlSchema` for convenience.
|
|
86
|
+
|
|
87
|
+
`principal` is present on the local contracts because a runtime stamps it. The
|
|
88
|
+
RPC schemas that exclude it do so on purpose: an authenticated server names the
|
|
89
|
+
identity, and a remote client cannot claim another.
|
|
90
|
+
|
|
91
|
+
## ControlSchema
|
|
92
|
+
|
|
93
|
+
`Control.list({ _tag: "executions", runId, executionIds })` reads at most 200 exact native executions under an authorized control run. It returns `{ _tag: "executions", source, revision, items }`. Each snapshot is `Observed` with a native observation, `Missing`, or `Unavailable`. Unrelated IDs and incomplete ancestry never expose another run. Hosts without this observer return `Unavailable`; historical journal statuses remain historical. A batch and its ancestry checks share one engine transaction and source revision. `runs show` reads every known ID in bounded batches and applies only matching observed rows. Its `executionSnapshots` array retains each batch's source and revision; batches may observe different revisions while a run progresses. A changed source refuses the show until retried.
|
|
94
|
+
|
|
95
|
+
The serializable values both halves of the wire decode. Every entry has a
|
|
96
|
+
schema constant and a type of the same name unless noted.
|
|
97
|
+
|
|
98
|
+
### Identifiers and identity
|
|
99
|
+
|
|
100
|
+
| Export | Shape |
|
|
101
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------- |
|
|
102
|
+
| `RunId`, `FlowId`, `IdempotencyKey` | `Schema.String` aliases that name what a string is. |
|
|
103
|
+
| `Principal` | `{ id: string; kind: string; stampedAt: number }`. Stamped at the control boundary. |
|
|
104
|
+
|
|
105
|
+
### Authority
|
|
106
|
+
|
|
107
|
+
| Export | Shape |
|
|
108
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `Envelope` | `{ capabilities: string[]; flows: string[]; budget: { tokens?: number; milliseconds?: number; usd?: number; onExceeded?: BudgetOnExceeded }; host?: string }`. |
|
|
110
|
+
| `GrantScope` | `"once" \| "run" \| "remembered"`. |
|
|
111
|
+
| `ApprovalTarget` | `{ _tag: "Plan", planId, digest, envelope }` or `{ _tag: "Node", runId, requestId, digest, envelope }`. |
|
|
112
|
+
| `ApprovalPayload` | `{ target: ApprovalTarget; scope: GrantScope; idempotencyKey: IdempotencyKey }`. |
|
|
113
|
+
|
|
114
|
+
### Plans
|
|
115
|
+
|
|
116
|
+
| Export | Shape |
|
|
117
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `PlanNodeStatus` | `"cached" \| "run"`. The two outcomes a step key already decides: reuse the cached result, or run the step. A card reports nothing else. |
|
|
119
|
+
| `PlanNode` | The persisted plan node's fields plus `status`. `key` is the step key [`@smthrs/plan`](/api/plan) compiled, so a node named here and a node in the persisted plan are the same node. |
|
|
120
|
+
| `PlanCard` | `{ planId, flowId, digest, inputSummary, warnings?: DiscoveryWarning[], envelope, deployClass, executionDigest?, plan?, nodes, graph?, approval }`. `approval` is the complete payload a reviewer resubmits unchanged. |
|
|
121
|
+
| `PlanEdgeReason` | `"value" \| "continuation" \| "failure" \| "conflict" \| "lane-merge"`. Why one node waits for another, in the vocabulary of whichever graph builder the host planned with. |
|
|
122
|
+
| `PlanEdge` | `{ from, to, reason: PlanEdgeReason }`. One labelled edge of the graph the plan was built from. |
|
|
123
|
+
| `PlanGraphNode` | `{ id, declaredAt?: { path, line } }`. Where one node was declared, repo-relative under `@smthrs/journal`'s own rule, which refuses an absolute path. A host that cannot make a path relative to its root omits it. |
|
|
124
|
+
| `PlanGraph` | `{ edges: PlanEdge[], nodes?: PlanGraphNode[], sourceRevision?: string }`. A `PlanNode` carries `dependsOn`, one unlabelled edge set that cannot tell a value dependency from a recovery arm or from an ordering edge a write conflict added; a host that graphs a flow reports the reasons here instead, and the declaration sites the key material deliberately does not carry. `sourceRevision` is the immutable name of the tree those sites were read out of: a jj working-copy commit id, or a git commit for a tree that still matches one, so a reader can ask for the file AT that revision instead of whatever is on disk now. A host that cannot name one omits it. |
|
|
125
|
+
|
|
126
|
+
`graph` sits deliberately OUTSIDE the digest an approval binds to. The edges
|
|
127
|
+
and the declaration sites describe the plan a reader draws, and nothing in
|
|
128
|
+
them changes what will run,
|
|
129
|
+
so a host that starts reporting them re-plans to the digest it planned to
|
|
130
|
+
before and every parked approval still validates. A host that graphs nothing
|
|
131
|
+
omits the field, and a card stored before the field existed still decodes.
|
|
132
|
+
|
|
133
|
+
`warnings` carries discovery diagnostics for the selected flow, including source
|
|
134
|
+
locations for conservative authority fallbacks. Like `graph`, it is outside the
|
|
135
|
+
approval digest and does not change the approval payload. Generic hosts and
|
|
136
|
+
cards stored before the field existed may omit it.
|
|
137
|
+
|
|
138
|
+
`executionDigest` binds a discovery-based host's measured source and metadata
|
|
139
|
+
to the approved card digest. It is optional for generic control-plane hosts,
|
|
140
|
+
but `AgentSession` requires it for prompt execution and checks it again at
|
|
141
|
+
launch and on every drive or resume. Changing prompt bytes, model, parameters,
|
|
142
|
+
or other discovered metadata requires a new plan and approval.
|
|
143
|
+
|
|
144
|
+
### Runs
|
|
145
|
+
|
|
146
|
+
| Export | Shape |
|
|
147
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| `RunStatus` | `"accepted" \| "running" \| "parked" \| "waiting-approval" \| "cancelled" \| "completed" \| "failed"`. |
|
|
149
|
+
| `RunOrigin` | `Lineage.Origin`, re-exported so a serializable projection needs one import. |
|
|
150
|
+
| `CancelSource` | `"control" \| "engine" \| "cascade"`. |
|
|
151
|
+
| `Cancellation` | `{ requestedAt; source; principal?; reason?; cascadedFrom? }`. See [cancellation attribution](./concepts/cancellation.md). |
|
|
152
|
+
| `RunSummary` | The projection every listing returns. Required: `runId`, `flowId`, `status`, `createdAt`, `updatedAt`. Optional: `planId`, `planDigest`, `ownerId`, `parentRunId`, `lineageId`, `roundOrdinal`, `origin`, `waitingReason`, `steering`, `pendingResume`, `parkedBy`, `cancellation`. |
|
|
153
|
+
|
|
154
|
+
`waitingReason` is the run row's own column, written by the engine and only
|
|
155
|
+
read here. The CLI's `runs list` and `runs show` also render `executor` in that
|
|
156
|
+
position for a run that has sat at `accepted` with no owner past the launch
|
|
157
|
+
handoff window; that value is computed at render time and never stored, so a
|
|
158
|
+
reader going through the RPC, the gateway, or a plugin sees the field absent on
|
|
159
|
+
the same run.
|
|
160
|
+
|
|
161
|
+
### Steering
|
|
162
|
+
|
|
163
|
+
| Export | Shape |
|
|
164
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `MessageSteer` | The envelope plus `kind?: "Message"` and `body: string`. |
|
|
166
|
+
| `SeatSteer` | The envelope plus the notification package's seat payload fields. |
|
|
167
|
+
| `ThinkingSteer` | The envelope plus its thinking payload fields. |
|
|
168
|
+
| `ToolsSteer` | The envelope plus its tools payload fields. |
|
|
169
|
+
| `SteerMessage` | The union of those four. The shared envelope is `{ messageId, runId, principal, createdAt }`. |
|
|
170
|
+
| `steerItem` | `(message: SteerMessage) => SteerPayload`. Strips the control envelope and returns the item the harness reads back. |
|
|
171
|
+
|
|
172
|
+
### Signals and events
|
|
173
|
+
|
|
174
|
+
| Export | Shape |
|
|
175
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| `SignalPayload` | `{ name: string; payload: Json }`. |
|
|
177
|
+
| `WatchCursor` | `{ sequence: number; offset?: number }`. A present offset is the last consumed expansion index; absent means the source entry is fully consumed. |
|
|
178
|
+
| `WatchFilter` | `{ runId?: RunId; afterSequence?: number; afterCursor?: WatchCursor; follow?: boolean }`. Either cursor requires `runId`; do not combine them. Omitting `follow` keeps the live stream; `false` requests a finite snapshot. |
|
|
179
|
+
| `ControlEvent` | `{ cursor?: WatchCursor; sequence: number; kind: string; runId?: RunId; occurredAt: number; payload: Json }`. |
|
|
180
|
+
|
|
181
|
+
### Listing
|
|
182
|
+
|
|
183
|
+
| Export | Shape |
|
|
184
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| `defaultPageSize` | `100`. |
|
|
186
|
+
| `maxPageSize` | `500`. |
|
|
187
|
+
| `PageLimit` | An integer between 1 and `maxPageSize`. |
|
|
188
|
+
| `ListRequest` | `{ _tag: "flows", filters?, cursor?, limit? }`, `{ _tag: "runs", filters?: { runId?, flowId?, status?, principalId?, parentRunId?, lineageId? }, cursor?, limit? }`, `{ _tag: "triggers", filters?: { triggerId?, flowId?, enabled? }, cursor?, limit? }`, `{ _tag: "fires", filters?: { triggerId?, runId?, outcome? }, cursor?, limit? }`, or `{ _tag: "plans", filters?: { flowId?, decision? }, cursor?, limit? }`. |
|
|
189
|
+
| `ListResponse` | `{ _tag: "flows", items, warnings?, nextCursor? }`, `{ _tag: "runs", items: RunSummary[], nextCursor? }`, `{ _tag: "triggers", items: TriggerSummary[], nextCursor? }`, `{ _tag: "fires", items: FireSummary[], nextCursor? }`, or `{ _tag: "plans", items: PlanSummary[], nextCursor? }`. |
|
|
190
|
+
| `TriggerSummary` | `{ triggerId, flowId, input: Json, cron, timezone?, overlap: "skip" \| "buffer-one" \| "supersede", catchUp: "none" \| "one" \| "all", maxCatchUp?, enabled, revision, lastFiredAtMs?, pendingAtMs?, activeRunId?, nextOccurrencesMs: number[], schedulerLastTickMs? }`. One registered trigger. `schedulerLastTickMs` absent means no scheduler has ticked on this host. |
|
|
191
|
+
| `FireOutcome` | `"launched" \| "completed" \| "skipped" \| "buffered" \| "superseded" \| "failed"`. The same words the triggers package records in its fire ledger. |
|
|
192
|
+
| `FireSummary` | `{ triggerId, occurrenceAtMs, outcome: FireOutcome \| null, runId?, error?, waiting?: "approval" }`. One claimed occurrence; `outcome: null` is the window between the claim and its result. |
|
|
193
|
+
| `PlanDecision` | `"pending" \| "approved" \| "denied"`. |
|
|
194
|
+
| `PlanSummary` | `{ card: PlanCard, input: Json, decision: PlanDecision }`. One stored plan; `card.approval` is the payload `approve` or `deny` takes. |
|
|
195
|
+
|
|
196
|
+
`plans` lists stored plans oldest first, narrowed by `flowId` and `decision`.
|
|
197
|
+
A build target that declares `approval: "required"` leaves a pending
|
|
198
|
+
`system/target` plan whose `input` is `{ label, digest }`; list them with
|
|
199
|
+
`{ _tag: "plans", filters: { flowId: "system/target", decision: "pending" } }`
|
|
200
|
+
and submit `card.approval` to `approve` or `deny`. A page returns at most
|
|
201
|
+
`limit` plans and `nextCursor` while later plans may match. Plans are an
|
|
202
|
+
operator's to read: a reader restricted to its own runs lists none.
|
|
203
|
+
|
|
204
|
+
Run listings default to 100 items and accept limits from 1 through 500. Pass
|
|
205
|
+
`nextCursor` back unchanged with the same filters. Run cursors contain stable
|
|
206
|
+
ordering keys, not numeric offsets. Control-launched runs retain launch order;
|
|
207
|
+
engine-created runs follow in creation-time and run-id order. Removing a prior
|
|
208
|
+
row does not skip the next row. Listings are live, not a fixed snapshot.
|
|
209
|
+
|
|
210
|
+
A run page returns at most `limit` rows; `limit` does not bound the work done
|
|
211
|
+
to fill it. Without a `status` or `terminal` filter, the runtime selects at most
|
|
212
|
+
`limit` rows on durable summary fields before decoding summaries, reading their
|
|
213
|
+
ancestry, or observing execution, and executor observations enrich only those
|
|
214
|
+
rows. When the executor reports observed status, a `status` or `terminal`
|
|
215
|
+
filter applies to that observed status instead: the runtime selects on the
|
|
216
|
+
remaining filters, observes each selected row, and keeps walking the source
|
|
217
|
+
until the page is full or no runs remain. One page can therefore observe many
|
|
218
|
+
more runs than `limit`, so a host cannot treat the page size as a bound on
|
|
219
|
+
`readExecution` calls. Pending steering counts enrich only the returned rows.
|
|
220
|
+
An exact `runId` filter keeps the direct lookup and applies the remaining
|
|
221
|
+
filters to that observation.
|
|
222
|
+
|
|
223
|
+
`principalId` selects the runs whose `RunSummary.launchedBy.id` it names. A
|
|
224
|
+
run the control plane launched records its launcher; one the engine created
|
|
225
|
+
(a child, a fork, a later round) records none. Over RPC the server restricts a
|
|
226
|
+
reader that `ControlRpcs.RunVisibility` does not make an operator to the runs
|
|
227
|
+
its own principal launched: `List` sets `ListInput.reader` and `Watch` sets
|
|
228
|
+
`WatchInput.reader`, neither of which is on the wire. Such a reader lists and
|
|
229
|
+
watches only those runs, sees only the fires that started them and no
|
|
230
|
+
triggers, receives nothing from a plan partition, and gets `RunNotFound` for
|
|
231
|
+
any other run. A lost-tail watch failure reaches it without partition names.
|
|
232
|
+
`steer`, `signal`, `cancel` and `resume` take the same `reader`: over RPC the
|
|
233
|
+
server sets it, and such a reader mutates only those runs and gets
|
|
234
|
+
`RunNotFound` for any other.
|
|
235
|
+
|
|
236
|
+
The `triggers` and `fires` variants are answered through the `DispatchReader`
|
|
237
|
+
port. A host without one refuses both with `InvalidInput` whose issue is
|
|
238
|
+
`this host serves no trigger store`, never with an empty page.
|
|
239
|
+
|
|
240
|
+
### Receipts
|
|
241
|
+
|
|
242
|
+
`Receipt` is the union every mutation answers:
|
|
243
|
+
|
|
244
|
+
| Member | Fields |
|
|
245
|
+
| ---------------- | ------------------------------------------------------------------- |
|
|
246
|
+
| `Accepted` | `{ receiptId: string; runId?: RunId; handedTo?: RunHost }` |
|
|
247
|
+
| `AlreadyApplied` | `{ receiptId: string; runId?: RunId }` |
|
|
248
|
+
| `Parked` | `{ receiptId: string; planId: string; status: "waiting-approval" }` |
|
|
249
|
+
| `Conflict` | `{ message: string }` |
|
|
250
|
+
| `Terminal` | `{ runId: RunId; status: RunStatus }` |
|
|
251
|
+
|
|
252
|
+
`RunHost` is `{ hostId: string; pid: number }`. An `Accepted` resume with
|
|
253
|
+
`handedTo` names the live host that parked the run and now drives it.
|
|
254
|
+
|
|
255
|
+
### RPC request schemas
|
|
256
|
+
|
|
257
|
+
`PlanInputSchema`, `RunInputSchema`, `ApprovalInputSchema`,
|
|
258
|
+
`SteerInputSchema`, `SignalInputSchema`, `RunMutationInputSchema`,
|
|
259
|
+
`ReasonedMutationInputSchema`, and `CancelInputSchema` are the wire forms.
|
|
260
|
+
`PlanInputSchema` takes `Schema.Json` where the local contract takes `unknown`.
|
|
261
|
+
`ReasonedMutationInputSchema` adds `reason` and omits `principal`, because the
|
|
262
|
+
server stamps the identity it authenticated. `CancelInputSchema` is a named
|
|
263
|
+
alias of it, so cancellation's public contract stays explicit.
|
|
264
|
+
|
|
265
|
+
## ControlError
|
|
266
|
+
|
|
267
|
+
Every stable failure the plane emits. Each class carries a constant `code` a
|
|
268
|
+
client may branch on.
|
|
269
|
+
|
|
270
|
+
| Class | `code` | Fields | Meaning |
|
|
271
|
+
| -------------------- | --------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
272
|
+
| `RunNotFound` | `run_not_found` | `runId` | No run with this id exists. |
|
|
273
|
+
| `PlanNotFound` | `plan_not_found` | `planId` | No plan with this id. Carries an operator-facing `message`. |
|
|
274
|
+
| `PlanDenied` | `plan_denied` | `planId` | The plan was denied. Carries an operator-facing `message`. |
|
|
275
|
+
| `FlowNotFound` | `flow_not_found` | `flowId` | No flow with this id is registered. |
|
|
276
|
+
| `PlanDigestMismatch` | `plan_digest_mismatch` | `planId`, `expected`, `actual` | The submitted plan does not hash to the declared digest. |
|
|
277
|
+
| `EnvelopeMismatch` | `envelope_mismatch` | `planId`, `expected`, `actual` | The plan's effect envelope differs from the declared one. |
|
|
278
|
+
| `ClaimLost` | `claim_lost` | `runId`, `reason?`, `parkedBy?` | The caller's claim lapsed or was fenced by a newer owner, or a live host parked the run. |
|
|
279
|
+
| `AlreadyResolved` | `already_resolved` | `requestId` | This request was already answered. |
|
|
280
|
+
| `InvalidInput` | `invalid_input` | `issue` | The request missed its schema or a stated precondition. |
|
|
281
|
+
| `Unauthorized` | `unauthorized` | `message` | No usable credential for this operation. |
|
|
282
|
+
| `Unavailable` | `unavailable` | `feature`, `ticket` | Not implemented in this deployment. |
|
|
283
|
+
| `TransportError` | `transport_error` | `message`, `retryable`, `cause?` | The request failed before a declared response arrived. |
|
|
284
|
+
| `PersistenceError` | `persistence_failed` | `operation`, `message`, `cause?` | A store operation failed. |
|
|
285
|
+
| `LaunchFailed` | `launch_failed` | `runId`, `message`, `cause?` | The executor refused or could not start the run. |
|
|
286
|
+
| `NoMatchingWait` | `no_matching_wait` | `runId`, `waitName` | A signal named a wait point the run does not have open. |
|
|
287
|
+
| `CredentialConflict` | `credential_conflict` | `id`, `expectedVersion`, `actualVersion` | A credential write lost a compare-and-set race. |
|
|
288
|
+
| `NotificationError` | `notification_closed`, `notification_full`, and existing notification codes | `message`, `notificationId?`, `path?` | Existing notification error class, now preserved by steering locally and through RPC. |
|
|
289
|
+
|
|
290
|
+
| Export | Kind | Meaning |
|
|
291
|
+
| -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
292
|
+
| `ControlErrorSchema` | schema | The single membership list. `ControlClient.isControlError` is `Schema.is` of it, so a class added here reaches both. |
|
|
293
|
+
| `ControlError` | type | `typeof ControlErrorSchema.Type`. |
|
|
294
|
+
|
|
295
|
+
`NoMatchingWait` spells its field `waitName` rather than `name`, because a
|
|
296
|
+
field named `name` on an `Error` subclass shadows `Error.prototype.name`, which
|
|
297
|
+
every renderer in the tree reads.
|
|
298
|
+
|
|
299
|
+
`TransportError.retryable` classifies the transport phase alone. Resend a
|
|
300
|
+
retryable mutation only when its idempotency key makes replay safe; a keyless
|
|
301
|
+
request can have reached the server even when its response was lost.
|
|
302
|
+
|
|
303
|
+
### New public steering refusal channel
|
|
304
|
+
|
|
305
|
+
`Control.steer` now preserves the existing notifications package's
|
|
306
|
+
`NotificationError`, including new `notification_closed` and `notification_full`
|
|
307
|
+
codes. This is an extension to the public error union and the Steer RPC schema;
|
|
308
|
+
it does not introduce a new error class or change successful receipts.
|
|
309
|
+
Existing `notification_unavailable`, `notification_id_reused`, and
|
|
310
|
+
`notification_invalid` failures also now retain this tag, replacing their former
|
|
311
|
+
`PersistenceError` wrapper with operation `control.steer.notification`. Update
|
|
312
|
+
callers that matched that wrapper to handle `NotificationError` directly.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { Effect } from "effect"
|
|
316
|
+
|
|
317
|
+
const outcome = yield* control.steer(input).pipe(
|
|
318
|
+
Effect.map(receipt => ({ kind: "receipt" as const, receipt })),
|
|
319
|
+
Effect.catchTag("/notifications/NotificationError", error => Effect.gen(function*() {
|
|
320
|
+
if (error.code === "notification_closed") return { kind: "start-new-request" as const }
|
|
321
|
+
if (error.code === "notification_full") return { kind: "retry-after-drain" as const }
|
|
322
|
+
return yield* Effect.fail(error)
|
|
323
|
+
}))
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A closed receiver cannot accept a new message, even if its surrounding run is
|
|
328
|
+
still finishing. A full queue has retained nothing; after a boundary drains it,
|
|
329
|
+
the caller can retry the same notification and idempotency key. Control records
|
|
330
|
+
no accepted mutation for a capacity refusal. Duplicate accepted messages retain
|
|
331
|
+
the existing `AlreadyApplied` behavior. Journal I/O failures still become
|
|
332
|
+
`PersistenceError`; their diagnostic cause is retained separately from the fixed
|
|
333
|
+
operator-facing message.
|
|
334
|
+
|
|
335
|
+
Deploy updated clients/UI and server together. Older RPC decoders do not know
|
|
336
|
+
this error variant and may report a decode or transport failure instead of the
|
|
337
|
+
closed/full reason. The change is additive in the source API but is **not fully
|
|
338
|
+
wire-compatible with older exhaustive error decoders**. Do not retry an unknown
|
|
339
|
+
decode failure as if it proved the request was never admitted. Existing error
|
|
340
|
+
codes, successful response shapes, and notification events are unchanged.
|
|
341
|
+
|
|
342
|
+
## ControlLive
|
|
343
|
+
|
|
344
|
+
| Export | Signature |
|
|
345
|
+
| ------- | ----------------------------------------------------------------------------------- |
|
|
346
|
+
| `layer` | `Layer<Control, never, ControlRuntime \| Journal \| NotificationQueue \| Registry>` |
|
|
347
|
+
|
|
348
|
+
Writes delegate to `ControlRuntime`; journal events are observational records
|
|
349
|
+
committed with the state they describe. `watch` only replays and follows
|
|
350
|
+
committed entries. `ControlExecutor` is read optionally, so a composition
|
|
351
|
+
without one records but starts nothing. `DispatchReader` is read optionally
|
|
352
|
+
too: without one, `list` still answers `flows` and `runs`, and refuses
|
|
353
|
+
`triggers` and `fires` with the typed issue `this host serves no trigger store`.
|
|
354
|
+
|
|
355
|
+
## ControlRuntime
|
|
356
|
+
|
|
357
|
+
The persistence port `ControlLive` writes through, and its deterministic
|
|
358
|
+
in-memory implementation. A production adapter fences every owner-sensitive
|
|
359
|
+
write, implements resume as join-or-claim, releases claims on every waiting or
|
|
360
|
+
terminal transition, and translates conflicts into typed failures.
|
|
361
|
+
|
|
362
|
+
| Export | Kind | Signature |
|
|
363
|
+
| ----------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
364
|
+
| `ControlRuntime` | class | `Context.Service<ControlRuntime, Service>` at key `/control/ControlRuntime` |
|
|
365
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
366
|
+
| `layerMemory` | layer | `(options?: MemoryOptions) => Layer<ControlRuntime, never, Crypto>` |
|
|
367
|
+
| `requireApproved` | function | `(token: ApprovalToken) => Effect<ApprovalToken & { _tag: "Approved" }, ApprovalPending \| ApprovalDenied>` |
|
|
368
|
+
| `ApprovalDecision` | schema | Tagged `Pending \| Approved \| Denied` decision |
|
|
369
|
+
| `ApprovalPending`, `ApprovalDenied` | error schemas | Fail-closed gate outcomes; include them in action/flow error schemas |
|
|
370
|
+
|
|
371
|
+
### Service
|
|
372
|
+
|
|
373
|
+
| Group | Members |
|
|
374
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
375
|
+
| Plans | `plan(input: PlanInput) => Effect<PlanOutcome, FlowNotFound \| InvalidInput \| PersistenceError>`, `getPlan(planId)`, `listPlanIds` |
|
|
376
|
+
| Approvals | `authorizeApproval(request)`, `lookupApproval(target)`, `registerApproval(nodeTarget)`, `resolveApproval(token, decision, principal, scope?)`, `installBulkGrant(token, envelope, scope)`, `grants` |
|
|
377
|
+
| Runs | `launch(planId, digest, envelope) => Effect<LaunchResult, ...>`, `getRun(runId)`, `queryRuns({ filters?, cursor?, limit })`, `listRuns`, `listFlows` |
|
|
378
|
+
| Signal history | `deliverSignal(runId, signal)`, `deliveredSignals(runId)` |
|
|
379
|
+
| Resume delegation | `requestResume(runId) => Effect<number, ...>`, `pendingResumes`, `clearResume(runId, sequence)` |
|
|
380
|
+
| Ownership | `registerFiber(runId, fiber)`, `interrupt(runId, settle?)`, `resume(runId, options?)`, `claimFence(runId)`, `releasePending(runId, fence)`, `writeStatus(runId, fence, status)` |
|
|
381
|
+
| Identity | `stampPrincipal(submitted?)`, `lookupMutation(key, fingerprint)`, `recordMutation(key, fingerprint, receipt)` |
|
|
382
|
+
|
|
383
|
+
`queryRuns` accepts `RunQuery`: optional `flowId`, `status`, `terminal`,
|
|
384
|
+
`parentRunId`, `lineageId`, `since` (inclusive creation epoch ms), `until`
|
|
385
|
+
(exclusive), and `runIds` filters, an optional `order` (`newest` or `oldest`
|
|
386
|
+
creation time), an optional `RunCursor`, and a required integer `limit` from 1
|
|
387
|
+
through 500. `Control.list` resolves a `runs` request's `triggerId` filter to
|
|
388
|
+
`runIds` from the trigger's recorded fires. It returns `RunPage` with `items` and optional `nextCursor`.
|
|
389
|
+
`RunCursor` contains `source` (0 for control launches, 1 for engine runs),
|
|
390
|
+
`sequence`, `createdAt`, and `runId`. Adapters must select the page before
|
|
391
|
+
summary decoding and ancestry projection. SQL reads one extra ordering key to
|
|
392
|
+
determine continuation. `listRuns` remains the full inventory for recovery and
|
|
393
|
+
journal partition discovery; interactive listings use `queryRuns`.
|
|
394
|
+
|
|
395
|
+
Steering uses `NotificationQueue.enqueue` and `NotificationQueue.drain`.
|
|
396
|
+
`enqueueSteer` and `drainSteering` have been removed from the runtime port and
|
|
397
|
+
both adapters. The legacy signal-history reader and database migrations remain;
|
|
398
|
+
old steering rows are not drained or delivered by the runtime.
|
|
399
|
+
|
|
400
|
+
`interrupt` must be called without mutation locks. Its optional `settle` wrapper
|
|
401
|
+
runs only after the fiber's finalizers finish and wraps the fenced status
|
|
402
|
+
reconciliation. `ControlLive` uses it to commit the terminal event alongside
|
|
403
|
+
the status. Direct callers may omit it.
|
|
404
|
+
|
|
405
|
+
`resume` takes `{ scope?: "launched" \| "any" }`. `scope: "launched"`
|
|
406
|
+
restricts claims to the shared `control_runs` launch index. Both public
|
|
407
|
+
spellings, `Control.resume` and `Control.run` with a Resume input, and every
|
|
408
|
+
steer wake pass it. `scope: "any"`, also the default, is a trusted low-level
|
|
409
|
+
runtime capability for hosts that can drive the claimed execution.
|
|
410
|
+
An owned non-terminal run is joined without replacing its fence, including
|
|
411
|
+
`accepted`; a run released by `releasePending` can be claimed again.
|
|
412
|
+
|
|
413
|
+
Explicit resume journals `control.run.resume` and never calls
|
|
414
|
+
`ControlExecutor.resumeRun`. A caller or journal subscriber must drive the
|
|
415
|
+
execution; polling `pendingResumes` does not take up a resume the caller
|
|
416
|
+
claimed. A run a live host parked fails the claim with `ClaimLost` naming the
|
|
417
|
+
host in `parkedBy`. `Control.resume` then hands it to that host: it records
|
|
418
|
+
`requestResume(runId, { consent })`, where `consent` is the journal sequence of
|
|
419
|
+
its `control.run.resume`, and answers `Accepted` with `handedTo`.
|
|
420
|
+
`pendingResumes` reports that `consent`, and the parking host records the
|
|
421
|
+
per-release retry permission under it before it re-drives the run.
|
|
422
|
+
A suspended engine-created run stays unclaimed and receives an `Accepted`
|
|
423
|
+
receipt for the journal intent. A live peer's owned run fails with `ClaimLost`.
|
|
424
|
+
A running engine-created run outside the launch index also fails with
|
|
425
|
+
`ClaimLost` when its owner is dead; its persisted execution state is preserved.
|
|
426
|
+
Node-approval decisions use the durable resume delegation instead.
|
|
427
|
+
|
|
428
|
+
`registerApproval` is idempotent and returns the token with its current
|
|
429
|
+
tagged decision. `Pending` parks, `Approved` opens a gate, and `Denied` fails it.
|
|
430
|
+
Use `requireApproved` to enforce this distinction. A registration that disagrees
|
|
431
|
+
with the stored digest or envelope is refused exactly as `lookupApproval`
|
|
432
|
+
refuses it. Terminal decisions carry `decisionPrincipal` and `decidedAt`;
|
|
433
|
+
`Approved` also carries `scope`. The low-level `resolveApproval` defaults scope
|
|
434
|
+
to `once`; when installing a wider grant, pass that same scope explicitly.
|
|
435
|
+
`Control.approve` does this automatically. Resolution checks the owning
|
|
436
|
+
`ApprovalAuthority` again and may fail with `Unauthorized`; it does not install
|
|
437
|
+
a grant. `installBulkGrant` is a trusted storage port, not an authorization API.
|
|
438
|
+
|
|
439
|
+
For a new decision, authenticate the principal and call `authorizeApproval`
|
|
440
|
+
before target reads or receipt replay. Then call `lookupApproval`,
|
|
441
|
+
`resolveApproval` exactly once with an authority recheck, `installBulkGrant` only
|
|
442
|
+
on approval, and journal the decision. Commit the decision, grant, journal entry,
|
|
443
|
+
receipt, and any node resume delegation atomically. Resolution must not require
|
|
444
|
+
an installed grant or a flushed journal decision.
|
|
445
|
+
|
|
446
|
+
Migration 6004 preserves legacy rows. Unknown old terminal decisions are
|
|
447
|
+
refused with `PersistenceError`; pending rows remain pending. Preserve the old
|
|
448
|
+
database and start a new run/request instead of inferring approval from a grant.
|
|
449
|
+
|
|
450
|
+
`requestResume` returns the durable sequence `clearResume` checks, so a resume
|
|
451
|
+
requested while one is being taken up is not lost with it.
|
|
452
|
+
|
|
453
|
+
### Models
|
|
454
|
+
|
|
455
|
+
| Type | Shape |
|
|
456
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
457
|
+
| `StoredPlan` | `{ card: PlanCard; decodedInput: unknown; decision: "pending" \| "approved" \| "denied" }` |
|
|
458
|
+
| `ApprovalToken` | `{ tokenId: string; target: ApprovalTarget } & ApprovalDecision`; `_tag: "Pending"`, or `_tag: "Approved"` with principal/time/scope, or `_tag: "Denied"` with principal/time |
|
|
459
|
+
| `BulkGrant` | `{ tokenId: string; envelope: Envelope; scope: GrantScope; installedAt: number }` |
|
|
460
|
+
| `LaunchResult` | `{ _tag: "Started"; receipt; run }` or `{ _tag: "Parked"; receipt }` |
|
|
461
|
+
| `PlanOutcome` | `{ card: PlanCard; created: boolean }`. `created` is what lets `plan` journal one creation per plan rather than one per retry. |
|
|
462
|
+
| `MutationRecord` | `{ fingerprint: string; receipt: Receipt }` |
|
|
463
|
+
| `PendingResume` | `{ runId: RunId; sequence: number; requestedAtMs: number }` |
|
|
464
|
+
| `MemoryFlow` | `{ flowId; description; deployClass; envelope; executionDigest?; decode?; plan? }`. The optional execution identity is included in the approved card; `decode` validates input and `plan` projects it into the keyed node graph, answering `{ plan, statuses?, graph? }`. |
|
|
465
|
+
| `MemoryOptions` | `{ flows?: MemoryFlow[]; now?: () => number; principal?: Omit<Principal, "stampedAt">; approvalAuthority?: ApprovalAuthority.Service }` |
|
|
466
|
+
|
|
467
|
+
`layerMemory` models the production fence and approval ordering seams but keeps
|
|
468
|
+
everything in a `Map`. Nothing it decides survives the process.
|
|
469
|
+
|
|
470
|
+
## ApprovalAuthority
|
|
471
|
+
|
|
472
|
+
Host-owned approval policy, separate from authentication and granted workflow
|
|
473
|
+
capabilities. Import `@smthrs/control/ApprovalAuthority` or its root namespace.
|
|
474
|
+
|
|
475
|
+
- `Request`: `{ principal, target, decision: "approved" | "denied", scope }`.
|
|
476
|
+
- `Service.authorize(request)`: `Effect<void, Unauthorized | PersistenceError>`.
|
|
477
|
+
- `Delegation`: schema/type for `{ principal: { id, kind }, scopes, targets }`.
|
|
478
|
+
Exact scopes are `once`, `run`, `remembered`; target kinds are `Plan`, `Node`.
|
|
479
|
+
- `make(delegations)`: validates and snapshots up to 1,024 explicit delegations;
|
|
480
|
+
returns `Effect<Service, InvalidInput>`. Empty configuration denies everyone.
|
|
481
|
+
- `local`: default policy for the fixed `local/operator` identity only. Custom
|
|
482
|
+
identities, including bearer and agent identities, need explicit delegation.
|
|
483
|
+
A principal's `kind` is not itself a role grant. The memory adapter's own
|
|
484
|
+
default also delegates its `memory/test` identity; `local` never does.
|
|
485
|
+
|
|
486
|
+
`Control.approve` and `deny` check before reads and receipt replay. Both runtime
|
|
487
|
+
adapters check again at resolution. Denial requires a delegated target kind but
|
|
488
|
+
does not require a grant scope because it grants nothing. See the
|
|
489
|
+
[approval guide](./guides/approvals.md#who-may-decide) for host composition.
|
|
490
|
+
|
|
491
|
+
## SqlControlRuntime
|
|
492
|
+
|
|
493
|
+
The durable `ControlRuntime` over a SQL database and the fenced run store from
|
|
494
|
+
[`@smthrs/run-store`](/api/run-store).
|
|
495
|
+
|
|
496
|
+
| Export | Kind | Signature |
|
|
497
|
+
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
498
|
+
| `DurableFlow` | type | `MemoryFlow`, so one catalog serves either runtime. |
|
|
499
|
+
| `Options` | interface | `{ flows?: ReadonlyArray<DurableFlow>; loadFlows?: () => Effect<ReadonlyArray<DurableFlow>, PersistenceError>; owner?: Ownership.OwnerId; isAlive?: Ownership.LivenessCheck; principal?: Omit<Principal, "stampedAt">; approvalAuthority?: ApprovalAuthority.Service }`; with `isAlive`, `resume` takes over a running run whose owner is gone once its lease expired |
|
|
500
|
+
| `migrate` | effect | `Effect<void, PersistenceError, SqlClient>`. Creates every control-plane table, idempotently. |
|
|
501
|
+
| `make` | function | `(options?: Options) => Effect<Service, PersistenceError, Crypto \| DurableWriter \| SqlClient \| RunStore>` |
|
|
502
|
+
| `layer` | layer | `(options?: Options) => Layer<ControlRuntime, PersistenceError, Crypto \| DurableWriter \| SqlClient \| RunStore>` |
|
|
503
|
+
| `layerWithStore` | layer | The same, with `RunStore.layer` provided. |
|
|
504
|
+
|
|
505
|
+
Dead-owner takeovers with `scope: "launched"` require a launch-index entry;
|
|
506
|
+
unindexed engine continuations remain unchanged.
|
|
507
|
+
|
|
508
|
+
`loadFlows` replaces the static `flows` or default system catalog. It runs afresh
|
|
509
|
+
for each `plan` and `listFlows` operation, and one plan uses one complete catalog
|
|
510
|
+
snapshot. Loader failures remain typed `PersistenceError`s. A host using a
|
|
511
|
+
refreshable registry can therefore plan from newly discovered or edited flows
|
|
512
|
+
without restarting the runtime. Stored plans and approvals retain the execution
|
|
513
|
+
identity they originally captured; refreshing the catalog does not rewrite them.
|
|
514
|
+
|
|
515
|
+
Omitting `owner` mints one synthetic identity for this runtime only, so
|
|
516
|
+
separately constructed runtimes cannot cross each other's fences. Hosts that
|
|
517
|
+
can report a real process identity should supply it.
|
|
518
|
+
|
|
519
|
+
The run lifecycle is not reimplemented here. `RunStore` owns it, and every
|
|
520
|
+
ownership move is a single SQL compare-and-swap. See
|
|
521
|
+
[Ownership, fences, and claims](./concepts/ownership.md) for the status
|
|
522
|
+
mapping.
|
|
523
|
+
|
|
524
|
+
## ControlExecutor
|
|
525
|
+
|
|
526
|
+
The acceptance port from the control plane into a real run executor.
|
|
527
|
+
|
|
528
|
+
`ControlExecutor.makeReadOnly({ readExecution, readExecutions })` accepts optional
|
|
529
|
+
point and batch readers in one options object. Omit the object for a host without
|
|
530
|
+
engine observations. Mutation methods remain refused.
|
|
531
|
+
|
|
532
|
+
| Export | Kind | Signature |
|
|
533
|
+
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
534
|
+
| `ControlExecutor` | class | `Context.Service<ControlExecutor, Service>` at key `/control/ControlExecutor` |
|
|
535
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
536
|
+
| `makeNoop` | function | `(overrides?: Partial<Service>) => Service`. Accepts every launch as `pending` and starts nothing. |
|
|
537
|
+
| `makeObserving` | function | `(service: Service) => Service`. The same executor with `launch` and `resumeRun` refused as defects, for a host composed to observe runs and drive none. |
|
|
538
|
+
| `layer` | layer | `(implementation: Service) => Layer<ControlExecutor>` |
|
|
539
|
+
| `layerNoop` | layer | `(overrides?: Partial<Service>) => Layer<ControlExecutor>` |
|
|
540
|
+
|
|
541
|
+
### Service
|
|
542
|
+
|
|
543
|
+
| Method | Signature |
|
|
544
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
545
|
+
| `launch` | `(input: Launch) => Effect<Acceptance, LaunchFailed>` |
|
|
546
|
+
| `requestComplete` | Optional `(input: { runId: RunId; receiptId: string }) => Effect<Receipt, PersistenceError>`. Host close after the last native module completes. Since 1.0.0. |
|
|
547
|
+
| `requestCancel` | `(input: CancelRequest) => Effect<CancelRecord, PersistenceError>` |
|
|
548
|
+
| `deliverSignal` | `(input: Signal) => Effect<SignalDelivery, PersistenceError>` |
|
|
549
|
+
| `resumeRun` | `(input: ResumeRequest) => Effect<ResumeUptake, PersistenceError>` |
|
|
550
|
+
| `settleCancelledPark` | `(input: CancelRequest) => Effect<void, PersistenceError>` |
|
|
551
|
+
|
|
552
|
+
### Models
|
|
553
|
+
|
|
554
|
+
| Type | Shape |
|
|
555
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
556
|
+
| `Launch` | `{ plan: StoredPlan; run: RunSummary }`. `run.runId` is the execution id the executor must start. |
|
|
557
|
+
| `Acceptance` | `"accepted"` (taken now) or `"pending"` (queued). |
|
|
558
|
+
| `CancelRequest`, `ResumeRequest` | `{ runId: RunId }` |
|
|
559
|
+
| `CancelTerminal` | `{ _tag: "Terminal"; status: "completed" \| "failed" \| "cancelled" }`. The engine's own status, which the plane cannot read itself. |
|
|
560
|
+
| `CancelRecord` | `"recorded" \| "already-requested" \| "unknown" \| CancelTerminal` |
|
|
561
|
+
| `ResumeUptake` | `"resuming" \| "unknown"` |
|
|
562
|
+
| `Signal` | `{ runId: RunId; signal: SignalPayload }` |
|
|
563
|
+
| `SignalDelivery` | `"delivered" \| "no-match" \| "unknown"` |
|
|
564
|
+
|
|
565
|
+
`settleCancelledPark` is called after the cancel mutation commits, never inside
|
|
566
|
+
it: driving a run re-enters the engine, whose writes would wait on the writer
|
|
567
|
+
the transaction holds.
|
|
568
|
+
|
|
569
|
+
## DispatchReader
|
|
570
|
+
|
|
571
|
+
The read port from the control plane into a host's trigger store. `Control.list`
|
|
572
|
+
answers `{ _tag: "triggers" }` and `{ _tag: "fires" }` through it. The port
|
|
573
|
+
lives here rather than in `@smthrs/triggers` because that package depends on
|
|
574
|
+
this one (its scheduler launches runs through `Control`), so the adapter over a
|
|
575
|
+
real `TriggerStore` is composed by the host.
|
|
576
|
+
|
|
577
|
+
| Export | Kind | Signature |
|
|
578
|
+
| ---------------- | -------- | --------------------------------------------------------------------------- |
|
|
579
|
+
| `DispatchReader` | class | `Context.Service<DispatchReader, Service>` at key `/control/DispatchReader` |
|
|
580
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
581
|
+
| `makeNone` | function | `() => Service`. Both methods fail with `refuse()`. |
|
|
582
|
+
| `refuse` | function | `() => InvalidInput` with code `invalid_input` and issue `noStoreIssue`. |
|
|
583
|
+
| `noStoreIssue` | constant | `"this host serves no trigger store"`. |
|
|
584
|
+
| `layer` | layer | `(implementation: Service) => Layer<DispatchReader>` |
|
|
585
|
+
| `layerNone` | layer | `Layer<DispatchReader>` providing `makeNone()`. |
|
|
586
|
+
|
|
587
|
+
### Service
|
|
588
|
+
|
|
589
|
+
| Method | Signature |
|
|
590
|
+
| ------- | ----------------------------------------------------------------------------------- |
|
|
591
|
+
| `list` | `(request: TriggersRequest) => Effect<ReadonlyArray<TriggerSummary>, ControlError>` |
|
|
592
|
+
| `fires` | `(request: FiresRequest) => Effect<ReadonlyArray<FireSummary>, ControlError>` |
|
|
593
|
+
|
|
594
|
+
Each method receives the whole listing request and answers every row it has,
|
|
595
|
+
newest fire first. A reader may narrow by `filters`; `Control.list` applies the
|
|
596
|
+
same filters again and pages the rows with `cursor` and `limit`, so a reader
|
|
597
|
+
that returns every row is still correct and both variants page exactly as
|
|
598
|
+
`flows` and `runs` do. `TriggersRequest` and `FiresRequest` are the two
|
|
599
|
+
`ListRequest` members by tag.
|
|
600
|
+
|
|
601
|
+
## ControlRpcs
|
|
602
|
+
|
|
603
|
+
The schema-backed RPC projection of the service.
|
|
604
|
+
|
|
605
|
+
| Export | Kind | Meaning |
|
|
606
|
+
| --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
607
|
+
| `ControlRpcs` | group | Ten procedures: `Plan`, `Run`, `Approve`, `Deny`, `Steer`, `Signal`, `Cancel`, `Resume`, `List`, and the streaming `Watch`. Carries the `ControlAuth` middleware. |
|
|
608
|
+
| `ControlPrincipal` | class | The authenticated principal, provided to every handler. Key `/control/ControlPrincipal`. |
|
|
609
|
+
| `ControlAuth` | class | The middleware boundary. Key `/control/ControlAuth`, error `Unauthorized`. |
|
|
610
|
+
| `Call` | interface | `{ rpc: string; payload: unknown }`: the frame a boundary is authenticating, absent at a transport edge. |
|
|
611
|
+
| `Authenticator` | interface | `{ authenticate: (headers: Record<string, string>, call?: Call) => Effect<Principal, Unauthorized> }` |
|
|
612
|
+
| `BearerAuthOptions` | interface | `{ token: string; principal: Omit<Principal, "stampedAt">; now?: () => number }` |
|
|
613
|
+
| `bearerCredential` | function | `(headers) => string \| undefined`. The bearer a request carries, or nothing. |
|
|
614
|
+
| `bearerAuthenticator` | function | `(options: BearerAuthOptions) => Authenticator`. Constant-time comparison; missing, malformed, empty, and incorrect credentials all fail closed identically. |
|
|
615
|
+
| `anyAuthenticator` | function | `(authenticators: ReadonlyArray<Authenticator>) => Authenticator`. The first to accept answers; none accepting fails with the last refusal. |
|
|
616
|
+
| `layerAuth` | layer | `(authenticator: Authenticator) => Layer<ControlAuth>` |
|
|
617
|
+
| `layerBearerAuth` | layer | `(options: BearerAuthOptions) => Layer<ControlAuth>` |
|
|
618
|
+
| `layerNoopAuth` | layer | `(principal?: Principal) => Layer<ControlAuth>`. Authenticates nothing. |
|
|
619
|
+
| `ControlDefect` | schema | The defect schema of every procedure. Encodes any non-string defect as `{ name: "Error", message: defectMessage }`; decodes like `Schema.Defect()`. |
|
|
620
|
+
| `defectMessage` | constant | `"Something went wrong on our side. Not your fault."` |
|
|
621
|
+
|
|
622
|
+
`List` and `Watch` declare the whole `ControlError` union rather than restating
|
|
623
|
+
its members.
|
|
624
|
+
|
|
625
|
+
A handler defect, an untyped failure no procedure declares, reaches a client as
|
|
626
|
+
a `Die` whose defect is `{ "name": "Error", "message": "Something went wrong on
|
|
627
|
+
our side. Not your fault." }`. Its raw message and stack never cross the wire;
|
|
628
|
+
the server logs them through `ControlServer.logDefect`. A string defect passes
|
|
629
|
+
unchanged, so a payload that fails to decode still answers with the request
|
|
630
|
+
decoder's own sentence. The encoded shape is the one `Schema.Defect()` decodes,
|
|
631
|
+
so older clients and servers read each other's defects.
|
|
632
|
+
|
|
633
|
+
## ScopedToken
|
|
634
|
+
|
|
635
|
+
Scoped, expiring tokens minted under a gateway's bearer credential: an
|
|
636
|
+
HMAC-SHA256 grant of named procedures, optionally confined to one run or flow,
|
|
637
|
+
that the gateway verifies with the credential it already holds. The wire form
|
|
638
|
+
is `smt1.<claims>.<signature>`. `smthrs token mint` is the command over it.
|
|
639
|
+
|
|
640
|
+
| Export | Kind | Meaning |
|
|
641
|
+
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
642
|
+
| `scopes`, `Scope` | constant | `read:runs`, `write:runs`, and `approve:runs`, each naming the procedures it grants across `ControlRpcs` and the gateway's `GatewayRpcs`. |
|
|
643
|
+
| `scopeNames` | constant | The scope names in declaration order. |
|
|
644
|
+
| `Claims` | schema | `{ v: 1; id; procedures; runId?; flowId?; iat; exp }`, milliseconds since the epoch. |
|
|
645
|
+
| `mint` | function | `(options: MintOptions) => Effect<Minted>`. Dies on an empty key, no scopes, or a non-positive lifetime. |
|
|
646
|
+
| `verify` | function | `(key, token, now) => Effect<Claims, Unauthorized>`. Signature and expiry; every malformation is the same refusal. |
|
|
647
|
+
| `authorizes` | function | `(claims, call) => boolean`. The procedure must be named; a confined token authorizes only calls naming its run or flow. |
|
|
648
|
+
| `authenticator` | function | `(options: AuthenticatorOptions) => Authenticator`. Signature and expiry always; procedure and confinement when the boundary knows the call. |
|
|
649
|
+
| `isScopedToken` | function | `(credential) => boolean`. |
|
|
650
|
+
| `procedures` | function | `(scopes) => ReadonlyArray<string>`, each once. |
|
|
651
|
+
| `prefix` | constant | `"smt1"`. |
|
|
652
|
+
|
|
653
|
+
## ControlServer
|
|
654
|
+
|
|
655
|
+
| Export | Meaning |
|
|
656
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
657
|
+
| `layer` | The handlers, delegating to `Control`. Every mutation that records who asked reads `ControlPrincipal` and stamps it rather than forwarding what the client sent. |
|
|
658
|
+
| `layerHttp` | Mounts both protocols on the ambient `HttpRouter`: unary procedures over `POST /rpc`, and `watch` over `WebSocket /rpc/ws`. |
|
|
659
|
+
| `logDefect` | `(cause: Cause<unknown>) => Effect<void>`. Logs a handler's raw defect at error level and ignores typed failures. Tap it around a handler with `Effect.tapCause` or `Stream.tapCause`. |
|
|
660
|
+
|
|
661
|
+
## ControlClient
|
|
662
|
+
|
|
663
|
+
| Export | Kind | Signature |
|
|
664
|
+
| ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
|
|
665
|
+
| `ClientConfig` | interface | `{ url: string; credential?: string }`. `credential` is attached as a bearer token on every HTTP RPC request. |
|
|
666
|
+
| `layer` | layer | `(config: ClientConfig) => Layer<Control, ...>` |
|
|
667
|
+
| `isControlError` | refinement | `(value: unknown) => value is ControlError`, derived from `ControlErrorSchema`. |
|
|
668
|
+
|
|
669
|
+
Unary procedures use HTTP at `url`; `watch` uses the abstract WebSocket the
|
|
670
|
+
platform layer supplies. Declared control failures cross the wire as
|
|
671
|
+
themselves; everything else becomes a `TransportError` whose `retryable` flag
|
|
672
|
+
classifies the transport phase.
|
|
673
|
+
|
|
674
|
+
In rc.0, `ClientConfig.credential` authenticates HTTP calls only; it does not
|
|
675
|
+
authenticate the `watch` WebSocket upgrade. Authenticated remote watch requires
|
|
676
|
+
a socket implementation that sends the Authorization header, or a trusted
|
|
677
|
+
proxy that authenticates the caller and supplies it. The default client fails
|
|
678
|
+
closed against a credentialed gateway. Tokens in URL query strings are not
|
|
679
|
+
supported.
|
|
680
|
+
|
|
681
|
+
## Lineage
|
|
682
|
+
|
|
683
|
+
Run ancestry as the control plane reads it. See
|
|
684
|
+
[Run lineage](./concepts/lineage.md).
|
|
685
|
+
|
|
686
|
+
| Export | Kind | Signature |
|
|
687
|
+
| ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
688
|
+
| `Origin` | schema and type | `"child" \| "fork" \| "continuation"` |
|
|
689
|
+
| `Ancestry` | interface | `{ parentRunId?: string; roundOrdinal?: number; forked?: boolean }` |
|
|
690
|
+
| `runDecisionEventType` | constant | `"flows.engine.run-decision"` |
|
|
691
|
+
| `forkCreatedEventType` | constant | `"flows.time-travel.fork-created"` |
|
|
692
|
+
| `lineageEventType` | constant | `"control.run.lineage"` |
|
|
693
|
+
| `originOf` | function | `(ancestry: Ancestry) => Origin \| undefined`. A fork wins over a plain child, because a fork records a parent too. |
|
|
694
|
+
| `derive` | function | `(event: ControlEvent) => ControlEvent \| undefined`. The ancestry delta one entry discloses, if it discloses one. |
|
|
695
|
+
| `expand` | function | `(event: ControlEvent) => ReadonlyArray<ControlEvent>`. The entry plus any delta. |
|
|
696
|
+
|
|
697
|
+
## Cancellation
|
|
698
|
+
|
|
699
|
+
Cancellation attribution as the plane reads it back. See
|
|
700
|
+
[Cancellation attribution](./concepts/cancellation.md).
|
|
701
|
+
|
|
702
|
+
| Export | Kind | Signature |
|
|
703
|
+
| ---------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
704
|
+
| `requestedEventType` | constant | `"control.run.cancel-requested"` |
|
|
705
|
+
| `interruptedEventType` | constant | `"flows.engine.interrupted"` |
|
|
706
|
+
| `Request` | interface | `{ requestedAt: number; principal?: Principal; reason?: string }` |
|
|
707
|
+
| `Evidence` | interface | `{ runId: string; parentRunId?: string; cancelRequestedAt?: number; cancelledAt?: number }` |
|
|
708
|
+
| `Input` | interface | `{ runs: ReadonlyArray<Evidence>; requests: ReadonlyMap<string, Request> }` |
|
|
709
|
+
| `attribute` | function | `(input: Input) => ReadonlyMap<string, Cancellation>`. Pure and scope-independent: it reads what it is handed and never queries. |
|
|
710
|
+
|
|
711
|
+
## Steering
|
|
712
|
+
|
|
713
|
+
The steer lifecycle as the plane reads it back. See
|
|
714
|
+
[Steer a running agent](./guides/steer-a-run.md).
|
|
715
|
+
|
|
716
|
+
| Export | Kind | Signature |
|
|
717
|
+
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
718
|
+
| `enqueuedEventType` | constant | `"control.steer.enqueued"`, written by `Control.steer`. |
|
|
719
|
+
| `promotedEventType` | constant | `"flows/notifications/Promoted"`, written by the queue. |
|
|
720
|
+
| `deliveredEventType` | constant | `"control.steer.delivered"`, derived. |
|
|
721
|
+
| `derive` | function | `(event: ControlEvent) => ReadonlyArray<ControlEvent>`. One delta per message a promotion named. A promotion that named nothing derives nothing. |
|
|
722
|
+
| `expand` | function | `(event: ControlEvent) => ReadonlyArray<ControlEvent>` |
|
|
723
|
+
|
|
724
|
+
## Health
|
|
725
|
+
|
|
726
|
+
Observational health for flows and native sessions. See
|
|
727
|
+
[Configure observational health](./guides/observe-health.md).
|
|
728
|
+
|
|
729
|
+
`HealthChecker<C>` accepts a read-only `ProbeContext` and schema-decoded config,
|
|
730
|
+
returning an Effect of `ProbeReport`. `makeRegistry(config, kind)` admits host
|
|
731
|
+
bindings and policies synchronously; `registry.resolve(key)` returns the selected
|
|
732
|
+
`ResolvedCheck`. `evaluate(check, context, stamp)` runs it with a timeout and safe
|
|
733
|
+
failure codes. The host rechecks ownership and commits the `HealthObservation`
|
|
734
|
+
before publication.
|
|
735
|
+
|
|
736
|
+
`rollup(input)` combines authoritative lifecycle, optional independent base
|
|
737
|
+
health, and the latest `{ observation, sequence }` into `StatusRollup`. The wire
|
|
738
|
+
axes are `state`, `activity`, `health`, `attention`, and `freshness`. Attention
|
|
739
|
+
is `none`, `awaiting-approval`, `needs-input`, `needs-resume`, or `unhealthy`;
|
|
740
|
+
`needs-resume` (reason `released`) marks a run parked over executions its owner
|
|
741
|
+
released, which only an explicit resume restarts. Provenance
|
|
742
|
+
carries checker/monitor IDs, opaque incarnation, evidence position, durable
|
|
743
|
+
version, observation time, and expiry. `latestObservation` compares only matching
|
|
744
|
+
incarnations and uses journal order for equal evidence. `runIncarnation` derives
|
|
745
|
+
an opaque fingerprint from current run ownership and lifecycle.
|
|
746
|
+
|
|
747
|
+
`CheckPolicy` configures interval, timeout, TTL, no-progress grace (`stallAfterMs`),
|
|
748
|
+
and failure backoff. `defaultPolicy` uses 5s/2s/20s/120s respectively and caps
|
|
749
|
+
backoff at 60s. Defaults report unknown semantic activity; no checker output grants
|
|
750
|
+
approval or authorizes a remedy.
|
|
751
|
+
|
|
752
|
+
## JevSessionChecker
|
|
753
|
+
|
|
754
|
+
The one registered checker that reads semantic activity, bound by the ID
|
|
755
|
+
`jev.session`. See [Configure observational health](./guides/observe-health.md).
|
|
756
|
+
|
|
757
|
+
`makeJevSessionChecker({ evaluator, timeoutMs })` uses the host's existing
|
|
758
|
+
subscription judge. `Health.makeRegistry(config, kind, evaluator)` binds it to
|
|
759
|
+
`jev.session`. It asks one choice question over `working`, `idle`, and
|
|
760
|
+
`needs-input`, plus a boolean question about waiting for a person. Evidence is
|
|
761
|
+
`alive`, `exitCode`, and the newest `jevStateTailCharacters` of output.
|
|
762
|
+
It reads no gateway key and has no separate authentication path.
|
|
763
|
+
|
|
764
|
+
An answer becomes a report only at confidence `jevConfidenceFloor` or above:
|
|
765
|
+
`needs-input` reports reason `prompt-detected`, `working` and `idle` report `ok`.
|
|
766
|
+
A reading below the floor is Jev's own answer and reports
|
|
767
|
+
`{ activity: "unknown", reason: "ok" }`.
|
|
768
|
+
|
|
769
|
+
There is no fallback to another model or to a healthy-looking answer. An
|
|
770
|
+
unexposed output tail and a session that is not alive keep the lifecycle report,
|
|
771
|
+
because there is nothing to ask about. Every other way the probe cannot ask
|
|
772
|
+
fails it with `JevProbeError`, whose `reason` is `unconfigured` (no host evaluator), `http` (with the provider's `status`), `timeout`,
|
|
773
|
+
`unreachable`, or `malformed` (a body that does not answer the question asked).
|
|
774
|
+
`Health.evaluate` records a failing probe as `outcome: "error"` with reason
|
|
775
|
+
`probe-error` and no report, so `rollup` reads the subject `stale`, activity
|
|
776
|
+
`unknown`, health `unknown`, reason `probe-error`, never healthy, and
|
|
777
|
+
`CheckPolicy.backoff` spaces the retries. A host that binds `jev.session` must
|
|
778
|
+
supply its evaluator through `Health.makeRegistry(config, kind, evaluator)`.
|
|
779
|
+
|
|
780
|
+
`jevRequestTimeoutMs` is the deadline on the call and `jevProbeTimeoutMs` the
|
|
781
|
+
wider probe budget this checker asks a binding for, so the typed `timeout`
|
|
782
|
+
failure surfaces instead of a bare `probe-timeout`.
|
|
783
|
+
|
|
784
|
+
## Monitor
|
|
785
|
+
|
|
786
|
+
Run health over the control plane. See
|
|
787
|
+
[Monitor a run and heal it](./guides/monitor-runs.md).
|
|
788
|
+
|
|
789
|
+
| Export | Kind | Signature |
|
|
790
|
+
| -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
791
|
+
| `Health` | schema and type | `"healthy" \| "stalled" \| "wedged-node" \| "runaway-loop" \| "awaiting-human" \| "failing" \| "unknown"` |
|
|
792
|
+
| `Observation` | interface | `{ summary?: RunSummary; events: ReadonlyArray<ControlEvent>; beatsWithoutProgress: number; stallBeats: number; roundBound?: number }` |
|
|
793
|
+
| `classify` | function | `(observation: Observation) => Health`. Pure. |
|
|
794
|
+
| `Remedy` | type | `"resume" \| "cancel" \| "none"` |
|
|
795
|
+
| `remedyFor` | function | `(health: Health) => Remedy` |
|
|
796
|
+
| `Beat` | interface | `{ beat: number; health: Health; sequence: number; healed?: Remedy; receipt?: Receipt }` |
|
|
797
|
+
| `Report` | interface | `{ runId: RunId; beats: ReadonlyArray<Beat>; health: Health }` |
|
|
798
|
+
| `Options` | interface | `{ runId; monitorId?; intervalMs?; maxChecks?; stallBeats?; roundBound?; autoHeal?; heal? }` |
|
|
799
|
+
| `run` | function | `(options: Options) => Effect<Report, ControlError, Control \| Journal>` |
|
|
800
|
+
| `attemptStartedEventType` | constant | `"flows.engine.attempt-started"` |
|
|
801
|
+
| `attemptFinishedEventType` | constant | `"flows.engine.attempt-finished"` |
|
|
802
|
+
| `beatEventType` | constant | `"control.monitor.beat"` |
|
|
803
|
+
| `healedEventType` | constant | `"control.monitor.healed"` |
|
|
804
|
+
|
|
805
|
+
Defaults: `monitorId` is `default`, `intervalMs` is 1,000, `maxChecks` is 10,
|
|
806
|
+
`stallBeats` is 3, `roundBound` is 32, and `autoHeal` is empty.
|
|
807
|
+
|
|
808
|
+
## Channels
|
|
809
|
+
|
|
810
|
+
Verified ingress. A channel verifies opaque transport data before it decodes or
|
|
811
|
+
maps it, and dispatches the result through `Control`.
|
|
812
|
+
|
|
813
|
+
| Export | Kind | Signature |
|
|
814
|
+
| -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
|
|
815
|
+
| `Channels` | interface and tag | `{ register; lookup; ingest; project }` at key `/control/Channels` |
|
|
816
|
+
| `Channel<A>` | interface | `{ name; schema; fingerprintHeaders?; verify; decode; map; project }` |
|
|
817
|
+
| `RawInbound` | interface | `{ body: Uint8Array; headers: Record<string, string \| undefined>; idempotencyKey: IdempotencyKey }` |
|
|
818
|
+
| `InboundResult` | type | `{ _tag: "Start"; flowId; input }` or `{ _tag: "Signal"; runId; signal }` |
|
|
819
|
+
| `IngestRequest` | interface | `{ channel: string; raw: RawInbound }` |
|
|
820
|
+
| `ProjectRequest` | interface | `{ channel: string; run: RunSummary }` |
|
|
821
|
+
| `Delivery` | interface | `{ cursor: string; messageId?: string }` |
|
|
822
|
+
| `DeliveryProjection` | interface | `{ cursor; messageId?; operation: "post" \| "edit" \| "noop"; message: unknown }` |
|
|
823
|
+
| `make` | effect | Builds the coordinator over `ControlRuntime`'s durable mutation store. |
|
|
824
|
+
| `makeMemory` | effect | Builds a process-local coordinator for adapter unit tests. |
|
|
825
|
+
| `layer` | layer | `Layer<Channels, never, ControlRuntime \| Control>` |
|
|
826
|
+
| `layerMemory` | layer | `Layer<Channels, never, Control>` |
|
|
827
|
+
|
|
828
|
+
`verify` inspects only opaque bytes and headers, and always precedes `decode`,
|
|
829
|
+
which is what keeps an untrusted public request from reaching planning.
|
|
830
|
+
`decode` and `map` must be deterministic and side-effect free; a retry may
|
|
831
|
+
evaluate either again. `fingerprintHeaders` names only the non-secret headers
|
|
832
|
+
that change the decoded command.
|
|
833
|
+
Outbound delivery cursors are scoped to the exact channel name and run ID,
|
|
834
|
+
including names and IDs containing `:`.
|
|
835
|
+
|
|
836
|
+
## WebhookChannel
|
|
837
|
+
|
|
838
|
+
| Export | Kind | Signature |
|
|
839
|
+
| ------------------- | --------- | ----------------------------------------------------------------------------------------------------- |
|
|
840
|
+
| `SignatureVerifier` | type | `(raw: RawInbound, credential: Redacted<CredentialRef>) => Effect<void, Unauthorized>` |
|
|
841
|
+
| `Config<A>` | interface | `{ name; schema; credential; fingerprintHeaders?; verify; map; project }` |
|
|
842
|
+
| `make` | function | `<A>(config: Config<A>) => Channel<A>` |
|
|
843
|
+
| `maximumBodyBytes` | constant | `1048576`, the default body ceiling for one mount. |
|
|
844
|
+
| `HandlerOptions` | interface | `{ maximumBodyBytes?: number }` |
|
|
845
|
+
| `handler` | function | `(channel: string, idempotencyKey: IdempotencyKey, options?: HandlerOptions) => Effect<Receipt, ...>` |
|
|
846
|
+
|
|
847
|
+
The body is bounded twice: a `content-length` over the limit is refused before
|
|
848
|
+
the body is read, and each streamed chunk is measured before it is retained.
|
|
849
|
+
Reading stops at the first chunk exceeding the limit, before verification.
|
|
850
|
+
Both refusals are `InvalidInput` naming the two byte counts and no body content.
|
|
851
|
+
Malformed JSON returns the fixed issue `invalid webhook JSON` without parser
|
|
852
|
+
messages or payload fragments.
|
|
853
|
+
|
|
854
|
+
## Credential
|
|
855
|
+
|
|
856
|
+
The credential boundary. Only a `CredentialRef` crosses it. See
|
|
857
|
+
[Store and resolve a credential](./guides/store-credentials.md).
|
|
858
|
+
|
|
859
|
+
| Export | Kind | Signature |
|
|
860
|
+
| --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
861
|
+
| `CredentialRef` | interface | `{ id: string; name: string }` |
|
|
862
|
+
| `Operation` | type | `"list" \| "get" \| "create" \| "resolve" \| "rotate" \| "revoke"` |
|
|
863
|
+
| `Credential` | interface and tag | The six operations, at key `/control/Credential` |
|
|
864
|
+
| `Options` | interface | `{ store: CredentialStore.Service; cipher: CredentialCipher.Service; authorize?: (operation, reference) => Effect<void, Unauthorized> }` |
|
|
865
|
+
| `make` | function | `(options: Options) => Credential` |
|
|
866
|
+
| `layer` | layer | `(options?: { authorize? }) => Layer<Credential, never, CredentialStore \| CredentialCipher>` |
|
|
867
|
+
| `makeNoop` | function | `() => Credential`. Every operation fails `Unavailable`. |
|
|
868
|
+
| `layerNoop` | layer | `Layer<Credential>` |
|
|
869
|
+
|
|
870
|
+
| Operation | Signature |
|
|
871
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
872
|
+
| `list` | `() => Effect<ReadonlyArray<CredentialRef>, Unavailable \| Unauthorized>` |
|
|
873
|
+
| `get` | `(id: string) => Effect<CredentialRef, Unavailable \| Unauthorized>` |
|
|
874
|
+
| `create` | `({ id, name, secret: Redacted<string> }) => Effect<CredentialRef, Unavailable \| Unauthorized \| CredentialConflict>` |
|
|
875
|
+
| `resolve` | `(reference: CredentialRef) => Effect<Redacted<string>, Unavailable \| Unauthorized \| PersistenceError>` |
|
|
876
|
+
| `rotate` | `(reference: CredentialRef, secret: Redacted<string>, options?: { expected? }) => Effect<CredentialRef, Unavailable \| Unauthorized \| CredentialConflict \| PersistenceError>` |
|
|
877
|
+
| `revoke` | `(reference: CredentialRef) => Effect<void, Unavailable \| Unauthorized>` |
|
|
878
|
+
|
|
879
|
+
`authorize` defaults to allowing every operation, which is correct for a
|
|
880
|
+
single-principal local process. A reference is authenticated on every
|
|
881
|
+
operation, so a forged or stale one is refused.
|
|
882
|
+
|
|
883
|
+
## CredentialStore
|
|
884
|
+
|
|
885
|
+
| Export | Kind | Signature |
|
|
886
|
+
| ----------------------- | --------------- | -------------------------------------------------------------- |
|
|
887
|
+
| `SealedRecord` | interface | `{ id; name; ciphertext; nonce; version; updatedAtMs }` |
|
|
888
|
+
| `Service` | interface | `{ list(); read(id); write(record); remove(id) }` |
|
|
889
|
+
| `CredentialStore` | class | Key `/control/CredentialStore` |
|
|
890
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
891
|
+
| `makeMemory` | function | `() => Service`. Process-local and browser-safe. |
|
|
892
|
+
| `layerMemory` | layer | `Layer<CredentialStore>` |
|
|
893
|
+
| `makeNoop`, `layerNoop` | function, layer | Every operation fails `Unavailable`. Accept partial overrides. |
|
|
894
|
+
|
|
895
|
+
`write` commits `record` only if the stored version is `record.version - 1`,
|
|
896
|
+
and fails `CredentialConflict` otherwise. Plaintext never reaches this
|
|
897
|
+
boundary.
|
|
898
|
+
|
|
899
|
+
## CredentialCipher
|
|
900
|
+
|
|
901
|
+
| Export | Kind | Signature |
|
|
902
|
+
| ----------------------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
903
|
+
| `Sealed` | interface | `{ ciphertext: string; nonce: string }`, both base64. |
|
|
904
|
+
| `Context` | interface | `{ id: string; name: string; version: number }`, the authenticated data. |
|
|
905
|
+
| `Service` | interface | `{ seal(plaintext, context); open(sealed, context) }` |
|
|
906
|
+
| `CredentialCipher` | class | Key `/control/CredentialCipher` |
|
|
907
|
+
| `make` | function | `(implementation: Service) => Service` |
|
|
908
|
+
| `unavailable` | function | `() => Unavailable`, the typed failure a host reports with no secure key material. |
|
|
909
|
+
| `makeNoop`, `layerNoop` | function, layer | Every operation fails `Unavailable`. Accept partial overrides. |
|
|
910
|
+
|
|
911
|
+
## SqlCredentialStore
|
|
912
|
+
|
|
913
|
+
| Export | Kind | Signature |
|
|
914
|
+
| --------- | ------ | -------------------------------------------------------------------------------- |
|
|
915
|
+
| `migrate` | effect | `Effect<void, Unavailable, SqlClient>`. Creates `control_credentials` if absent. |
|
|
916
|
+
| `make` | effect | `Effect<CredentialStore.Service, Unavailable, DurableWriter \| SqlClient>` |
|
|
917
|
+
| `layer` | layer | `Layer<CredentialStore, Unavailable, DurableWriter \| SqlClient>` |
|
|
918
|
+
|
|
919
|
+
The read and the compare-and-set write run in one transaction, so two
|
|
920
|
+
concurrent rotations serialize.
|
|
921
|
+
|
|
922
|
+
## WebCryptoCipher
|
|
923
|
+
|
|
924
|
+
| Export | Kind | Signature |
|
|
925
|
+
| --------- | --------- | ------------------------------------------------------------------------------------- |
|
|
926
|
+
| `Options` | interface | `{ key: Redacted<string> }`, 32 raw bytes base64-encoded. |
|
|
927
|
+
| `make` | effect | `(options: Options) => Effect<CredentialCipher.Service, Unavailable \| InvalidInput>` |
|
|
928
|
+
| `layer` | layer | `(options: Options) => Layer<CredentialCipher, Unavailable \| InvalidInput>` |
|
|
929
|
+
|
|
930
|
+
AES-256-GCM over the Web Crypto API, which serves both Node and the browser.
|
|
931
|
+
The key is imported as a non-extractable `CryptoKey` and never reaches
|
|
932
|
+
`CredentialStore`. A host without Web Crypto fails with `Unavailable` rather
|
|
933
|
+
than a defect, and a key that is not 32 base64-encoded bytes fails with
|
|
934
|
+
`InvalidInput`. `open` fails with `PersistenceError` on operation
|
|
935
|
+
`credential.open` when the stored nonce is malformed or the ciphertext fails
|
|
936
|
+
authentication under this key and context.
|
|
937
|
+
|
|
938
|
+
## Migrations
|
|
939
|
+
|
|
940
|
+
| Export | Kind | Signature |
|
|
941
|
+
| ------- | ------------- | --------------------------------------------------------------------- |
|
|
942
|
+
| `set` | migration set | Namespace `control`, at the migration id block after time travel. |
|
|
943
|
+
| `run` | effect | Creates every durable control-plane and credential table. |
|
|
944
|
+
| `layer` | layer | Runs the migrations before exposing the database to control services. |
|
|
945
|
+
|
|
946
|
+
Hosts compose this set with the journal and run-store sets before opening a
|
|
947
|
+
shared control database. See
|
|
948
|
+
[Store control state in a database](./guides/durable-storage.md).
|
|
949
|
+
|
|
950
|
+
## SystemFlows
|
|
951
|
+
|
|
952
|
+
The reserved command-line verb to flow-id map the CLI projects.
|
|
953
|
+
|
|
954
|
+
| Export | Kind | Signature |
|
|
955
|
+
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
956
|
+
| `SystemFlowEntry` | interface | `{ verb: string; flowId: "system/" template literal; projection: "procedure" \| "systemFlow"; deployClass: boolean; planBearing: boolean; plannable: boolean }` |
|
|
957
|
+
| `catalog` | constant | Every reserved verb, including the ones a runtime may not plan. |
|
|
958
|
+
| `plannable` | constant | The entries a control runtime may offer as flows. |
|
|
959
|
+
|
|
960
|
+
`plannable: false` means the row is command-line metadata and nothing else: the
|
|
961
|
+
verb is named so the binary can refuse it by name. `system/replay` is the case
|
|
962
|
+
that matters. It is in `catalog` and not in `plannable`, so a runtime that
|
|
963
|
+
offers `plannable` as its flow catalog refuses it at `plan` rather than minting
|
|
964
|
+
an approval card no `run` can honor.
|
|
965
|
+
|
|
966
|
+
Both runtimes default their flow catalog to `plannable`, so a composition that
|
|
967
|
+
builds its own map and the runtimes' defaults cannot disagree about which
|
|
968
|
+
reserved ids exist.
|
|
969
|
+
|
|
970
|
+
## test/TestControl
|
|
971
|
+
|
|
972
|
+
Importable only from `@smthrs/control/test/TestControl`.
|
|
973
|
+
|
|
974
|
+
| Export | Signature |
|
|
975
|
+
| ------- | -------------------------------------------------------------------------------------------- |
|
|
976
|
+
| `layer` | `(options?: ControlRuntime.MemoryOptions, executor?: ControlExecutor.Service) => Layer<...>` |
|
|
977
|
+
|
|
978
|
+
Provides `Control` together with every collaborator it built: the deterministic
|
|
979
|
+
runtime, the in-memory journal bundle, a notification queue over that journal,
|
|
980
|
+
the executor (`ControlExecutor.makeNoop()` by default), and an empty registry.
|
|
981
|
+
Runtime flow metadata falls back to the reserved system catalog. See
|
|
982
|
+
[Test against the control plane](./guides/testing.md).
|