@smthrs/control 0.0.0-stage → 1.0.0-rc.3
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 +1280 -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 +740 -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 +1521 -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 +1585 -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 +807 -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 +5 -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 +2113 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1597 -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 +2476 -0
- package/src/SqlCredentialStore.ts +148 -0
- package/src/Steering.ts +96 -0
- package/src/SystemFlows.ts +225 -0
- package/src/WebCryptoCipher.ts +169 -0
- package/src/WebhookChannel.ts +166 -0
- package/src/index.ts +188 -0
- package/src/internal/MutationBoundary.ts +46 -0
- package/src/internal/activeFibers.ts +22 -0
- package/src/internal/issues.ts +40 -0
- package/src/internal/planning.ts +262 -0
- package/src/internal/sqlSchemaErrors.ts +37 -0
- package/src/migrations/0001_control_tables.ts +99 -0
- package/src/migrations/0002_run_keys.ts +23 -0
- package/src/migrations/0003_signal_commands.ts +27 -0
- package/src/migrations/0004_approval_decisions.ts +25 -0
- package/src/migrations/0005_signal_principals.ts +27 -0
- package/src/migrations/0006_run_principals.ts +31 -0
- package/src/migrations/0007_resume_consent.ts +28 -0
- package/src/test/TestControl.ts +47 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Serve the control plane over RPC"
|
|
3
|
+
description: "Mount the same Control service as HTTP and WebSocket RPC, project it back into the Control interface on a client, authenticate with a bearer token, and read the transport failures a client can retry."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 9
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`ControlServer.layerHttp` mounts the `Control` service as RPC;
|
|
9
|
+
`ControlClient.layer` projects the RPC client back into the same interface.
|
|
10
|
+
Remote operations also require transport configuration and authentication.
|
|
11
|
+
|
|
12
|
+
## Mount the server
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { NodeHttpServer } from "@effect/platform-node"
|
|
16
|
+
import * as ControlRpcs from "@smthrs/control/ControlRpcs"
|
|
17
|
+
import * as ControlServer from "@smthrs/control/ControlServer"
|
|
18
|
+
import * as Layer from "effect/Layer"
|
|
19
|
+
import { HttpRouter } from "effect/unstable/http"
|
|
20
|
+
import { RpcSerialization } from "effect/unstable/rpc"
|
|
21
|
+
import { createServer } from "node:http"
|
|
22
|
+
|
|
23
|
+
const served = HttpRouter.serve(
|
|
24
|
+
ControlServer.layerHttp.pipe(
|
|
25
|
+
Layer.provide(ControlRpcs.layerBearerAuth({
|
|
26
|
+
token: process.env["SMITHERS_CONTROL_TOKEN"] ?? "",
|
|
27
|
+
principal: { id: "operator", kind: "bearer" }
|
|
28
|
+
})),
|
|
29
|
+
Layer.provide(RpcSerialization.layerNdjson)
|
|
30
|
+
)
|
|
31
|
+
).pipe(
|
|
32
|
+
Layer.provideMerge(NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0 }))
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`layerHttp` mounts both protocols together: unary procedures over
|
|
37
|
+
`POST /rpc`, and the `watch` stream over `WebSocket /rpc/ws`. The operations
|
|
38
|
+
divide cleanly. Plan, approve, run, and list are requests with answers; `watch`
|
|
39
|
+
is a projection that keeps arriving, so it rides a socket.
|
|
40
|
+
|
|
41
|
+
`ControlServer.layer` is the handler layer alone, for a host that mounts its
|
|
42
|
+
own protocols.
|
|
43
|
+
|
|
44
|
+
## Connect a client
|
|
45
|
+
|
|
46
|
+
`credential` authenticates HTTP requests only. Authenticated `watch` also
|
|
47
|
+
requires an `Authorization` header on the WebSocket upgrade. An ordinary
|
|
48
|
+
`NodeSocket.layerWebSocket` does not send it and watch fails with
|
|
49
|
+
`Unauthorized`. This Node composition uses the `ws` constructor exported by
|
|
50
|
+
`NodeSocket` to supply the upgrade header:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { NodeHttpClient, NodeSocket } from "@effect/platform-node"
|
|
54
|
+
import * as ControlClient from "@smthrs/control/ControlClient"
|
|
55
|
+
import * as Layer from "effect/Layer"
|
|
56
|
+
import { RpcSerialization } from "effect/unstable/rpc"
|
|
57
|
+
import { Socket } from "effect/unstable/socket"
|
|
58
|
+
|
|
59
|
+
const credential = process.env["SMITHERS_CONTROL_TOKEN"] ?? ""
|
|
60
|
+
const authenticatedSocket = Socket.layerWebSocket("ws://127.0.0.1:4000/rpc/ws").pipe(
|
|
61
|
+
Layer.provide(
|
|
62
|
+
Layer.succeed(Socket.WebSocketConstructor)((url, options) => {
|
|
63
|
+
const configured = options !== undefined && typeof options !== "string" && !Array.isArray(options)
|
|
64
|
+
? options
|
|
65
|
+
: undefined
|
|
66
|
+
const protocols = configured === undefined ? options as string | Array<string> | undefined : undefined
|
|
67
|
+
const socket = new NodeSocket.NodeWS.WebSocket(url, protocols, {
|
|
68
|
+
...configured,
|
|
69
|
+
headers: { ...configured?.headers, Authorization: `Bearer ${credential}` }
|
|
70
|
+
})
|
|
71
|
+
// Effect removes reader listeners before closing. `ws` can report a
|
|
72
|
+
// late handshake error during that close; keep Node from treating it as
|
|
73
|
+
// an unhandled event. Active Effect listeners still receive all errors.
|
|
74
|
+
socket.on("error", () => {})
|
|
75
|
+
return socket
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const client = ControlClient.layer({
|
|
81
|
+
url: "http://127.0.0.1:4000/rpc",
|
|
82
|
+
credential
|
|
83
|
+
}).pipe(
|
|
84
|
+
Layer.provide([
|
|
85
|
+
NodeHttpClient.layerUndici,
|
|
86
|
+
authenticatedSocket,
|
|
87
|
+
RpcSerialization.layerNdjson
|
|
88
|
+
])
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With both HTTP and the socket upgrade authenticated, programs written against
|
|
93
|
+
`Control` can use this client layer. Handle `TransportError` and `Unauthorized`
|
|
94
|
+
in addition to each operation's domain failures; both are declared on every
|
|
95
|
+
`Control.Service` method.
|
|
96
|
+
|
|
97
|
+
Browser WebSockets cannot set upgrade headers. Browser deployments need a
|
|
98
|
+
trusted proxy that authenticates the caller and supplies the header. Tokens
|
|
99
|
+
in URL query strings are not supported.
|
|
100
|
+
|
|
101
|
+
`layerHttp` refuses, with 403 and before authentication, any request whose
|
|
102
|
+
`Origin` header does not match its `Host` header, on POST `/rpc` and on the
|
|
103
|
+
`/rpc/ws` upgrade. A browser always sends `Origin`, so a page on another site
|
|
104
|
+
cannot use a credential the proxy attaches for its victim. Non-browser
|
|
105
|
+
clients send no `Origin` and are unaffected. Serve the browser app from the
|
|
106
|
+
same origin as the control mount, and keep the proxy's `Host` header intact.
|
|
107
|
+
|
|
108
|
+
## Authenticate
|
|
109
|
+
|
|
110
|
+
`ControlRpcs.ControlAuth` is the middleware boundary, and it provides
|
|
111
|
+
`ControlPrincipal` to every handler.
|
|
112
|
+
|
|
113
|
+
| Layer | Use |
|
|
114
|
+
| --------------------------------------- | ------------------------------------------------------------------------ |
|
|
115
|
+
| `layerBearerAuth({ token, principal })` | One shared token. Every request carrying it receives the same principal. |
|
|
116
|
+
| `layerAuth(authenticator)` | Your own header authenticator, returning a principal or `Unauthorized`. |
|
|
117
|
+
| `layerNoopAuth(principal?)` | Trusted in-process use and tests. Authenticates nothing. |
|
|
118
|
+
|
|
119
|
+
The bearer comparison is constant time, and a missing, malformed, empty, or
|
|
120
|
+
incorrect credential all fail closed with the same `Unauthorized` response.
|
|
121
|
+
|
|
122
|
+
## Who reads which runs
|
|
123
|
+
|
|
124
|
+
Every run the control plane launches records the principal that launched it as
|
|
125
|
+
`RunSummary.launchedBy`. `List` and `Watch` answer an operator with every run
|
|
126
|
+
and every other principal with only the runs it launched: its own run
|
|
127
|
+
summaries, the fires that started them, and their events. `Steer`, `Signal`,
|
|
128
|
+
`Cancel` and `Resume` accept the same runs. Triggers, a plan's events and a run
|
|
129
|
+
the engine created (a child, a fork, a later round) reach operators only.
|
|
130
|
+
Another principal's run answers `RunNotFound`, exactly as a missing one does.
|
|
131
|
+
|
|
132
|
+
The authentication layer names the operators, because it knows which
|
|
133
|
+
identities it stamps. `layerBearerAuth` and `layerNoopAuth` stamp one principal
|
|
134
|
+
and make it the operator. `layerAuth` takes the rule as `seesAllRuns`, and
|
|
135
|
+
without it no principal is an operator:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const auth = ControlRpcs.layerAuth(authenticator, {
|
|
139
|
+
seesAllRuns: (principal) => principal.kind === "operator"
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
An authenticator also receives the call it is guarding, `{ rpc, payload }`,
|
|
144
|
+
on every in-band frame and nothing at a transport edge; `anyAuthenticator`
|
|
145
|
+
asks several in turn. `ScopedToken.authenticator` uses the call to confine a
|
|
146
|
+
minted token to the procedures, run, or flow it names, and composes with the
|
|
147
|
+
bearer:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import * as ControlRpcs from "@smthrs/control/ControlRpcs"
|
|
151
|
+
import * as ScopedToken from "@smthrs/control/ScopedToken"
|
|
152
|
+
|
|
153
|
+
const auth = ControlRpcs.layerAuth(ControlRpcs.anyAuthenticator([
|
|
154
|
+
ControlRpcs.bearerAuthenticator({ token, principal: { id: "gateway", kind: "bearer" } }),
|
|
155
|
+
ScopedToken.authenticator({ key: token, principal: { id: "gateway", kind: "scoped" } })
|
|
156
|
+
]))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A custom authenticator is an object with one method:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { Unauthorized } from "@smthrs/control/ControlError"
|
|
163
|
+
import type * as ControlRpcs from "@smthrs/control/ControlRpcs"
|
|
164
|
+
import * as Effect from "effect/Effect"
|
|
165
|
+
|
|
166
|
+
const authenticator: ControlRpcs.Authenticator = {
|
|
167
|
+
authenticate: (headers) =>
|
|
168
|
+
lookupSession(headers["x-session"]).pipe(
|
|
169
|
+
Effect.map((session) => ({ id: session.userId, kind: "user", stampedAt: Date.now() })),
|
|
170
|
+
Effect.mapError(() => new Unauthorized({ message: "A valid session is required" }))
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## The server stamps identity, always
|
|
176
|
+
|
|
177
|
+
Every mutation that records who asked reads `ControlPrincipal` and stamps it,
|
|
178
|
+
rather than forwarding whatever the client sent. The identity the middleware
|
|
179
|
+
authenticated is the only one the server can stand behind, and it is what
|
|
180
|
+
reaches the journal, `RunSummary.cancellation`, and a steer's notification
|
|
181
|
+
provenance.
|
|
182
|
+
|
|
183
|
+
`Steer` is the one payload that carries a principal on the wire, because an
|
|
184
|
+
in-process caller names one that is not an operator. Over RPC the
|
|
185
|
+
authenticated identity replaces whatever arrived. See
|
|
186
|
+
[attribution over a wire](./steer-a-run.md).
|
|
187
|
+
|
|
188
|
+
## Failures a client sees
|
|
189
|
+
|
|
190
|
+
`ControlClient` normalizes everything into `ControlError`. A declared control
|
|
191
|
+
failure crosses the wire as itself; anything else becomes `TransportError`,
|
|
192
|
+
whose `retryable` flag classifies only the transport phase:
|
|
193
|
+
|
|
194
|
+
| Cause | `retryable` |
|
|
195
|
+
| ------------------------------------------------------ | ----------- |
|
|
196
|
+
| Connection, socket open, read, write, or close failure | `true` |
|
|
197
|
+
| HTTP 5xx | `true` |
|
|
198
|
+
| HTTP 4xx | `false` |
|
|
199
|
+
| Request encode failure | `false` |
|
|
200
|
+
| Response decode failure | `false` |
|
|
201
|
+
| Invalid client URL | `false` |
|
|
202
|
+
|
|
203
|
+
Resend a retryable mutation only when its idempotency key makes replay safe. A
|
|
204
|
+
keyless request can have reached the server even when its response was lost.
|
|
205
|
+
|
|
206
|
+
`ControlClient.isControlError` is `Schema.is` of the same union the errors are
|
|
207
|
+
declared in, so an error class added to the package reaches the refinement
|
|
208
|
+
without anyone updating a second list.
|
|
209
|
+
|
|
210
|
+
An operator's own interruption is re-raised exactly as it arrived rather than
|
|
211
|
+
described as a transport failure. Cancelling a request is not the server
|
|
212
|
+
failing.
|
|
213
|
+
|
|
214
|
+
## Where to go next
|
|
215
|
+
|
|
216
|
+
- [The complete loopback example](https://github.com/smithersai/smithers/blob/main/examples/src/24-control-plane-and-gateway.ts):
|
|
217
|
+
a discovered flow planned, approved, launched, and watched over the wire.
|
|
218
|
+
- [Watch a run's events](./watch-a-run.md): the operation the WebSocket exists
|
|
219
|
+
for.
|
|
220
|
+
- [`smthrs serve`](/cli/serve): the shipped server over this layer.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Signal a run
|
|
2
|
+
|
|
3
|
+
`Control.signal` admits a named JSON payload under an actor-scoped idempotency
|
|
4
|
+
key. `Accepted` confirms that admission is durable. It does not claim that the
|
|
5
|
+
run has consumed the payload. Reusing the key with different input returns
|
|
6
|
+
`Conflict` before the conflicting payload reaches an executor.
|
|
7
|
+
|
|
8
|
+
The command, its receipt, and `control.signal.admitted` journal event commit
|
|
9
|
+
in the control database first. Execution occurs after that transaction ends;
|
|
10
|
+
there is no transaction spanning control.db and engine.db.
|
|
11
|
+
|
|
12
|
+
The production executor binds each command to one concrete durable wait token
|
|
13
|
+
before applying it. One token has at most one admitted command. Application
|
|
14
|
+
atomically checks the engine's current wait token, and recovery verifies the
|
|
15
|
+
stored deferred result. A crash after application but before acknowledgment
|
|
16
|
+
retries the original token. It cannot move the command to a later wait.
|
|
17
|
+
|
|
18
|
+
Commands without a visible wait remain pending. The running executor
|
|
19
|
+
reconciles a bounded page every 250 milliseconds, starting at host startup,
|
|
20
|
+
so a signal admitted before its wait opens does not require a restart or
|
|
21
|
+
manual resend. Pages rotate so unavailable waits cannot starve later commands.
|
|
22
|
+
Malformed stored payloads are rejected and logged rather than blocking a page.
|
|
23
|
+
|
|
24
|
+
`ControlRuntime.signalCommand(commandId)` exposes `pending`, `delivered`,
|
|
25
|
+
`rejected`, and `terminal` dispositions for integrations holding the admitted
|
|
26
|
+
command identity. The CLI does not yet expose a dedicated delivery-status
|
|
27
|
+
lookup. A definite incompatible wait raises `NoMatchingWait`; retries preserve
|
|
28
|
+
that refusal. A signal initially submitted to a settled run returns `Terminal`.
|
|
29
|
+
|
|
30
|
+
A human wait, one parked with reason `approval` such as a `HumanTask`
|
|
31
|
+
question, is an approval gate. `Control.signal` stamps the caller's principal
|
|
32
|
+
on the admitted command. Before the executor completes a human wait, it asks
|
|
33
|
+
`ApprovalAuthority` whether that principal may approve the wait's `Node`
|
|
34
|
+
target, with the wait name as `requestId`, the wait token as `digest`, and
|
|
35
|
+
scope `once`. A refused principal fails `Unauthorized`, the command is
|
|
36
|
+
rejected, and the wait stays open. A replay after restart is judged by the
|
|
37
|
+
principal recorded at admission; a command with no recorded principal cannot
|
|
38
|
+
answer a human wait. Plain `WaitFor` events need no approval.
|
|
39
|
+
|
|
40
|
+
A signal from a webhook channel carries the principal
|
|
41
|
+
`{ id: <channel name>, kind: "channel" }`. It can answer a human wait only if
|
|
42
|
+
the host delegates approval to that principal.
|
|
43
|
+
|
|
44
|
+
`WaitFor` names one durable fact per `(flowName, executionId, name)`. Calling
|
|
45
|
+
it twice with the same name in the same execution intentionally observes the
|
|
46
|
+
same fact. Use distinct names for distinct rendezvous points. This is not a
|
|
47
|
+
stream of repeated same-name events.
|
|
48
|
+
|
|
49
|
+
Legacy `control_run_messages` signals have no application identity or token
|
|
50
|
+
binding. They remain readable through `deliveredSignals`, but are not
|
|
51
|
+
replayed automatically by the new inbox. Operators must inspect legacy wait
|
|
52
|
+
state before explicitly resubmitting under a new key; silently replaying them
|
|
53
|
+
could apply historical intent to a different wait.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Steer a running agent"
|
|
3
|
+
description: "Send a message, a seat change, a thinking level, or a tool set to a run's next turn boundary: the four variants, which parks a steer wakes, and the two durable moments a steer has."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`steer` writes one durable item into the notification queue and journals the
|
|
9
|
+
enqueue beside it. The run picks it up at its next turn boundary.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Control } from "@smthrs/control/Control"
|
|
13
|
+
import * as Effect from "effect/Effect"
|
|
14
|
+
|
|
15
|
+
const steer = Effect.gen(function*() {
|
|
16
|
+
const control = yield* Control
|
|
17
|
+
return yield* control.steer({
|
|
18
|
+
runId: "run-17",
|
|
19
|
+
message: {
|
|
20
|
+
messageId: "steer-1",
|
|
21
|
+
runId: "run-17",
|
|
22
|
+
principal: { id: "ada", kind: "user", stampedAt: Date.now() },
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
body: "prefer the smaller diff"
|
|
25
|
+
},
|
|
26
|
+
idempotencyKey: "steer:run-17:1"
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The four variants
|
|
32
|
+
|
|
33
|
+
An operator steers a run for four different reasons, and only one of them is
|
|
34
|
+
something to tell the model. Saying "your seat changed" would spend a turn on
|
|
35
|
+
bookkeeping; changing the seat is what was asked for.
|
|
36
|
+
|
|
37
|
+
| Variant | Field | What the next turn does |
|
|
38
|
+
| ----------------------------------------------------------- | ----------- | ------------------------------------- |
|
|
39
|
+
| `Message` (the default, and what a `body` alone decodes as) | `body` | Inserts the body into the transcript. |
|
|
40
|
+
| `Seat` | `seat` | Runs the turn on that model seat. |
|
|
41
|
+
| `Thinking` | `thinking` | Runs the turn at that thinking level. |
|
|
42
|
+
| `Tools` | `toolNames` | Adds those tools to the active set. |
|
|
43
|
+
|
|
44
|
+
`kind` is optional on `Message` and required on the other three, which is what
|
|
45
|
+
keeps a steer written before the vocabulary widened readable: a body and no
|
|
46
|
+
kind is a message, and always was.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { steerItem } from "@smthrs/control/ControlSchema"
|
|
50
|
+
|
|
51
|
+
steerItem({ ...envelope, body: "prefer the smaller diff" })
|
|
52
|
+
// { kind: "Message", body: "prefer the smaller diff" }
|
|
53
|
+
steerItem({ ...envelope, kind: "Seat", seat: "anthropic:claude-sonnet-4-5" })
|
|
54
|
+
// { kind: "Seat", seat: "anthropic:claude-sonnet-4-5" }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`ControlSchema.steerItem` strips the control envelope, which is who asked,
|
|
58
|
+
when, and for which run, and returns the
|
|
59
|
+
[`@smthrs/notifications`](/api/notifications) payload the harness reads back.
|
|
60
|
+
The harness maps each payload onto its matching steering item, so a seat steer
|
|
61
|
+
changes the seat instead of spending a turn announcing it.
|
|
62
|
+
|
|
63
|
+
## The two durable moments
|
|
64
|
+
|
|
65
|
+
| Event | Writer | Payload |
|
|
66
|
+
| ------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------- |
|
|
67
|
+
| `control.steer.enqueued` | `Control.steer` | `{ runId, messageId, kind, createdAt }` |
|
|
68
|
+
| `control.steer.delivered` | derived by `Steering.derive` from the queue's `flows/notifications/Promoted` entry | `{ runId, messageId, boundary }` |
|
|
69
|
+
|
|
70
|
+
Delivery is derived rather than recorded, because the boundary that delivered
|
|
71
|
+
the steer runs in the agent process and not in the control plane. A control
|
|
72
|
+
plane that wrote its own delivery record would be asserting a fact it did not
|
|
73
|
+
observe.
|
|
74
|
+
|
|
75
|
+
One promotion entry names a batch, so it derives one delta per message id, each
|
|
76
|
+
carrying the sequence of the entry it came from. Checkpoint `event.cursor`
|
|
77
|
+
and resume with `afterCursor` to continue even between deliveries in a batch.
|
|
78
|
+
|
|
79
|
+
`RunSummary.steering.pending` counts what has been admitted and not yet
|
|
80
|
+
promoted. It comes from the queue rather than a column, because the queue owns
|
|
81
|
+
both halves.
|
|
82
|
+
|
|
83
|
+
## Waking a parked run
|
|
84
|
+
|
|
85
|
+
A steer resumes a parked run when the park is one a message can end:
|
|
86
|
+
|
|
87
|
+
| `waitingReason` | Steered |
|
|
88
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `event` | Resumed. The run is waiting for something to arrive, and a steer is something arriving. |
|
|
90
|
+
| `released` | Resumed. A sweep took the run away from a dead owner, and nothing is coming to claim it. |
|
|
91
|
+
| `approval`, `timer`, `quota` | Left parked. The run is waiting for a decision, a clock, or a budget that a message does not supply. |
|
|
92
|
+
| absent | Left parked. A park with no reason is an operator's own park, and a message queued behind it is queued for when they resume it. |
|
|
93
|
+
|
|
94
|
+
A reason this table does not name is left parked too: a control plane that
|
|
95
|
+
cannot explain a park should not end it. The wake claims with
|
|
96
|
+
`scope: "launched"`, so a run another driver created keeps its park and that
|
|
97
|
+
driver delivers the steer at the run's next boundary. The steer is already
|
|
98
|
+
durable either way.
|
|
99
|
+
|
|
100
|
+
A successful wake journals `control.steer.woke` with the run's new status.
|
|
101
|
+
|
|
102
|
+
## Refusals
|
|
103
|
+
|
|
104
|
+
A steer whose `message.runId` names a different run than the call does is
|
|
105
|
+
refused before anything is admitted:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
InvalidInput: message.runId: must be "run-17", received "run-18"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The notification would be admitted to the call's run while the stored message
|
|
112
|
+
claimed another, so an operator reading it later would be told it belongs
|
|
113
|
+
somewhere it was never delivered.
|
|
114
|
+
|
|
115
|
+
A steer to a run that already reached `cancelled`, `completed`, or `failed`
|
|
116
|
+
answers `Terminal` and stores nothing. Storing it anyway would leave an
|
|
117
|
+
operator watching a message with no boundary left to deliver it.
|
|
118
|
+
|
|
119
|
+
## Attribution over a wire
|
|
120
|
+
|
|
121
|
+
`SteerMessage` carries a `principal` even though `cancel` refuses one on the
|
|
122
|
+
wire, and the difference is who the callers are. A cancel is only ever an
|
|
123
|
+
operator command, so the server can be its sole source of identity. A steer is
|
|
124
|
+
not: `agent/send` steers a child run and attributes the message to the parent
|
|
125
|
+
flow, which is an identity no authenticator knows and no operator issued.
|
|
126
|
+
|
|
127
|
+
So the field stays, and `ControlServer` overwrites it with the authenticated
|
|
128
|
+
principal on every steer that arrives over RPC. An in-process caller keeps
|
|
129
|
+
naming its own. The value reaches the notification's `sourceActor` and the run
|
|
130
|
+
transcript, which is exactly where a spoofed name would be read as truth.
|
|
131
|
+
|
|
132
|
+
## Where to go next
|
|
133
|
+
|
|
134
|
+
- [Watch a run's events](./watch-a-run.md): where both moments show up.
|
|
135
|
+
- [Deliver a signal to a waiting run](./signal-a-run.md): the other way to
|
|
136
|
+
reach a parked run, and why it is not the same thing.
|
|
137
|
+
- [`smthrs runs steer`](/cli/runs) and
|
|
138
|
+
[steering on smithers.sh](/docs/guides/steering/): the operator surface.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Store and resolve a credential"
|
|
3
|
+
description: "Keep a connection secret out of flow input, plan digests, journals, and model context: the reference that crosses the boundary, the store and cipher ports behind it, and the compare-and-set that serializes a rotation."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 12
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Credentials are capabilities. They must never enter flow input, plan digests,
|
|
9
|
+
journal payloads, or model context, so only a `CredentialRef` crosses the
|
|
10
|
+
browser-safe contract:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
interface CredentialRef {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly name: string
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Plaintext exists in exactly two places: inside a `Redacted` handed to `create`
|
|
20
|
+
or `rotate`, and inside the `Redacted` returned by `resolve`.
|
|
21
|
+
|
|
22
|
+
## Compose the boundary
|
|
23
|
+
|
|
24
|
+
`Credential` composes two ports, and a host chooses an adapter for each:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import * as Credential from "@smthrs/control/Credential"
|
|
28
|
+
import * as CredentialStore from "@smthrs/control/CredentialStore"
|
|
29
|
+
import * as WebCryptoCipher from "@smthrs/control/WebCryptoCipher"
|
|
30
|
+
import * as Layer from "effect/Layer"
|
|
31
|
+
import * as Redacted from "effect/Redacted"
|
|
32
|
+
|
|
33
|
+
const credentials = Credential.layer().pipe(
|
|
34
|
+
Layer.provide(Layer.merge(
|
|
35
|
+
CredentialStore.layerMemory,
|
|
36
|
+
WebCryptoCipher.layer({ key: Redacted.make(process.env["SMITHERS_CREDENTIAL_KEY"]!) })
|
|
37
|
+
))
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Port | What it does | Adapters |
|
|
42
|
+
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ |
|
|
43
|
+
| `CredentialStore` | Persists an opaque sealed record, with compare-and-set on writes. | `layerMemory`, `SqlCredentialStore.layer`, `layerNoop` |
|
|
44
|
+
| `CredentialCipher` | Seals and opens the secret under host-managed keys. | `WebCryptoCipher.layer`, `layerNoop` |
|
|
45
|
+
|
|
46
|
+
`Credential.layerNoop` is the whole boundary reporting `Unavailable`, which is
|
|
47
|
+
the honest composition for a host with no credential storage.
|
|
48
|
+
|
|
49
|
+
## Use it
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const program = Effect.gen(function*() {
|
|
53
|
+
const credentials = yield* Credential.Credential
|
|
54
|
+
|
|
55
|
+
const reference = yield* credentials.create({
|
|
56
|
+
id: "github-webhook",
|
|
57
|
+
name: "GitHub webhook",
|
|
58
|
+
secret: Redacted.make(incomingSecret)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Only this call sees plaintext again.
|
|
62
|
+
const secret = yield* credentials.resolve(reference)
|
|
63
|
+
|
|
64
|
+
const rotated = yield* credentials.rotate(reference, Redacted.make(nextSecret))
|
|
65
|
+
yield* credentials.revoke(rotated)
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`list` and `get` answer references. `resolve` is the one operation that crosses
|
|
70
|
+
into a secret-bearing adapter boundary.
|
|
71
|
+
|
|
72
|
+
## Authorization is the host's
|
|
73
|
+
|
|
74
|
+
`Credential.layer({ authorize })` injects a policy hook, called with the
|
|
75
|
+
operation and the reference before anything else runs:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
Credential.layer({
|
|
79
|
+
authorize: (operation, reference) =>
|
|
80
|
+
operation === "resolve" && !Option.exists(reference, allowed)
|
|
81
|
+
? Effect.fail(new Unauthorized({ message: "Not available to this caller" }))
|
|
82
|
+
: Effect.void
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The default allows every operation, which is correct for a single-principal
|
|
87
|
+
local process. The six operations are `list`, `get`, `create`, `resolve`,
|
|
88
|
+
`rotate`, and `revoke`.
|
|
89
|
+
|
|
90
|
+
A reference is also _authenticated_ on every operation: caller-owned fields are
|
|
91
|
+
snapshotted before policy effects run, and a forged or stale `CredentialRef` is
|
|
92
|
+
refused because the snapshotted name must still match the stored record. The
|
|
93
|
+
refusal for a missing credential and the refusal for a denied one are
|
|
94
|
+
deliberately indistinguishable, because telling an unauthorized caller which
|
|
95
|
+
ids exist is itself a leak.
|
|
96
|
+
|
|
97
|
+
## What is stored, and what is not
|
|
98
|
+
|
|
99
|
+
`SealedRecord` is everything at rest:
|
|
100
|
+
|
|
101
|
+
| Field | Meaning |
|
|
102
|
+
| ------------- | ------------------------------------------------------------ |
|
|
103
|
+
| `id`, `name` | Opaque metadata, and the cipher's authenticated data. |
|
|
104
|
+
| `ciphertext` | Base64, produced by the cipher. |
|
|
105
|
+
| `nonce` | Base64, per record, never reused across versions. |
|
|
106
|
+
| `version` | Monotonic write counter, 1 for a freshly created credential. |
|
|
107
|
+
| `updatedAtMs` | When the record was last written. |
|
|
108
|
+
|
|
109
|
+
The key never reaches the store, so a stolen store is ciphertext and nothing
|
|
110
|
+
else. `WebCryptoCipher` holds it as a non-extractable `CryptoKey`, so it cannot
|
|
111
|
+
be read back out of the cipher either.
|
|
112
|
+
|
|
113
|
+
The id, name, and version are written beside the blob _and_ authenticated with
|
|
114
|
+
it, so moving a blob to another id, name, or version makes it unreadable.
|
|
115
|
+
|
|
116
|
+
## Rotation is serialized
|
|
117
|
+
|
|
118
|
+
Writes are compare-and-set on `version`. A writer that read version _n_ commits
|
|
119
|
+
version _n + 1_; a concurrent writer that read the same _n_ is refused with
|
|
120
|
+
`CredentialConflict` carrying both versions, rather than silently overwriting
|
|
121
|
+
the winner.
|
|
122
|
+
|
|
123
|
+
`SqlCredentialStore` does the read and the write in one transaction, so the
|
|
124
|
+
version a writer read and the row it guards cannot interleave.
|
|
125
|
+
|
|
126
|
+
## The key
|
|
127
|
+
|
|
128
|
+
`WebCryptoCipher` uses AES-256-GCM over the Web Crypto API, which is the
|
|
129
|
+
browser's own and has been Node's since v19, so this adapter imports nothing
|
|
130
|
+
from `node:*` and runs unmodified on a server. `Options.key` is 32 raw bytes,
|
|
131
|
+
base64-encoded, held redacted so it cannot be printed or serialized by
|
|
132
|
+
accident.
|
|
133
|
+
|
|
134
|
+
A host without Web Crypto, an old runtime or a locked-down worker, fails with
|
|
135
|
+
the typed `Unavailable` rather than a defect. A key that is not 32
|
|
136
|
+
base64-encoded bytes fails with `InvalidInput`.
|
|
137
|
+
|
|
138
|
+
A record that does not open fails with `PersistenceError` on operation
|
|
139
|
+
`credential.open`, and the host log records why. The message names a malformed
|
|
140
|
+
stored nonce, or a failed authentication: the key is not the one that sealed
|
|
141
|
+
the record, the record's id, name or version changed, or the ciphertext was
|
|
142
|
+
tampered with.
|
|
143
|
+
|
|
144
|
+
## Durable storage
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import * as SqlCredentialStore from "@smthrs/control/SqlCredentialStore"
|
|
148
|
+
|
|
149
|
+
const store = SqlCredentialStore.layer
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It requires `DurableWriter` and `SqlClient`, and creates `control_credentials`
|
|
153
|
+
on construction. The table is part of the package's
|
|
154
|
+
[migration set](./durable-storage.md), so a host that composes that set has it
|
|
155
|
+
already.
|
|
156
|
+
|
|
157
|
+
## Where to go next
|
|
158
|
+
|
|
159
|
+
- [Accept a webhook](./ingest-a-webhook.md): the caller that carries a
|
|
160
|
+
`CredentialRef` into a signature verifier.
|
|
161
|
+
- [Store control state in a database](./durable-storage.md): the migration set
|
|
162
|
+
the credential table belongs to.
|
|
163
|
+
- [Troubleshooting](../troubleshooting.md): what `Unavailable` and
|
|
164
|
+
`CredentialConflict` mean in practice.
|