@smthrs/control 0.0.0-stage → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/cjs/ApprovalAuthority.d.ts +73 -0
- package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
- package/dist/cjs/ApprovalAuthority.js +62 -0
- package/dist/cjs/ApprovalAuthority.js.map +7 -0
- package/dist/cjs/Cancellation.d.ts +107 -0
- package/dist/cjs/Cancellation.d.ts.map +1 -0
- package/dist/cjs/Cancellation.js +72 -0
- package/dist/cjs/Cancellation.js.map +7 -0
- package/dist/cjs/Channels.d.ts +170 -0
- package/dist/cjs/Channels.d.ts.map +1 -0
- package/dist/cjs/Channels.js +278 -0
- package/dist/cjs/Channels.js.map +7 -0
- package/dist/cjs/Control.d.ts +202 -0
- package/dist/cjs/Control.d.ts.map +1 -0
- package/dist/cjs/Control.js +47 -0
- package/dist/cjs/Control.js.map +7 -0
- package/dist/cjs/ControlClient.d.ts +52 -0
- package/dist/cjs/ControlClient.d.ts.map +1 -0
- package/dist/cjs/ControlClient.js +191 -0
- package/dist/cjs/ControlClient.js.map +7 -0
- package/dist/cjs/ControlError.d.ts +318 -0
- package/dist/cjs/ControlError.d.ts.map +1 -0
- package/dist/cjs/ControlError.js +249 -0
- package/dist/cjs/ControlError.js.map +7 -0
- package/dist/cjs/ControlExecutor.d.ts +372 -0
- package/dist/cjs/ControlExecutor.d.ts.map +1 -0
- package/dist/cjs/ControlExecutor.js +123 -0
- package/dist/cjs/ControlExecutor.js.map +7 -0
- package/dist/cjs/ControlFacts.d.ts +454 -0
- package/dist/cjs/ControlFacts.d.ts.map +1 -0
- package/dist/cjs/ControlFacts.js +261 -0
- package/dist/cjs/ControlFacts.js.map +7 -0
- package/dist/cjs/ControlLive.d.ts +23 -0
- package/dist/cjs/ControlLive.d.ts.map +1 -0
- package/dist/cjs/ControlLive.js +1293 -0
- package/dist/cjs/ControlLive.js.map +7 -0
- package/dist/cjs/ControlRpcs.d.ts +1204 -0
- package/dist/cjs/ControlRpcs.d.ts.map +1 -0
- package/dist/cjs/ControlRpcs.js +247 -0
- package/dist/cjs/ControlRpcs.js.map +7 -0
- package/dist/cjs/ControlRuntime.d.ts +635 -0
- package/dist/cjs/ControlRuntime.d.ts.map +1 -0
- package/dist/cjs/ControlRuntime.js +744 -0
- package/dist/cjs/ControlRuntime.js.map +7 -0
- package/dist/cjs/ControlSchema.d.ts +2642 -0
- package/dist/cjs/ControlSchema.d.ts.map +1 -0
- package/dist/cjs/ControlSchema.js +634 -0
- package/dist/cjs/ControlSchema.js.map +7 -0
- package/dist/cjs/ControlServer.d.ts +51 -0
- package/dist/cjs/ControlServer.d.ts.map +1 -0
- package/dist/cjs/ControlServer.js +121 -0
- package/dist/cjs/ControlServer.js.map +7 -0
- package/dist/cjs/Credential.d.ts +136 -0
- package/dist/cjs/Credential.d.ts.map +1 -0
- package/dist/cjs/Credential.js +168 -0
- package/dist/cjs/Credential.js.map +7 -0
- package/dist/cjs/CredentialCipher.d.ts +90 -0
- package/dist/cjs/CredentialCipher.d.ts.map +1 -0
- package/dist/cjs/CredentialCipher.js +45 -0
- package/dist/cjs/CredentialCipher.js.map +7 -0
- package/dist/cjs/CredentialStore.d.ts +97 -0
- package/dist/cjs/CredentialStore.d.ts.map +1 -0
- package/dist/cjs/CredentialStore.js +81 -0
- package/dist/cjs/CredentialStore.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +112 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +45 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Health.d.ts +333 -0
- package/dist/cjs/Health.d.ts.map +1 -0
- package/dist/cjs/Health.js +311 -0
- package/dist/cjs/Health.js.map +7 -0
- package/dist/cjs/JevSessionChecker.d.ts +57 -0
- package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
- package/dist/cjs/JevSessionChecker.js +113 -0
- package/dist/cjs/JevSessionChecker.js.map +7 -0
- package/dist/cjs/Lineage.d.ts +131 -0
- package/dist/cjs/Lineage.d.ts.map +1 -0
- package/dist/cjs/Lineage.js +81 -0
- package/dist/cjs/Lineage.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +34 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +60 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/Monitor.d.ts +282 -0
- package/dist/cjs/Monitor.d.ts.map +1 -0
- package/dist/cjs/Monitor.js +283 -0
- package/dist/cjs/Monitor.js.map +7 -0
- package/dist/cjs/ScopedToken.d.ts +193 -0
- package/dist/cjs/ScopedToken.d.ts.map +1 -0
- package/dist/cjs/ScopedToken.js +135 -0
- package/dist/cjs/ScopedToken.js.map +7 -0
- package/dist/cjs/SqlControlRuntime.d.ts +161 -0
- package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
- package/dist/cjs/SqlControlRuntime.js +1522 -0
- package/dist/cjs/SqlControlRuntime.js.map +7 -0
- package/dist/cjs/SqlCredentialStore.d.ts +43 -0
- package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
- package/dist/cjs/SqlCredentialStore.js +113 -0
- package/dist/cjs/SqlCredentialStore.js.map +7 -0
- package/dist/cjs/Steering.d.ts +69 -0
- package/dist/cjs/Steering.d.ts.map +1 -0
- package/dist/cjs/Steering.js +49 -0
- package/dist/cjs/Steering.js.map +7 -0
- package/dist/cjs/SystemFlows.d.ts +223 -0
- package/dist/cjs/SystemFlows.d.ts.map +1 -0
- package/dist/cjs/SystemFlows.js +195 -0
- package/dist/cjs/SystemFlows.js.map +7 -0
- package/dist/cjs/WebCryptoCipher.d.ts +49 -0
- package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
- package/dist/cjs/WebCryptoCipher.js +129 -0
- package/dist/cjs/WebCryptoCipher.js.map +7 -0
- package/dist/cjs/WebhookChannel.d.ts +113 -0
- package/dist/cjs/WebhookChannel.d.ts.map +1 -0
- package/dist/cjs/WebhookChannel.js +98 -0
- package/dist/cjs/WebhookChannel.js.map +7 -0
- package/dist/cjs/index.d.ts +160 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +91 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
- package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/MutationBoundary.js +50 -0
- package/dist/cjs/internal/MutationBoundary.js.map +7 -0
- package/dist/cjs/internal/activeFibers.d.ts +12 -0
- package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
- package/dist/cjs/internal/activeFibers.js +30 -0
- package/dist/cjs/internal/activeFibers.js.map +7 -0
- package/dist/cjs/internal/issues.d.ts +28 -0
- package/dist/cjs/internal/issues.d.ts.map +1 -0
- package/dist/cjs/internal/issues.js +34 -0
- package/dist/cjs/internal/issues.js.map +7 -0
- package/dist/cjs/internal/planning.d.ts +347 -0
- package/dist/cjs/internal/planning.d.ts.map +1 -0
- package/dist/cjs/internal/planning.js +137 -0
- package/dist/cjs/internal/planning.js.map +7 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
- package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_control_tables.js +115 -0
- package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_run_keys.js +44 -0
- package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_signal_commands.js +50 -0
- package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
- package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_signal_principals.js +45 -0
- package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_run_principals.js +49 -0
- package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_resume_consent.js +46 -0
- package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestControl.d.ts +19 -0
- package/dist/cjs/test/TestControl.d.ts.map +1 -0
- package/dist/cjs/test/TestControl.js +62 -0
- package/dist/cjs/test/TestControl.js.map +7 -0
- package/dist/esm/ApprovalAuthority.d.ts +73 -0
- package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
- package/dist/esm/ApprovalAuthority.js +72 -0
- package/dist/esm/ApprovalAuthority.js.map +1 -0
- package/dist/esm/Cancellation.d.ts +107 -0
- package/dist/esm/Cancellation.d.ts.map +1 -0
- package/dist/esm/Cancellation.js +116 -0
- package/dist/esm/Cancellation.js.map +1 -0
- package/dist/esm/Channels.d.ts +170 -0
- package/dist/esm/Channels.d.ts.map +1 -0
- package/dist/esm/Channels.js +312 -0
- package/dist/esm/Channels.js.map +1 -0
- package/dist/esm/Control.d.ts +202 -0
- package/dist/esm/Control.d.ts.map +1 -0
- package/dist/esm/Control.js +42 -0
- package/dist/esm/Control.js.map +1 -0
- package/dist/esm/ControlClient.d.ts +52 -0
- package/dist/esm/ControlClient.d.ts.map +1 -0
- package/dist/esm/ControlClient.js +217 -0
- package/dist/esm/ControlClient.js.map +1 -0
- package/dist/esm/ControlError.d.ts +318 -0
- package/dist/esm/ControlError.d.ts.map +1 -0
- package/dist/esm/ControlError.js +359 -0
- package/dist/esm/ControlError.js.map +1 -0
- package/dist/esm/ControlExecutor.d.ts +372 -0
- package/dist/esm/ControlExecutor.d.ts.map +1 -0
- package/dist/esm/ControlExecutor.js +212 -0
- package/dist/esm/ControlExecutor.js.map +1 -0
- package/dist/esm/ControlFacts.d.ts +454 -0
- package/dist/esm/ControlFacts.d.ts.map +1 -0
- package/dist/esm/ControlFacts.js +324 -0
- package/dist/esm/ControlFacts.js.map +1 -0
- package/dist/esm/ControlLive.d.ts +23 -0
- package/dist/esm/ControlLive.d.ts.map +1 -0
- package/dist/esm/ControlLive.js +1589 -0
- package/dist/esm/ControlLive.js.map +1 -0
- package/dist/esm/ControlRpcs.d.ts +1204 -0
- package/dist/esm/ControlRpcs.d.ts.map +1 -0
- package/dist/esm/ControlRpcs.js +299 -0
- package/dist/esm/ControlRpcs.js.map +1 -0
- package/dist/esm/ControlRuntime.d.ts +635 -0
- package/dist/esm/ControlRuntime.d.ts.map +1 -0
- package/dist/esm/ControlRuntime.js +808 -0
- package/dist/esm/ControlRuntime.js.map +1 -0
- package/dist/esm/ControlSchema.d.ts +2642 -0
- package/dist/esm/ControlSchema.d.ts.map +1 -0
- package/dist/esm/ControlSchema.js +1030 -0
- package/dist/esm/ControlSchema.js.map +1 -0
- package/dist/esm/ControlServer.d.ts +51 -0
- package/dist/esm/ControlServer.d.ts.map +1 -0
- package/dist/esm/ControlServer.js +145 -0
- package/dist/esm/ControlServer.js.map +1 -0
- package/dist/esm/Credential.d.ts +136 -0
- package/dist/esm/Credential.d.ts.map +1 -0
- package/dist/esm/Credential.js +190 -0
- package/dist/esm/Credential.js.map +1 -0
- package/dist/esm/CredentialCipher.d.ts +90 -0
- package/dist/esm/CredentialCipher.d.ts.map +1 -0
- package/dist/esm/CredentialCipher.js +56 -0
- package/dist/esm/CredentialCipher.js.map +1 -0
- package/dist/esm/CredentialStore.d.ts +97 -0
- package/dist/esm/CredentialStore.d.ts.map +1 -0
- package/dist/esm/CredentialStore.js +101 -0
- package/dist/esm/CredentialStore.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +112 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +76 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Health.d.ts +333 -0
- package/dist/esm/Health.d.ts.map +1 -0
- package/dist/esm/Health.js +400 -0
- package/dist/esm/Health.js.map +1 -0
- package/dist/esm/JevSessionChecker.d.ts +57 -0
- package/dist/esm/JevSessionChecker.d.ts.map +1 -0
- package/dist/esm/JevSessionChecker.js +108 -0
- package/dist/esm/JevSessionChecker.js.map +1 -0
- package/dist/esm/Lineage.d.ts +131 -0
- package/dist/esm/Lineage.d.ts.map +1 -0
- package/dist/esm/Lineage.js +174 -0
- package/dist/esm/Lineage.js.map +1 -0
- package/dist/esm/Migrations.d.ts +34 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +53 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/Monitor.d.ts +282 -0
- package/dist/esm/Monitor.d.ts.map +1 -0
- package/dist/esm/Monitor.js +415 -0
- package/dist/esm/Monitor.js.map +1 -0
- package/dist/esm/ScopedToken.d.ts +193 -0
- package/dist/esm/ScopedToken.d.ts.map +1 -0
- package/dist/esm/ScopedToken.js +224 -0
- package/dist/esm/ScopedToken.js.map +1 -0
- package/dist/esm/SqlControlRuntime.d.ts +161 -0
- package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
- package/dist/esm/SqlControlRuntime.js +1756 -0
- package/dist/esm/SqlControlRuntime.js.map +1 -0
- package/dist/esm/SqlCredentialStore.d.ts +43 -0
- package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
- package/dist/esm/SqlCredentialStore.js +97 -0
- package/dist/esm/SqlCredentialStore.js.map +1 -0
- package/dist/esm/Steering.d.ts +69 -0
- package/dist/esm/Steering.d.ts.map +1 -0
- package/dist/esm/Steering.js +89 -0
- package/dist/esm/Steering.js.map +1 -0
- package/dist/esm/SystemFlows.d.ts +223 -0
- package/dist/esm/SystemFlows.d.ts.map +1 -0
- package/dist/esm/SystemFlows.js +198 -0
- package/dist/esm/SystemFlows.js.map +1 -0
- package/dist/esm/WebCryptoCipher.d.ts +49 -0
- package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
- package/dist/esm/WebCryptoCipher.js +123 -0
- package/dist/esm/WebCryptoCipher.js.map +1 -0
- package/dist/esm/WebhookChannel.d.ts +113 -0
- package/dist/esm/WebhookChannel.d.ts.map +1 -0
- package/dist/esm/WebhookChannel.js +109 -0
- package/dist/esm/WebhookChannel.js.map +1 -0
- package/dist/esm/index.d.ts +160 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +160 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/MutationBoundary.d.ts +27 -0
- package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/esm/internal/MutationBoundary.js +40 -0
- package/dist/esm/internal/MutationBoundary.js.map +1 -0
- package/dist/esm/internal/activeFibers.d.ts +12 -0
- package/dist/esm/internal/activeFibers.d.ts.map +1 -0
- package/dist/esm/internal/activeFibers.js +17 -0
- package/dist/esm/internal/activeFibers.js.map +1 -0
- package/dist/esm/internal/issues.d.ts +28 -0
- package/dist/esm/internal/issues.d.ts.map +1 -0
- package/dist/esm/internal/issues.js +35 -0
- package/dist/esm/internal/issues.js.map +1 -0
- package/dist/esm/internal/planning.d.ts +347 -0
- package/dist/esm/internal/planning.d.ts.map +1 -0
- package/dist/esm/internal/planning.js +199 -0
- package/dist/esm/internal/planning.js.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.js +38 -0
- package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
- package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
- package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/esm/migrations/0001_control_tables.js +96 -0
- package/dist/esm/migrations/0001_control_tables.js.map +1 -0
- package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
- package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/esm/migrations/0002_run_keys.js +22 -0
- package/dist/esm/migrations/0002_run_keys.js.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.js +26 -0
- package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.js +25 -0
- package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.js +27 -0
- package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
- package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
- package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0006_run_principals.js +30 -0
- package/dist/esm/migrations/0006_run_principals.js.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.js +27 -0
- package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
- package/dist/esm/test/TestControl.d.ts +19 -0
- package/dist/esm/test/TestControl.d.ts.map +1 -0
- package/dist/esm/test/TestControl.js +30 -0
- package/dist/esm/test/TestControl.js.map +1 -0
- package/docs/README.md +189 -0
- package/docs/api.md +982 -0
- package/docs/concepts/authority.md +109 -0
- package/docs/concepts/cancellation.md +129 -0
- package/docs/concepts/lineage.md +132 -0
- package/docs/concepts/ownership.md +139 -0
- package/docs/concepts/projections.md +203 -0
- package/docs/concepts/receipts.md +128 -0
- package/docs/guides/approvals.md +284 -0
- package/docs/guides/cancel-and-resume.md +162 -0
- package/docs/guides/durable-storage.md +147 -0
- package/docs/guides/implement-an-executor.md +173 -0
- package/docs/guides/ingest-a-webhook.md +177 -0
- package/docs/guides/list-runs.md +160 -0
- package/docs/guides/monitor-runs.md +176 -0
- package/docs/guides/observe-health.md +147 -0
- package/docs/guides/postgres-tests.md +7 -0
- package/docs/guides/serve-over-rpc.md +220 -0
- package/docs/guides/signal-a-run.md +53 -0
- package/docs/guides/steer-a-run.md +138 -0
- package/docs/guides/store-credentials.md +164 -0
- package/docs/guides/testing.md +139 -0
- package/docs/guides/watch-a-run.md +154 -0
- package/docs/installation.md +106 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +208 -0
- package/package.json +405 -3
- package/src/ApprovalAuthority.ts +114 -0
- package/src/Cancellation.ts +172 -0
- package/src/Channels.ts +493 -0
- package/src/Control.ts +337 -0
- package/src/ControlClient.ts +319 -0
- package/src/ControlError.ts +378 -0
- package/src/ControlExecutor.ts +490 -0
- package/src/ControlFacts.ts +383 -0
- package/src/ControlLive.ts +2127 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1601 -0
- package/src/ControlSchema.ts +1380 -0
- package/src/ControlServer.ts +182 -0
- package/src/Credential.ts +310 -0
- package/src/CredentialCipher.ts +110 -0
- package/src/CredentialStore.ts +152 -0
- package/src/DispatchReader.ts +122 -0
- package/src/Health.ts +591 -0
- package/src/JevSessionChecker.ts +127 -0
- package/src/Lineage.ts +203 -0
- package/src/Migrations.ts +56 -0
- package/src/Monitor.ts +600 -0
- package/src/ScopedToken.ts +306 -0
- package/src/SqlControlRuntime.ts +2478 -0
- package/src/SqlCredentialStore.ts +148 -0
- package/src/Steering.ts +96 -0
- package/src/SystemFlows.ts +225 -0
- package/src/WebCryptoCipher.ts +169 -0
- package/src/WebhookChannel.ts +166 -0
- package/src/index.ts +188 -0
- package/src/internal/MutationBoundary.ts +46 -0
- package/src/internal/activeFibers.ts +22 -0
- package/src/internal/issues.ts +40 -0
- package/src/internal/planning.ts +262 -0
- package/src/internal/sqlSchemaErrors.ts +37 -0
- package/src/migrations/0001_control_tables.ts +99 -0
- package/src/migrations/0002_run_keys.ts +23 -0
- package/src/migrations/0003_signal_commands.ts +27 -0
- package/src/migrations/0004_approval_decisions.ts +25 -0
- package/src/migrations/0005_signal_principals.ts +27 -0
- package/src/migrations/0006_run_principals.ts +31 -0
- package/src/migrations/0007_resume_consent.ts +28 -0
- package/src/test/TestControl.ts +47 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Journal projections"
|
|
3
|
+
description: "How watch turns committed journal entries into ControlEvent values, why a cursor scopes to one run, how the snapshot hands off to the live tail, and which deltas the plane derives rather than records."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`watch` is a projection, not a bus. It reads committed journal entries and maps
|
|
9
|
+
each one onto a `ControlEvent`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface ControlEvent {
|
|
13
|
+
readonly sequence: number
|
|
14
|
+
readonly kind: string
|
|
15
|
+
readonly runId?: string | undefined
|
|
16
|
+
readonly occurredAt: number
|
|
17
|
+
readonly payload: Json
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A consumer that subscribes after the fact still receives what it missed,
|
|
22
|
+
because the cursor is durable and the source is a table rather than a live
|
|
23
|
+
fan-out that forgets.
|
|
24
|
+
|
|
25
|
+
## Partitions, and why a cursor needs a run
|
|
26
|
+
|
|
27
|
+
The journal is partitioned. Every run is a partition, and each plan gets one of
|
|
28
|
+
its own under the id `plan:<planId>`. Sequences are partition-local: the plan
|
|
29
|
+
partition and every run partition each start at 0.
|
|
30
|
+
|
|
31
|
+
One scalar cursor applied to all of them would therefore skip every lower
|
|
32
|
+
unseen sequence in every partition but the one the cursor came from. So
|
|
33
|
+
`watch` refuses either `afterSequence` or `afterCursor` without a `runId`:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
InvalidInput: afterSequence: a watch cursor resumes one run, so it requires runId
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Exactly-once resumption is a promise about a scoped watch, and only about a
|
|
40
|
+
scoped one.
|
|
41
|
+
|
|
42
|
+
## Snapshot, follow, and the handoff between them
|
|
43
|
+
|
|
44
|
+
`WatchFilter.follow` selects the delivery mode.
|
|
45
|
+
|
|
46
|
+
- `follow: false` asks for a finite snapshot of what is durable when the
|
|
47
|
+
request is handled. The stream ends. This is the mode a test and a one-shot
|
|
48
|
+
reader want.
|
|
49
|
+
- Omitting `follow` opens the live stream a UI subscribes to. It does not end.
|
|
50
|
+
|
|
51
|
+
The live stream is a handoff, not a deduplicated overlap. The projection
|
|
52
|
+
subscribes to journal changes first, then pins a high-water sequence for each
|
|
53
|
+
partition it can see. A row committed at or below its partition's mark is read
|
|
54
|
+
from the finite snapshot; a row above it is read from the buffered tail. An
|
|
55
|
+
entry from a partition the snapshot never read has no mark and passes straight
|
|
56
|
+
through.
|
|
57
|
+
|
|
58
|
+
The unscoped watch reads eight partition snapshots at a time and keeps one
|
|
59
|
+
reserved slot so the live tail is never starved behind snapshot work. An
|
|
60
|
+
unbounded merge would read every partition of an unbounded database at once,
|
|
61
|
+
which is an allocation a remote watcher could force.
|
|
62
|
+
|
|
63
|
+
## What the plane writes
|
|
64
|
+
|
|
65
|
+
These entries are the control plane's own records. With the SQL runtime and
|
|
66
|
+
journal on the same database, each commits inside the same transaction as the
|
|
67
|
+
state change it describes.
|
|
68
|
+
|
|
69
|
+
| Kind | Written by | Partition |
|
|
70
|
+
| ---------------------------------------------------------------------- | --------------------------------------------------- | ------------------- |
|
|
71
|
+
| `control.plan.created` | `plan`, on creation or repair of a missing entry | `plan:<planId>` |
|
|
72
|
+
| `control.approval.approved`, `control.approval.denied` | `approve`, `deny` | the plan or the run |
|
|
73
|
+
| `control.run.accepted` | `run`, once the row exists | the run |
|
|
74
|
+
| `control.run.running` | `run`, when the executor took the launch | the run |
|
|
75
|
+
| `control.run.pending` | `run`, when it did not | the run |
|
|
76
|
+
| `control.run.resumed` | an approval on a node target, naming the delegation | the run |
|
|
77
|
+
| `control.run.resume` | `resume`, carrying the principal and the reason | the run |
|
|
78
|
+
| `control.run.cancel-requested` | `cancel`, carrying the principal and the reason | the run |
|
|
79
|
+
| `control.run.cancelled`, `control.run.completed`, `control.run.failed` | `cancel` and launch settlement | the run |
|
|
80
|
+
| `control.signal.delivered` | `signal` | the run |
|
|
81
|
+
| `control.steer.enqueued` | `steer` | the run |
|
|
82
|
+
| `control.steer.woke` | `steer`, when it ended a park | the run |
|
|
83
|
+
| `control.monitor.beat`, `control.monitor.healed` | `Monitor.run` | the run |
|
|
84
|
+
|
|
85
|
+
`plan` commits the card, idempotency key, approval token and creation entry in
|
|
86
|
+
one journal transaction. A keyed retry returns the stored card and checks its
|
|
87
|
+
partition for the creation entry. If an older write left that entry missing,
|
|
88
|
+
the retry appends it once before returning.
|
|
89
|
+
|
|
90
|
+
The memory runtime publishes one card per key, including concurrent requests.
|
|
91
|
+
It cannot roll back its maps with a journal transaction. A keyed retry repairs
|
|
92
|
+
a failed creation entry while retaining the original card.
|
|
93
|
+
|
|
94
|
+
## What the plane derives
|
|
95
|
+
|
|
96
|
+
Two kinds are computed from entries other packages wrote, and are emitted
|
|
97
|
+
beside their source entry rather than recorded:
|
|
98
|
+
|
|
99
|
+
| Derived kind | Derived from | Module |
|
|
100
|
+
| ------------------------- | ------------------------------------------------------------- | ---------- |
|
|
101
|
+
| `control.run.lineage` | `flows.engine.run-decision`, `flows.time-travel.fork-created` | `Lineage` |
|
|
102
|
+
| `control.steer.delivered` | `flows/notifications/Promoted` | `Steering` |
|
|
103
|
+
|
|
104
|
+
Deriving rather than re-recording is what keeps the halves honest. The boundary
|
|
105
|
+
that delivers a steer runs in the agent process, not this one, so a control
|
|
106
|
+
plane that wrote its own delivery record would be asserting a fact it did not
|
|
107
|
+
observe.
|
|
108
|
+
|
|
109
|
+
Each derived event carries its source sequence. `watch` also assigns a
|
|
110
|
+
composite `cursor` that distinguishes members of an expansion. Checkpoint
|
|
111
|
+
`event.cursor` and resume with `afterCursor` to retain unconsumed deltas.
|
|
112
|
+
`afterSequence` skips the whole source entry, including its deltas. Expansion
|
|
113
|
+
runs after the snapshot-to-tail handoff.
|
|
114
|
+
|
|
115
|
+
`Lineage.derive`, `Lineage.expand`, `Steering.derive`, and `Steering.expand`
|
|
116
|
+
are exported, so a client reading the journal directly reaches the same
|
|
117
|
+
conclusions the server does.
|
|
118
|
+
|
|
119
|
+
The two foreign event types are named as strings rather than imported. A
|
|
120
|
+
control plane reads journals, not engines, and one that depended on the engine
|
|
121
|
+
could not project a journal a different engine wrote.
|
|
122
|
+
|
|
123
|
+
## Where to go next
|
|
124
|
+
|
|
125
|
+
- [Watch a run's events](../guides/watch-a-run.md): the projection as a task.
|
|
126
|
+
- [Run lineage](./lineage.md): what a `control.run.lineage` delta says.
|
|
127
|
+
- [Steer a running agent](../guides/steer-a-run.md): the two moments a steer
|
|
128
|
+
has, and their two writers.
|
|
129
|
+
|
|
130
|
+
## Versioned lifecycle and approval producer contract
|
|
131
|
+
|
|
132
|
+
`ControlFacts` exports the version-1 control producer contract and the shared
|
|
133
|
+
pure run/approval fold consumed by gateway snapshots and subscriptions. This
|
|
134
|
+
version is independent of the harness transcript's `journalVersion` and of
|
|
135
|
+
journal cursor generations. Existing event kind names and source identities
|
|
136
|
+
remain unchanged; old consumers can continue reading their original fields.
|
|
137
|
+
|
|
138
|
+
A lifecycle fact adds `{factVersion: 1, baseline, run}` to the usual event
|
|
139
|
+
payload. `run` is the complete, detached `RunSummary` returned by that fenced
|
|
140
|
+
control write. `control.run.accepted` starts a `created` baseline. A first
|
|
141
|
+
upgraded status, resume claim, pending handoff, or reconciliation starts a
|
|
142
|
+
`legacy` baseline at its own committed sequence. Earlier history is retained
|
|
143
|
+
without claiming that lost transitions have been reconstructed. Unknown or
|
|
144
|
+
missing lifecycle facts invalidate continuous coverage until another complete
|
|
145
|
+
snapshot establishes a new legacy baseline. No read fabricates migration events.
|
|
146
|
+
|
|
147
|
+
`ControlFacts.commitRun` commits a control write and its fact with
|
|
148
|
+
`Journal.transact`. `AgentSession` uses it for terminal/parked status and resume
|
|
149
|
+
claims; failure does not leave a newer status with no event. `ControlLive`'s
|
|
150
|
+
normal admission, resume, steer, and cancellation transactions carry the same
|
|
151
|
+
facts, and its exceptional launch settlement and terminal reconciliation also
|
|
152
|
+
join a journal transaction. Low-level `ControlRuntime` calls remain available
|
|
153
|
+
for ownership mechanics and legacy integrations; calling them outside these
|
|
154
|
+
producer boundaries does not establish event coverage.
|
|
155
|
+
|
|
156
|
+
`commitApprovalRequest` validates and captures the full request, then commits
|
|
157
|
+
its token registration and `control.approval.requested` event together. A
|
|
158
|
+
resolved token does not emit a new pending request. Decisions add
|
|
159
|
+
`{factVersion: 1, tokenId, approvalTarget}` to their existing payload and commit
|
|
160
|
+
with the decision, grant, idempotency receipt, and durable node-resume intent.
|
|
161
|
+
The pure fold joins current requests and decisions by run/request identity and
|
|
162
|
+
digest. Repeated requests do not reopen a decision. Legacy records retain their
|
|
163
|
+
historical read contract; only legacy requests are eligible for unnamed legacy
|
|
164
|
+
decision fallback.
|
|
165
|
+
|
|
166
|
+
A request a runaway guard makes carries an optional `incident`
|
|
167
|
+
(`ControlFacts.GuardIncident`): its `Runaway` or `Stuck` classification, the
|
|
168
|
+
guard `source` (`tokens`, `usd`, `latency`, `model-call`, `tool-call`, `cell`), the
|
|
169
|
+
triggering `message`, and the numbers frozen when it tripped (`used`,
|
|
170
|
+
`reserved`, `max`, `next`, the `allowance` Continue authorizes, and the
|
|
171
|
+
timed-out `subject`). A host that re-parks the run reuses the recorded request
|
|
172
|
+
and its incident rather than measuring again.
|
|
173
|
+
|
|
174
|
+
These atomic guarantees require the SQL control runtime and journal to share
|
|
175
|
+
the same database/writer, as the production control composition does. The
|
|
176
|
+
in-memory test runtime has no transactional rollback protocol. `SqlJournal`
|
|
177
|
+
publishes committed rows after the owning writer's COMMIT; a failed insert or
|
|
178
|
+
outer transaction publishes no fact. Followers also replay from disk, so a
|
|
179
|
+
process dying after commit and before an in-process notification does not lose
|
|
180
|
+
the recorded transition.
|
|
181
|
+
|
|
182
|
+
This is a control-plane boundary, not a claim that all runtime state is now a
|
|
183
|
+
control-event fold. Native lifecycle observations commit with their own fenced
|
|
184
|
+
state in the engine database. The durable engine bridge copies those facts and
|
|
185
|
+
an authenticated `control.engine.bound` root binding into the control journal.
|
|
186
|
+
Gateway snapshots and subscriptions use the shared `ExecutionFact` fold to
|
|
187
|
+
compare that evidence with the executor's coherent root/current-round view.
|
|
188
|
+
The separate `executionProvenance` reports native `events`, `legacy-observation`
|
|
189
|
+
or `unverified-observation`; `lifecycleProvenance` still labels the executor
|
|
190
|
+
overlay `engine-observed` rather than calling it control replay. Bridge lag,
|
|
191
|
+
missing bindings, generation gaps and unknown versions retain explicit fallback.
|
|
192
|
+
The two databases do not become one transaction, and operational ownership,
|
|
193
|
+
heartbeats and protected resolver credentials retain their native authorities.
|
|
194
|
+
|
|
195
|
+
Authorized native cell calls also commit `flows.harness.call-fact.v1` invocation
|
|
196
|
+
and controller-result facts in their owning action transactions. The immutable
|
|
197
|
+
native journal is their outbox through that same bridge. Shared call projections
|
|
198
|
+
prefer committed facts over matching identified telemetry while preserving
|
|
199
|
+
display identities. Legacy trace producer hashes remain unchanged; unidentified
|
|
200
|
+
calls keep their limited fallback, and low-level custom ports without these
|
|
201
|
+
annotations remain legacy. Model deltas, printed output and other trace records
|
|
202
|
+
still use the best-effort channel. Durable call facts do not reconstruct missing
|
|
203
|
+
trace history or promise exactly-once external effects.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Receipts and idempotency"
|
|
3
|
+
description: "The five receipts a control mutation can answer, the bounded identity boundary every mutation crosses first, and why cancel and resume read a run's terminality before replaying a recorded receipt."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Every control mutation answers a `Receipt` rather than throwing on a second
|
|
9
|
+
ask. A receipt is the plane's whole answer to "did my request take effect, and
|
|
10
|
+
what happened to it?".
|
|
11
|
+
|
|
12
|
+
| Receipt | Meaning |
|
|
13
|
+
| ---------------- | --------------------------------------------------------------------------------------- |
|
|
14
|
+
| `Accepted` | This call admitted the mutation. Carries `receiptId`, and `runId` when a run exists. |
|
|
15
|
+
| `AlreadyApplied` | An earlier call under this key admitted the mutation. Carries the same `runId`. |
|
|
16
|
+
| `Parked` | The plan is waiting for an approval. Carries `planId` and `status: "waiting-approval"`. |
|
|
17
|
+
| `Conflict` | The key names a different intent than the one it was first used for. Carries a message. |
|
|
18
|
+
| `Terminal` | The run had already settled. Carries `runId` and the status it settled with. |
|
|
19
|
+
|
|
20
|
+
`plan` is the exception: it returns a `PlanCard`, because a plan is a value to
|
|
21
|
+
review rather than an outcome to acknowledge.
|
|
22
|
+
|
|
23
|
+
## The identity boundary
|
|
24
|
+
|
|
25
|
+
Before a mutation waits on anything, it copies its own input and decodes the
|
|
26
|
+
copy. Nothing downstream ever sees the caller's object.
|
|
27
|
+
|
|
28
|
+
The copy admits only enumerable own data properties, so an accessor, a
|
|
29
|
+
`toJSON`, a sparse array, a symbol key, a cycle, a non-plain object, or
|
|
30
|
+
ill-formed text is refused with `InvalidInput` before a collaborator is
|
|
31
|
+
touched. That is not defensive tidying: a getter that returns one value to the
|
|
32
|
+
fingerprint and another to the write is the whole exploit.
|
|
33
|
+
|
|
34
|
+
The copy is also bounded:
|
|
35
|
+
|
|
36
|
+
| Bound | Value |
|
|
37
|
+
| ----------------------------------- | --------------------------------- |
|
|
38
|
+
| Canonical bytes | 4 MiB |
|
|
39
|
+
| Nesting depth | 128 |
|
|
40
|
+
| Total JSON values | 100,000 |
|
|
41
|
+
| Total array items and object fields | 100,000 |
|
|
42
|
+
| Idempotency key length | 1 to 1,024 well-formed characters |
|
|
43
|
+
|
|
44
|
+
## What a key identifies
|
|
45
|
+
|
|
46
|
+
The durable key is the operation, the caller's key, and, when a principal is
|
|
47
|
+
present, a digest of that principal's stable `kind` and `id`. The stamped
|
|
48
|
+
`stampedAt` is deliberately excluded: it is a wall clock, and keeping it made a
|
|
49
|
+
bearer authenticated retry of one cancel look like a different mutation under
|
|
50
|
+
the same key.
|
|
51
|
+
|
|
52
|
+
Beside the key the runtime stores a **fingerprint**: a canonical SHA-256 digest
|
|
53
|
+
of the operation, the actor's `id` and `kind`, and the decoded mutation with
|
|
54
|
+
its two server-stamped `principal` fields removed. A second call whose
|
|
55
|
+
fingerprint matches replays the recorded receipt. A second call under the same
|
|
56
|
+
key with a different fingerprint answers `Conflict`, which is the honest answer
|
|
57
|
+
to "this key already means something else".
|
|
58
|
+
|
|
59
|
+
Only `input.principal` and `input.message.principal` are removed, and only at
|
|
60
|
+
their own depth. A `principal` nested inside a signal payload is part of the
|
|
61
|
+
fingerprint, because two signals that differ only there are two different
|
|
62
|
+
signals.
|
|
63
|
+
|
|
64
|
+
## Replay, and the two mutations that do not replay it
|
|
65
|
+
|
|
66
|
+
For a mutation that changes something once, the recorded receipt is everything:
|
|
67
|
+
it is the proof the change was made, and replaying it is the whole guarantee.
|
|
68
|
+
|
|
69
|
+
`cancel` and `resume` are different, because their receipt is an answer _about
|
|
70
|
+
a run_, and the run moves on afterwards. Both read the run's terminality first,
|
|
71
|
+
before the idempotency lookup:
|
|
72
|
+
|
|
73
|
+
- `cancel` runs with replay disabled. A second ask re-executes, reads the run,
|
|
74
|
+
and answers what is true now. A replayed answer would describe the moment of
|
|
75
|
+
the first ask, so a caller could keep cancelling a run that never moved and
|
|
76
|
+
keep being told it was cancelled.
|
|
77
|
+
- `resume` reads terminality before the replay for the same reason. Asking to
|
|
78
|
+
restart a completed run must answer `Terminal`, not `AlreadyApplied`, which
|
|
79
|
+
describes an earlier call and says nothing about the run you named.
|
|
80
|
+
|
|
81
|
+
Cancellation needs no receipt to be idempotent. The run's own terminality is a
|
|
82
|
+
stronger guarantee, and it is what the second ask reads.
|
|
83
|
+
|
|
84
|
+
## Run admission precedes execution
|
|
85
|
+
|
|
86
|
+
The run row, approval facts, and idempotency receipt commit before the plane
|
|
87
|
+
calls the executor. `Accepted` proves admission; the run's current status proves
|
|
88
|
+
whether execution started or finished. Another connection can read the committed
|
|
89
|
+
run as soon as the executor receives it.
|
|
90
|
+
|
|
91
|
+
If the executor refuses the launch, the plane records the run as failed and
|
|
92
|
+
retains its admission receipt. Repeating the same key returns `AlreadyApplied`
|
|
93
|
+
for that run. An explicit retry uses a new key. A failed admission commit never
|
|
94
|
+
reaches the executor.
|
|
95
|
+
|
|
96
|
+
If the admitting process dies after the commit and before the executor takes
|
|
97
|
+
the run, the run stays `accepted` under that process. Repeating the same key
|
|
98
|
+
once the process's heartbeat lease has lapsed claims the run and launches it,
|
|
99
|
+
then answers `AlreadyApplied` as usual. A live admitter keeps its run.
|
|
100
|
+
|
|
101
|
+
## A parked receipt is not recorded
|
|
102
|
+
|
|
103
|
+
`run` against an undecided plan answers `Parked` and records nothing. That is
|
|
104
|
+
what lets the [Quickstart](../quickstart.md) launch, park, approve, and launch
|
|
105
|
+
again under one key: the key was never spent on the refusal.
|
|
106
|
+
|
|
107
|
+
## Where a key comes from
|
|
108
|
+
|
|
109
|
+
A caller chooses it, and the choice is a contract with itself:
|
|
110
|
+
|
|
111
|
+
- A CLI or an operator uses something that names the intent:
|
|
112
|
+
`deploy:v1.4.0`, `cancel:run-17`.
|
|
113
|
+
- A [channel](../guides/ingest-a-webhook.md) uses the platform's own delivery
|
|
114
|
+
id, namespaced by the channel, so a webhook redelivery is the same mutation.
|
|
115
|
+
- A [monitor](../guides/monitor-runs.md) uses
|
|
116
|
+
`monitor:<monitorId>:<remedy>:<runId>:<beat>`, so two monitors watching one
|
|
117
|
+
run never share a key.
|
|
118
|
+
|
|
119
|
+
Reusing a key for a different intent is a caller mistake the plane reports
|
|
120
|
+
rather than absorbs.
|
|
121
|
+
|
|
122
|
+
## Where to go next
|
|
123
|
+
|
|
124
|
+
- [Ownership, fences, and claims](./ownership.md): the other reason a mutation
|
|
125
|
+
refuses.
|
|
126
|
+
- [Cancel a run, and restart one](../guides/cancel-and-resume.md): the two
|
|
127
|
+
verbs that read terminality first.
|
|
128
|
+
- [Troubleshooting](../troubleshooting.md): what each refusal means in practice.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Gate work behind an approval"
|
|
3
|
+
description: "Take a plan approval before a run starts, and a node approval inside a run that already started: what each gate pins, how a step registers its own request, and what a decision restarts."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Two gates carry an approval, and they are different mechanisms worth keeping
|
|
9
|
+
apart. A **plan approval** decides whether a run starts. A **node approval**
|
|
10
|
+
decides something inside a run that already started.
|
|
11
|
+
|
|
12
|
+
Both are decided with the same two verbs, `approve` and `deny`, and both pin
|
|
13
|
+
what was reviewed with a digest and an envelope, so a decision cannot be
|
|
14
|
+
re-aimed at a different request afterwards.
|
|
15
|
+
|
|
16
|
+
## Who may decide
|
|
17
|
+
|
|
18
|
+
An approval payload identifies the request; it is not permission to decide it.
|
|
19
|
+
Authentication supplies a trusted `Principal`, while `ApprovalAuthority` supplies
|
|
20
|
+
the independent host policy. `Control.approve` and `deny` check that policy before
|
|
21
|
+
target reads or receipt replay, and the runtime checks it again at resolution.
|
|
22
|
+
Refusal is `Unauthorized`; unavailable policy storage fails closed with
|
|
23
|
+
`PersistenceError`.
|
|
24
|
+
|
|
25
|
+
The default policy recognizes only the fixed local identity `local/operator`.
|
|
26
|
+
The memory adapter's own default also recognizes its `memory/test` identity, and
|
|
27
|
+
no production policy does. A custom `principal` option,
|
|
28
|
+
an actor's `kind`, or a valid bearer credential does not delegate approval.
|
|
29
|
+
Hosts may replace the policy explicitly:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import * as ApprovalAuthority from "@smthrs/control/ApprovalAuthority"
|
|
33
|
+
import * as SqlControlRuntime from "@smthrs/control/SqlControlRuntime"
|
|
34
|
+
import { Effect } from "effect"
|
|
35
|
+
|
|
36
|
+
const runtime = Effect.gen(function*() {
|
|
37
|
+
const approvalAuthority = yield* ApprovalAuthority.make([
|
|
38
|
+
{
|
|
39
|
+
principal: { id: "local", kind: "operator" },
|
|
40
|
+
scopes: ["once", "run", "remembered"],
|
|
41
|
+
targets: ["Plan", "Node"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
principal: { id: "release-bot", kind: "agent" },
|
|
45
|
+
scopes: ["once"],
|
|
46
|
+
targets: ["Node"]
|
|
47
|
+
}
|
|
48
|
+
])
|
|
49
|
+
return yield* SqlControlRuntime.make({ approvalAuthority })
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Delegations bind an exact identity tuple, target kind, and approval scope. Scopes
|
|
54
|
+
are not hierarchical. A delegated actor may deny its listed target kinds without
|
|
55
|
+
installing any grant. Delegation is reusable: `scopes: ["once"]` permits
|
|
56
|
+
once-scoped grants; it is not a single-use delegation. A custom host policy must
|
|
57
|
+
enforce expiry, revocation, or one-use delegation when needed.
|
|
58
|
+
A custom `ApprovalAuthority.Service` can additionally
|
|
59
|
+
restrict specific runs, plans, or envelopes. Keep that policy bounded and safe
|
|
60
|
+
inside the writer transaction; never invoke Control recursively from it.
|
|
61
|
+
|
|
62
|
+
Transport adapters must authenticate identities and overwrite caller-supplied
|
|
63
|
+
principal fields. Direct Control/runtime references and database access are
|
|
64
|
+
trusted host capabilities, not endpoints for untrusted input. In particular,
|
|
65
|
+
`installBulkGrant` is a storage port, not an authorization API. Use Control for
|
|
66
|
+
an atomic durable decision, grant, journal entry, and receipt.
|
|
67
|
+
|
|
68
|
+
Default MCP surfaces omit approval decisions and auto-approving starts. The
|
|
69
|
+
compatibility server requires both host tool exposure and a separately delegated
|
|
70
|
+
agent identity. Delegating an agent to approve is automated approval, not an
|
|
71
|
+
independent human review. None of these checks sandboxes a caller that already
|
|
72
|
+
has arbitrary host shell, code execution, or direct database write access.
|
|
73
|
+
On loopback, `smthrs serve` prints a fresh per-session approval token to
|
|
74
|
+
stderr (even with `--quiet`). Send `Authorization: Bearer <token>` to approve,
|
|
75
|
+
deny, or answer a human wait over `/rpc` or `/projections`, HTTP or WebSocket.
|
|
76
|
+
Calls without that token retain read access, but cannot decide approvals.
|
|
77
|
+
Keep the token away from agents: a caller holding it has operator approval authority.
|
|
78
|
+
A network bind still requires the configured bearer credential for all protected calls.
|
|
79
|
+
|
|
80
|
+
## Gate the launch: a plan approval
|
|
81
|
+
|
|
82
|
+
`plan` produces the reviewable card. `run` against an undecided plan starts
|
|
83
|
+
nothing and says so:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Control } from "@smthrs/control/Control"
|
|
87
|
+
import * as Effect from "effect/Effect"
|
|
88
|
+
|
|
89
|
+
const gate = Effect.gen(function*() {
|
|
90
|
+
const control = yield* Control
|
|
91
|
+
|
|
92
|
+
const card = yield* control.plan({ flowId: "ops/Deploy", input: { build: "v1.4.0" } })
|
|
93
|
+
|
|
94
|
+
const launch = {
|
|
95
|
+
_tag: "Plan" as const,
|
|
96
|
+
planId: card.planId,
|
|
97
|
+
digest: card.digest,
|
|
98
|
+
envelope: card.envelope
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// { _tag: "Parked", planId, status: "waiting-approval" }
|
|
102
|
+
yield* control.run({ ...launch, idempotencyKey: "deploy:v1.4.0" })
|
|
103
|
+
|
|
104
|
+
// The card carries the exact approval payload, so a reviewer resubmits it
|
|
105
|
+
// unchanged rather than reconstructing authority on the client.
|
|
106
|
+
yield* control.approve(card.approval)
|
|
107
|
+
|
|
108
|
+
// Now the same call launches.
|
|
109
|
+
return yield* control.run({ ...launch, idempotencyKey: "deploy:v1.4.0" })
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Deny it instead and the launch refuses with `PlanDenied` rather than parking.
|
|
114
|
+
A denied plan cannot be revived: create and approve a new plan.
|
|
115
|
+
|
|
116
|
+
### What the card pins
|
|
117
|
+
|
|
118
|
+
`PlanCard.digest` covers the flow id, the decoded input, the envelope, the
|
|
119
|
+
deploy-class flag, and the digest of the persisted plan. `nodes` is the keyed
|
|
120
|
+
node graph the plan phase produced, and it is part of the digest because
|
|
121
|
+
"approve this flow with this input" and "approve this graph of keyed work" are
|
|
122
|
+
different promises: a change that re-keys a node changes what will run, and an
|
|
123
|
+
approval taken against the old graph must not authorize the new one. A host
|
|
124
|
+
that has not built a graph reports an empty one and loses nothing.
|
|
125
|
+
|
|
126
|
+
`Envelope` is the authority itself: the capabilities, the collaborator flows,
|
|
127
|
+
the budget, and the placement being granted.
|
|
128
|
+
|
|
129
|
+
| Submitted value differs from the stored one | Failure |
|
|
130
|
+
| ------------------------------------------- | --------------------------------------------------------------------- |
|
|
131
|
+
| `digest` | `PlanDigestMismatch`, carrying `expected` and `actual` |
|
|
132
|
+
| `envelope` | `EnvelopeMismatch`, compared by canonical bytes rather than key order |
|
|
133
|
+
|
|
134
|
+
`GrantScope` says how long the grant lasts: `once`, `run`, or `remembered`. The
|
|
135
|
+
card defaults to `run`.
|
|
136
|
+
|
|
137
|
+
## Gate a step: a node approval
|
|
138
|
+
|
|
139
|
+
A node approval belongs to a run that already exists, and the _step_ registers
|
|
140
|
+
it. Nothing outside the run asks for it, so `approve` can only decide a request
|
|
141
|
+
a step actually made.
|
|
142
|
+
|
|
143
|
+
The step registers the token and handles its explicit `Pending`, `Approved`, or
|
|
144
|
+
`Denied` tag. Only approval may open the gate:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import * as ControlRuntime from "@smthrs/control/ControlRuntime"
|
|
148
|
+
import type * as ControlSchema from "@smthrs/control/ControlSchema"
|
|
149
|
+
import { Action, DurableDeferred, FlowRuntime } from "@smthrs/flow"
|
|
150
|
+
import * as Effect from "effect/Effect"
|
|
151
|
+
import * as Schema from "effect/Schema"
|
|
152
|
+
|
|
153
|
+
/** The step that asks, declared like any other. */
|
|
154
|
+
const Clearance = Action.make("ops/Clearance", {
|
|
155
|
+
payload: { requestId: Schema.String },
|
|
156
|
+
success: Schema.String,
|
|
157
|
+
error: Schema.Union([ControlRuntime.ApprovalPending, ControlRuntime.ApprovalDenied])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The wait point the park awaits. Nothing ever completes it, which is the
|
|
162
|
+
* point: an approval is not a value arriving, it is a decision recorded
|
|
163
|
+
* somewhere else. The wake is the run being re-driven, and the step reads the
|
|
164
|
+
* decision off the token rather than off this deferred.
|
|
165
|
+
*/
|
|
166
|
+
const clearanceGate = DurableDeferred.make("Approval/ship-clearance", { success: Schema.Json })
|
|
167
|
+
|
|
168
|
+
/** The exact request this step registers and an operator decides. */
|
|
169
|
+
const request = (runId: string): Extract<ControlSchema.ApprovalTarget, { readonly _tag: "Node" }> => ({
|
|
170
|
+
_tag: "Node",
|
|
171
|
+
runId: runId as ControlSchema.RunId,
|
|
172
|
+
requestId: "ship-clearance",
|
|
173
|
+
digest: "ops/Ship:clearance",
|
|
174
|
+
envelope: { capabilities: [], flows: [], budget: {} }
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const clearance = Clearance.toLayer(({ requestId }) =>
|
|
178
|
+
Effect.gen(function*() {
|
|
179
|
+
const instance = yield* FlowRuntime.FlowInstance
|
|
180
|
+
const runtime = yield* ControlRuntime.ControlRuntime
|
|
181
|
+
const target = request(instance.executionId)
|
|
182
|
+
let token = yield* Effect.orDie(runtime.registerApproval(target))
|
|
183
|
+
if (token._tag === "Pending") {
|
|
184
|
+
yield* FlowRuntime.annotateWaiting({ reason: "approval", token: requestId })
|
|
185
|
+
yield* DurableDeferred.await(clearanceGate)
|
|
186
|
+
// Completing a wait point is not approval. Read the durable answer.
|
|
187
|
+
token = yield* Effect.orDie(runtime.registerApproval(target))
|
|
188
|
+
}
|
|
189
|
+
return (yield* ControlRuntime.requireApproved(token)).tokenId
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`registerApproval` is idempotent. It creates the token on the first attempt and
|
|
195
|
+
answers the existing one afterwards. `requireApproved` succeeds only for
|
|
196
|
+
`Approved`, fails with `ApprovalDenied` on denial, and fails with
|
|
197
|
+
`ApprovalPending` if no decision exists. Include these errors in the enclosing
|
|
198
|
+
flow's error schema. Use `Node.andThen(clearance, next)` to gate all of `next`.
|
|
199
|
+
Both terminal tags carry `decisionPrincipal` and `decidedAt`; `Approved` also
|
|
200
|
+
carries the installed grant's `scope`. A denied token cannot be changed to
|
|
201
|
+
approved. A changed target digest or envelope is refused.
|
|
202
|
+
|
|
203
|
+
The token describes a decision; it is not an authentication credential and
|
|
204
|
+
`requireApproved` does not install permissions or establish who may approve.
|
|
205
|
+
Keep approval authority separate from permission to execute a workflow.
|
|
206
|
+
|
|
207
|
+
### Upgrading an unfinished run
|
|
208
|
+
|
|
209
|
+
Migration 6004 adds explicit durable decisions. Old pending tokens remain
|
|
210
|
+
pending. An old resolved token did not retain whether it was approved or
|
|
211
|
+
denied, so reads fail with an actionable `PersistenceError` instead of guessing
|
|
212
|
+
from grants or journal projections. Preserve the database for review and use a
|
|
213
|
+
new run with a new approval request. This is refusal, not automatic migration of
|
|
214
|
+
unfinished executions. The action and flow error schema changes also require
|
|
215
|
+
newly planned/approved work; they do not retrofit old cached action results.
|
|
216
|
+
|
|
217
|
+
The complete example, with the two drives around the decision, is
|
|
218
|
+
[`examples/src/18-approval-and-signal.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/18-approval-and-signal.ts).
|
|
219
|
+
|
|
220
|
+
### Decide it from outside the run
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const receipt = yield * control.approve({
|
|
224
|
+
target: request("run-17"),
|
|
225
|
+
scope: "once",
|
|
226
|
+
idempotencyKey: "approve:ship-clearance"
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The node digest is deliberately not the plan's. The two gates are separate
|
|
231
|
+
mechanisms, and a run that was never planned still has steps worth gating.
|
|
232
|
+
|
|
233
|
+
## What a decision does
|
|
234
|
+
|
|
235
|
+
For a new decision, the adapter contract follows this order:
|
|
236
|
+
|
|
237
|
+
| Step | Plan target | Node target |
|
|
238
|
+
| ------------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
239
|
+
| Authenticate and authorize | Authenticate the principal; `authorizeApproval` before reads or receipt replay | same |
|
|
240
|
+
| Look the token up | `lookupApproval` refuses an unknown or already-resolved token | same |
|
|
241
|
+
| Resolve the token | `resolveApproval` exactly once, with an authority recheck | same |
|
|
242
|
+
| Install the grant, on approval | `installBulkGrant` with the submitted envelope and scope | same |
|
|
243
|
+
| Journal the decision | `control.approval.approved` or `.denied` on `plan:<planId>` | the same kinds, on the run |
|
|
244
|
+
| Record the restart | nothing to restart | records a resume delegation, journals `control.run.resumed` |
|
|
245
|
+
|
|
246
|
+
Commit the decision, grant, journal entry, receipt, and any node resume delegation
|
|
247
|
+
atomically. Resolution must not require an installed grant or a flushed journal
|
|
248
|
+
decision. An authority refusal at resolution prevents grant installation.
|
|
249
|
+
|
|
250
|
+
A decision on a node target restarts the run the ask parked, in the same call.
|
|
251
|
+
Nothing else wakes that run: without the restart it would sit at
|
|
252
|
+
`waiting-approval` holding a decision it never reads, and a denial it never
|
|
253
|
+
learns about would decide nothing.
|
|
254
|
+
|
|
255
|
+
The restart is _recorded_, not performed, and the deciding plane does not claim
|
|
256
|
+
the row. See [a resume is a delegation before it is a claim](../concepts/ownership.md).
|
|
257
|
+
|
|
258
|
+
A decision on a run that has already settled answers `Terminal` and decides
|
|
259
|
+
nothing, read before the idempotency replay so the answer describes the run
|
|
260
|
+
rather than an earlier call.
|
|
261
|
+
|
|
262
|
+
## Refusals
|
|
263
|
+
|
|
264
|
+
| Failure | Cause |
|
|
265
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
266
|
+
| `Unauthorized` | The authenticated caller lacks approval authority for this target/scope. |
|
|
267
|
+
| `PlanNotFound` | No plan or node token with this id. Its `message` names the next action. |
|
|
268
|
+
| `PlanDenied` | The plan was denied. Create and approve a new one. |
|
|
269
|
+
| `PlanDigestMismatch` | The submitted digest is not the stored one. |
|
|
270
|
+
| `EnvelopeMismatch` | The submitted envelope is not the stored one. |
|
|
271
|
+
| `AlreadyResolved` | This token already carries a terminal decision. |
|
|
272
|
+
| `RunNotFound` | A node target names a run this plane cannot find. |
|
|
273
|
+
|
|
274
|
+
`AlreadyResolved` is also the durable evidence that a decision stuck: a second
|
|
275
|
+
`lookupApproval` on a decided token refuses rather than answering.
|
|
276
|
+
|
|
277
|
+
## Where to go next
|
|
278
|
+
|
|
279
|
+
- [Receipts and idempotency](../concepts/receipts.md): why the launch and the
|
|
280
|
+
retry share one key.
|
|
281
|
+
- [Ownership, fences, and claims](../concepts/ownership.md): what the recorded
|
|
282
|
+
resume reaches.
|
|
283
|
+
- [Plan, approve, run on smithers.sh](/docs/guides/plan-approve-run/): the same
|
|
284
|
+
gate from the CLI.
|