@k2b/cloud 0.6.0-rc.0
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/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns an app's declared actions into something the executor can run.
|
|
3
|
+
*
|
|
4
|
+
* This is what makes "an app brings only actions" true rather than aspirational.
|
|
5
|
+
* Without it, every app would hand-wire the same four things around every
|
|
6
|
+
* external call: resolve the config, charge the budget, mark the effect as
|
|
7
|
+
* started, settle it afterwards. Both apps did, slightly differently, which is
|
|
8
|
+
* how one of them ended up with no budget at all.
|
|
9
|
+
*
|
|
10
|
+
* The budget is charged from the action's own `plan` hook — the same hook a dry
|
|
11
|
+
* run uses. That is the point: preflight and execution cannot disagree about
|
|
12
|
+
* what an action costs, because one function answers for both. Mail's known
|
|
13
|
+
* defect was exactly that divergence, and here it is not a bug to fix but a
|
|
14
|
+
* shape that cannot occur.
|
|
15
|
+
*/
|
|
16
|
+
import type { SQL } from "bun";
|
|
17
|
+
import { type WorkflowDependency, type WorkflowJsonValue, type WorkflowStepOutcome, workflowPathKey } from "../contracts";
|
|
18
|
+
import type { ErasedWorkflowAction, WorkflowActionContext } from "../definition";
|
|
19
|
+
import type { DefinedWorkflowModule } from "../module";
|
|
20
|
+
import type {
|
|
21
|
+
WorkflowActionStep,
|
|
22
|
+
WorkflowDryRunActionContext,
|
|
23
|
+
WorkflowDryRunActionPort,
|
|
24
|
+
WorkflowExecuteActionContext,
|
|
25
|
+
WorkflowExecuteActionPort,
|
|
26
|
+
} from "../runtime/ports";
|
|
27
|
+
import { budgetError, budgetRootRunId, chargeWorkflowEffectBudget } from "./budget";
|
|
28
|
+
import { beginWorkflowEffect, readWorkflowEffect, recordWorkflowEffect, settleWorkflowEffect } from "./runs";
|
|
29
|
+
import { withTransaction } from "./transaction";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The key an idempotent effect deduplicates on.
|
|
33
|
+
*
|
|
34
|
+
* Derived from the run and step rather than generated, so a replay after a
|
|
35
|
+
* crash presents the provider with the same key and gets the same answer
|
|
36
|
+
* instead of performing the work twice.
|
|
37
|
+
*/
|
|
38
|
+
export const workflowEffectKey = (runId: string, stepKey: string): string => `workflow:${runId}:step:${stepKey}`;
|
|
39
|
+
|
|
40
|
+
/** Unwinds a transactional action's transaction while keeping how it failed. */
|
|
41
|
+
class WorkflowTransactionalFailure extends Error {
|
|
42
|
+
constructor(readonly failure: { message: string; code?: string; retryable?: boolean }) {
|
|
43
|
+
super(failure.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Rolls a transactional action back before parking its run. */
|
|
48
|
+
class WorkflowTransactionalWaiting extends Error {
|
|
49
|
+
constructor(readonly dependency: WorkflowDependency) {
|
|
50
|
+
super("workflow action is waiting");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type StepError = Extract<WorkflowStepOutcome, { state: "failed" }>["error"];
|
|
55
|
+
|
|
56
|
+
const asError = (message: string, retryable = false, code = "WORKFLOW_ACTION_ERROR"): StepError => ({ code, message, retryable });
|
|
57
|
+
|
|
58
|
+
/** Carries the action's own code and retryability into the run, where both are read. */
|
|
59
|
+
const reportedError = (result: { message: string; code?: string; retryable?: boolean }): StepError => ({
|
|
60
|
+
code: result.code ?? "WORKFLOW_ACTION_ERROR",
|
|
61
|
+
message: result.message,
|
|
62
|
+
retryable: result.retryable ?? false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a step's written config into the values the implementation receives.
|
|
67
|
+
*
|
|
68
|
+
* References in the config — `{{ steps.foo.output }}` and friends — are what the
|
|
69
|
+
* workflow language exists for, so they have to be evaluated before the action
|
|
70
|
+
* ever sees them. The action's parameter type is derived from its schema, which
|
|
71
|
+
* is what keeps the two honest.
|
|
72
|
+
*/
|
|
73
|
+
const resolveConfig = async (
|
|
74
|
+
ctx: { evaluate(value: WorkflowJsonValue, path?: Array<string | number>): Promise<WorkflowJsonValue> },
|
|
75
|
+
step: WorkflowActionStep,
|
|
76
|
+
): Promise<Record<string, WorkflowJsonValue>> => {
|
|
77
|
+
const resolved = await ctx.evaluate(step.config as WorkflowJsonValue, [...step.sourcePath, step.action]);
|
|
78
|
+
return (resolved && typeof resolved === "object" && !Array.isArray(resolved) ? resolved : {}) as Record<string, WorkflowJsonValue>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The context an action implementation receives, built once per invocation. */
|
|
82
|
+
const actionContext = (
|
|
83
|
+
ctx: WorkflowExecuteActionContext | WorkflowDryRunActionContext,
|
|
84
|
+
step: WorkflowActionStep,
|
|
85
|
+
effectKey: string,
|
|
86
|
+
tx?: SQL,
|
|
87
|
+
) => {
|
|
88
|
+
// Both `binding` and `resolveReference` are relative to this step's own config,
|
|
89
|
+
// so an implementation names the key it wrote rather than reassembling the
|
|
90
|
+
// plan-wide path the compiler used.
|
|
91
|
+
const configPath = (path: Array<string | number>) => [...step.sourcePath, step.action, ...path];
|
|
92
|
+
return {
|
|
93
|
+
runId: ctx.run.runId,
|
|
94
|
+
stepKey: ctx.step.key,
|
|
95
|
+
invocation: ctx.invocation,
|
|
96
|
+
effectKey,
|
|
97
|
+
...(tx ? { tx } : {}),
|
|
98
|
+
binding: (...path: Array<string | number>): WorkflowJsonValue | undefined => ctx.plan.bindings[workflowPathKey(configPath(path))],
|
|
99
|
+
resolveReference: (reference: string, ...path: Array<string | number>): Promise<WorkflowJsonValue | undefined> =>
|
|
100
|
+
ctx.resolveReference(reference, configPath(path)),
|
|
101
|
+
variableSnapshot: (): Record<string, WorkflowJsonValue> => ctx.variables.snapshot?.() ?? {},
|
|
102
|
+
heartbeat: async (): Promise<void> => {
|
|
103
|
+
await ctx.heartbeat();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Refused access is a failure of this step, not of the whole run. */
|
|
109
|
+
const denied = (): WorkflowStepOutcome => ({
|
|
110
|
+
state: "failed",
|
|
111
|
+
error: { code: "FORBIDDEN", message: "not authorized to perform this action", retryable: false },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stores a step's output under the name its config gives.
|
|
116
|
+
*
|
|
117
|
+
* A plan refers to an earlier step's result by name, so something has to put it
|
|
118
|
+
* there. Both apps did it inside every action — which meant every action also
|
|
119
|
+
* had to remember to do it again when a replay restored a recorded outcome, and
|
|
120
|
+
* an action that forgot produced a plan that silently resolved to nothing.
|
|
121
|
+
*/
|
|
122
|
+
const applySaveAs = (
|
|
123
|
+
ctx: { variables: { set(name: string, value: WorkflowJsonValue): void } },
|
|
124
|
+
step: WorkflowActionStep,
|
|
125
|
+
output: WorkflowJsonValue | undefined,
|
|
126
|
+
): void => {
|
|
127
|
+
const name = step.config.saveAs;
|
|
128
|
+
if (typeof name === "string" && name && output !== undefined) ctx.variables.set(name, output);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** The step an effect belongs to. Its generation fences a stale worker's writes. */
|
|
132
|
+
export type WorkflowEffectJournalStep = { runId: string; key: string; executionGeneration: number };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Where the evidence that an effect happened is written.
|
|
136
|
+
*
|
|
137
|
+
* The kernel's own journal is the default and the destination. The seam exists
|
|
138
|
+
* because an app adopts declared actions before its runs live in
|
|
139
|
+
* `workflows.run`, and during that window the evidence has to land in the table
|
|
140
|
+
* that does hold its runs. The ordering guarantees are the port's, not the
|
|
141
|
+
* table's, so they hold either way.
|
|
142
|
+
*/
|
|
143
|
+
export type WorkflowEffectJournalPort = {
|
|
144
|
+
/** What an earlier attempt recorded about this step's effect, if anything. */
|
|
145
|
+
read(step: WorkflowEffectJournalStep): Promise<{ key: string; state: string; output: WorkflowJsonValue } | null>;
|
|
146
|
+
/** Marks an effect as started, before it is performed. */
|
|
147
|
+
begin(step: WorkflowEffectJournalStep, effectKey: string): Promise<void>;
|
|
148
|
+
/** Records a completed effect on the handle that performed it, so both commit together. */
|
|
149
|
+
record(tx: SQL, step: WorkflowEffectJournalStep, effectKey: string, output: WorkflowJsonValue): Promise<void>;
|
|
150
|
+
/** Settles an effect once its fate is known. `ambiguous` is a real answer, not a failure. */
|
|
151
|
+
settle(step: WorkflowEffectJournalStep, state: "succeeded" | "ambiguous" | "failed"): Promise<void>;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const kernelEffectJournal = (db?: SQL): WorkflowEffectJournalPort => ({
|
|
155
|
+
read: (step) => readWorkflowEffect(step, { ...(db ? { db } : {}) }),
|
|
156
|
+
begin: (step, effectKey) => beginWorkflowEffect(step, effectKey, { ...(db ? { db } : {}) }),
|
|
157
|
+
record: (tx, step, effectKey, output) => recordWorkflowEffect(tx, step, effectKey, output),
|
|
158
|
+
settle: (step, state) => settleWorkflowEffect(step, state, { ...(db ? { db } : {}) }),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export type WorkflowActionPortOptions = {
|
|
162
|
+
/** Charged against the root of a fan-out, so children share one allowance. */
|
|
163
|
+
budget?: boolean;
|
|
164
|
+
db?: SQL;
|
|
165
|
+
journal?: WorkflowEffectJournalPort;
|
|
166
|
+
/** App-owned authorization applied to selected shared actions at execution time. */
|
|
167
|
+
authorize?: (context: WorkflowActionContext, step: WorkflowActionStep) => Promise<boolean>;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Runs one declared action, with everything its effect class implies.
|
|
172
|
+
*
|
|
173
|
+
* The order is the contract: plan, then charge, then mark, then act, then
|
|
174
|
+
* settle. Charging before acting is the only ordering that cannot overspend,
|
|
175
|
+
* and marking before acting is the only one that leaves evidence when the
|
|
176
|
+
* process dies mid-effect.
|
|
177
|
+
*/
|
|
178
|
+
const runDeclaredAction = async (
|
|
179
|
+
action: ErasedWorkflowAction,
|
|
180
|
+
ctx: WorkflowExecuteActionContext,
|
|
181
|
+
step: WorkflowActionStep,
|
|
182
|
+
options: WorkflowActionPortOptions,
|
|
183
|
+
): Promise<WorkflowStepOutcome> => {
|
|
184
|
+
const config = await resolveConfig(ctx, step);
|
|
185
|
+
const journalStep = { runId: ctx.run.runId, key: ctx.step.key, executionGeneration: ctx.run.executionGeneration };
|
|
186
|
+
const effectKey = workflowEffectKey(ctx.run.runId, ctx.step.key);
|
|
187
|
+
const journal = options.journal ?? kernelEffectJournal(options.db);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Asks the action what it would do, and charges the run for it.
|
|
191
|
+
*
|
|
192
|
+
* Only reached when there is a budget to charge: the answer is otherwise
|
|
193
|
+
* discarded, and `plan` is allowed to be expensive — an HTTP action resolves
|
|
194
|
+
* its target to check it is safe to call.
|
|
195
|
+
*/
|
|
196
|
+
const charge = async (db?: SQL): Promise<Extract<WorkflowStepOutcome, { state: "failed" }> | null> => {
|
|
197
|
+
if (options.budget === false || !action.plan) return null;
|
|
198
|
+
const planned = await action.plan(actionContext(ctx, step, effectKey), config as never);
|
|
199
|
+
if (!planned.consumes || Object.keys(planned.consumes).length === 0) return null;
|
|
200
|
+
const handle = db ?? options.db;
|
|
201
|
+
const root = await budgetRootRunId(ctx.run.runId, { ...(handle ? { db: handle } : {}) });
|
|
202
|
+
const charged = await chargeWorkflowEffectBudget(root, planned.consumes, { ...(handle ? { db: handle } : {}) });
|
|
203
|
+
if (charged.state !== "exceeded") return null;
|
|
204
|
+
const error = budgetError(charged);
|
|
205
|
+
return {
|
|
206
|
+
state: "failed",
|
|
207
|
+
error: {
|
|
208
|
+
...asError(error.message, false, "WORKFLOW_BUDGET_EXCEEDED"),
|
|
209
|
+
details: {
|
|
210
|
+
dimension: error.dimension,
|
|
211
|
+
limit: error.limit,
|
|
212
|
+
used: error.used,
|
|
213
|
+
requested: error.requested,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/*
|
|
220
|
+
* An earlier attempt of this step may have escaped without telling us how it
|
|
221
|
+
* ended. Re-running is how the same message goes out twice, so ask the
|
|
222
|
+
* external system instead — that is the entire reason `reconcile` is
|
|
223
|
+
* mandatory for this class.
|
|
224
|
+
*
|
|
225
|
+
* Only ambiguous actions need it. A transactional effect was undone by the
|
|
226
|
+
* crash that interrupted it, and an idempotent one is safe to repeat under
|
|
227
|
+
* the same key.
|
|
228
|
+
*/
|
|
229
|
+
if (action.effect === "ambiguous" && action.reconcile) {
|
|
230
|
+
const prior = await journal.read(journalStep);
|
|
231
|
+
if (prior && (prior.state === "executing" || prior.state === "ambiguous")) {
|
|
232
|
+
let verdict: Awaited<ReturnType<NonNullable<typeof action.reconcile>>>;
|
|
233
|
+
try {
|
|
234
|
+
verdict = await action.reconcile(actionContext(ctx, step, prior.key), prior.key);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return {
|
|
237
|
+
state: "failed",
|
|
238
|
+
error: asError(error instanceof Error ? error.message : String(error), true, "WORKFLOW_EFFECT_RECONCILE_FAILED"),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (verdict.state === "succeeded") {
|
|
242
|
+
await journal.settle(journalStep, "succeeded");
|
|
243
|
+
return { state: "completed", output: (verdict.output ?? null) as WorkflowJsonValue };
|
|
244
|
+
}
|
|
245
|
+
if (verdict.state === "failed") {
|
|
246
|
+
await journal.settle(journalStep, "failed");
|
|
247
|
+
return { state: "failed", error: reportedError(verdict) };
|
|
248
|
+
}
|
|
249
|
+
// Still unknown. A human decides; nothing is repeated on their behalf.
|
|
250
|
+
await journal.settle(journalStep, "ambiguous");
|
|
251
|
+
return { state: "needs_attention", error: reportedError(verdict) };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/*
|
|
256
|
+
* A transactional action performs its work and records that it happened in
|
|
257
|
+
* one transaction, so a crash leaves neither. A replay therefore finds the
|
|
258
|
+
* recorded outcome and returns it instead of doing the work a second time.
|
|
259
|
+
*/
|
|
260
|
+
if (action.effect === "transactional") {
|
|
261
|
+
const prior = await journal.read(journalStep);
|
|
262
|
+
if (prior?.state === "succeeded") return { state: "completed", output: prior.output };
|
|
263
|
+
|
|
264
|
+
return withTransaction(options.db, async (tx) => {
|
|
265
|
+
const txCtx = actionContext(ctx, step, effectKey, tx);
|
|
266
|
+
// Checked on the transaction's own handle: access can be revoked between
|
|
267
|
+
// queueing and running, and a check on another connection is checking a
|
|
268
|
+
// world this write will not see.
|
|
269
|
+
if (options.authorize && !(await options.authorize(txCtx, step))) return denied();
|
|
270
|
+
if (action.authorize && !(await action.authorize(txCtx, config as never))) return denied();
|
|
271
|
+
const overspent = await charge(tx);
|
|
272
|
+
if (overspent) return overspent;
|
|
273
|
+
|
|
274
|
+
const result = await action.run(txCtx, config as never);
|
|
275
|
+
// Let the transaction unwind: the effect did not happen.
|
|
276
|
+
if (result.state === "waiting") throw new WorkflowTransactionalWaiting(result.dependency);
|
|
277
|
+
if (result.state !== "succeeded") throw new WorkflowTransactionalFailure(result);
|
|
278
|
+
await journal.record(tx, journalStep, effectKey, (result.output ?? null) as WorkflowJsonValue);
|
|
279
|
+
return { state: "completed", output: (result.output ?? null) as WorkflowJsonValue } satisfies WorkflowStepOutcome;
|
|
280
|
+
}).catch((error) => {
|
|
281
|
+
if (error instanceof WorkflowTransactionalWaiting)
|
|
282
|
+
return { state: "waiting", dependency: error.dependency } satisfies WorkflowStepOutcome;
|
|
283
|
+
if (error instanceof WorkflowTransactionalFailure)
|
|
284
|
+
return { state: "failed", error: reportedError(error.failure) } satisfies WorkflowStepOutcome;
|
|
285
|
+
throw error;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const context = actionContext(ctx, step, effectKey);
|
|
290
|
+
if (options.authorize && !(await options.authorize(context, step))) return denied();
|
|
291
|
+
if (action.authorize && !(await action.authorize(context, config as never))) return denied();
|
|
292
|
+
|
|
293
|
+
if (action.effect !== "pure") {
|
|
294
|
+
// Charged per attempt. A replayed in-flight step charges twice, which is
|
|
295
|
+
// conservative — it can only refuse more, never permit more.
|
|
296
|
+
const overspent = await charge();
|
|
297
|
+
if (overspent) return overspent;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Only an ambiguous effect needs evidence that it started: the others are
|
|
301
|
+
// either safe to repeat or undone by the crash that interrupted them.
|
|
302
|
+
if (action.effect === "ambiguous") await journal.begin(journalStep, effectKey);
|
|
303
|
+
|
|
304
|
+
let result: Awaited<ReturnType<typeof action.run>>;
|
|
305
|
+
try {
|
|
306
|
+
result = await action.run(context, config as never);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (action.effect !== "ambiguous") throw error;
|
|
309
|
+
// The exception happened after the external effect was marked executing.
|
|
310
|
+
// Its fate is unknown, so never turn it into a retryable ordinary failure.
|
|
311
|
+
await journal.settle(journalStep, "ambiguous");
|
|
312
|
+
return {
|
|
313
|
+
state: "needs_attention",
|
|
314
|
+
error: asError(error instanceof Error ? error.message : String(error), false, "WORKFLOW_EFFECT_OUTCOME_UNKNOWN"),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (action.effect === "ambiguous") {
|
|
319
|
+
await journal.settle(journalStep, result.state === "succeeded" ? "succeeded" : result.state === "ambiguous" ? "ambiguous" : "failed");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
switch (result.state) {
|
|
323
|
+
case "succeeded":
|
|
324
|
+
return { state: "completed", output: (result.output ?? null) as WorkflowJsonValue };
|
|
325
|
+
case "failed":
|
|
326
|
+
return { state: "failed", error: reportedError(result) };
|
|
327
|
+
case "waiting":
|
|
328
|
+
return { state: "waiting", dependency: result.dependency };
|
|
329
|
+
case "ambiguous":
|
|
330
|
+
// Not a failure: "the send may have gone through" needs a human, and
|
|
331
|
+
// treating it as failure either loses messages or sends them twice.
|
|
332
|
+
return { state: "needs_attention", error: reportedError(result) };
|
|
333
|
+
}
|
|
334
|
+
throw new Error("workflow action returned an unknown state");
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The port the worker executes with, built from an app's `workflows.ts`.
|
|
339
|
+
*
|
|
340
|
+
* ```ts
|
|
341
|
+
* import { workflows } from "./workflows";
|
|
342
|
+
* tickWorkflows({ worker, appId: "grids", actions: createWorkflowActionPort(workflows) });
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
export const createWorkflowActionPort = (
|
|
346
|
+
module: DefinedWorkflowModule,
|
|
347
|
+
options: WorkflowActionPortOptions = {},
|
|
348
|
+
): WorkflowExecuteActionPort => {
|
|
349
|
+
const actions = module.actions;
|
|
350
|
+
return {
|
|
351
|
+
get: (name) => {
|
|
352
|
+
const action = actions[name];
|
|
353
|
+
if (!action) return undefined;
|
|
354
|
+
return {
|
|
355
|
+
execute: async (ctx, step) => {
|
|
356
|
+
const outcome = await runDeclaredAction(action, ctx, step, options);
|
|
357
|
+
if (outcome.state === "completed") applySaveAs(ctx, step, outcome.output);
|
|
358
|
+
return outcome;
|
|
359
|
+
},
|
|
360
|
+
// A restored outcome has to land in the same variable the first attempt
|
|
361
|
+
// would have written, or every step after it resolves to nothing.
|
|
362
|
+
restoreCompleted: (ctx, step, outcome) => applySaveAs(ctx, step, outcome.output),
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The port a dry run executes with, built from the same declarations.
|
|
370
|
+
*
|
|
371
|
+
* A dry run and a preflight are the same operation: run the plan with impure
|
|
372
|
+
* steps reporting what they *would* do. Because both come from one `plan` hook,
|
|
373
|
+
* what a dry run promises and what execution charges cannot drift apart.
|
|
374
|
+
*
|
|
375
|
+
* A pure action is simply run — it is deterministic and touches nothing, which
|
|
376
|
+
* is what makes its dry run exact rather than a description.
|
|
377
|
+
*/
|
|
378
|
+
export const createWorkflowDryRunPort = (module: DefinedWorkflowModule): WorkflowDryRunActionPort => {
|
|
379
|
+
const actions = module.actions;
|
|
380
|
+
return {
|
|
381
|
+
get: (name) => {
|
|
382
|
+
const action = actions[name];
|
|
383
|
+
if (!action) return undefined;
|
|
384
|
+
return {
|
|
385
|
+
restoreCompleted: (ctx, step, outcome) => applySaveAs(ctx, step, outcome.output),
|
|
386
|
+
plan: async (ctx: WorkflowDryRunActionContext, step: WorkflowActionStep) => {
|
|
387
|
+
const config = await resolveConfig(ctx, step);
|
|
388
|
+
const context = actionContext(ctx, step, workflowEffectKey(ctx.run.runId, ctx.step.key));
|
|
389
|
+
|
|
390
|
+
if (action.effect === "pure") {
|
|
391
|
+
const result = await action.run(context, config as never);
|
|
392
|
+
if (result.state === "waiting") {
|
|
393
|
+
return {
|
|
394
|
+
state: "indeterminate" as const,
|
|
395
|
+
reason: `dependency ${result.dependency.kind}:${result.dependency.key} is not satisfied`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (result.state !== "succeeded") {
|
|
399
|
+
return { state: "terminal" as const, status: "failed" as const, message: result.message, effects: [] };
|
|
400
|
+
}
|
|
401
|
+
applySaveAs(ctx, step, (result.output ?? undefined) as WorkflowJsonValue | undefined);
|
|
402
|
+
return { state: "planned" as const, output: (result.output ?? null) as WorkflowJsonValue, effects: [] };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!action.plan) return { state: "unsupported" as const, reason: `${name} cannot be planned` };
|
|
406
|
+
|
|
407
|
+
const planned = await action.plan(context, config as never);
|
|
408
|
+
applySaveAs(ctx, step, (planned.output ?? undefined) as WorkflowJsonValue | undefined);
|
|
409
|
+
return {
|
|
410
|
+
state: "planned" as const,
|
|
411
|
+
output: (planned.output ?? null) as WorkflowJsonValue,
|
|
412
|
+
effects: [
|
|
413
|
+
{ action: name, summary: planned.summary, ...(planned.consumes ? { consumes: planned.consumes } : {}) } as WorkflowJsonValue,
|
|
414
|
+
],
|
|
415
|
+
// An issue names the step it belongs to, so the dry-run view can point
|
|
416
|
+
// at what could not be determined rather than listing loose strings.
|
|
417
|
+
...(planned.issues?.length
|
|
418
|
+
? {
|
|
419
|
+
issues: planned.issues.map((reason) => ({
|
|
420
|
+
state: "indeterminate" as const,
|
|
421
|
+
reason,
|
|
422
|
+
step: {
|
|
423
|
+
key: ctx.step.key,
|
|
424
|
+
sourcePath: ctx.step.sourcePath,
|
|
425
|
+
iterationPath: ctx.step.iterationPath,
|
|
426
|
+
path: ctx.step.path,
|
|
427
|
+
kind: ctx.step.kind,
|
|
428
|
+
...(ctx.step.action ? { action: ctx.step.action } : {}),
|
|
429
|
+
},
|
|
430
|
+
})),
|
|
431
|
+
}
|
|
432
|
+
: {}),
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect budgets.
|
|
3
|
+
*
|
|
4
|
+
* A dry run and a preflight are the same operation: execute the plan with
|
|
5
|
+
* impure steps reporting what they *would* do instead of doing it. Preflight is
|
|
6
|
+
* that, with the reported effects counted against a cap.
|
|
7
|
+
*
|
|
8
|
+
* Grids had no cap at all — a workflow with an email action inside a loop over
|
|
9
|
+
* ten thousand records was bounded only by `maxLoopItems`, which limits
|
|
10
|
+
* iterations, not messages. Mail had budgets but checked them once, before
|
|
11
|
+
* execution, so a run could be approved for one set of effects and go on to
|
|
12
|
+
* perform a different one.
|
|
13
|
+
*
|
|
14
|
+
* Hence the rule here: the budget is charged at the moment of the effect, not
|
|
15
|
+
* only ahead of it. That makes the preflight/execution divergence harmless
|
|
16
|
+
* rather than something a hash has to detect — whatever the plan turns out to
|
|
17
|
+
* do, it still cannot exceed what was approved.
|
|
18
|
+
*/
|
|
19
|
+
import { type SQL, sql } from "bun";
|
|
20
|
+
import { withTransaction } from "./transaction";
|
|
21
|
+
|
|
22
|
+
/** Caps keyed by dimension — `{ emails: 100, httpRequests: 50 }`. */
|
|
23
|
+
export type WorkflowEffectBudget = Record<string, number>;
|
|
24
|
+
|
|
25
|
+
/** What one step reports consuming, matching an action's `plan` hook. */
|
|
26
|
+
export type WorkflowEffectCharge = Record<string, number>;
|
|
27
|
+
|
|
28
|
+
export type WorkflowBudgetOutcome =
|
|
29
|
+
| { state: "ok"; used: WorkflowEffectBudget }
|
|
30
|
+
| { state: "exceeded"; dimension: string; limit: number; used: number; requested: number };
|
|
31
|
+
|
|
32
|
+
/** The error a budget rejection settles a run with — structured, so the UI can badge it. */
|
|
33
|
+
export type WorkflowBudgetError = {
|
|
34
|
+
kind: "budget_exceeded";
|
|
35
|
+
dimension: string;
|
|
36
|
+
limit: number;
|
|
37
|
+
used: number;
|
|
38
|
+
requested: number;
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Refuses values JSON accepts but arithmetic cannot safely enforce. */
|
|
43
|
+
export const validateWorkflowEffectBudget = (value: Record<string, number>, label = "workflow effect budget"): void => {
|
|
44
|
+
for (const [dimension, amount] of Object.entries(value)) {
|
|
45
|
+
if (!dimension || !Number.isFinite(amount) || amount < 0) {
|
|
46
|
+
throw new TypeError(`${label} ${JSON.stringify(dimension)} must be a finite non-negative number`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const budgetError = (outcome: Extract<WorkflowBudgetOutcome, { state: "exceeded" }>): WorkflowBudgetError => ({
|
|
52
|
+
kind: "budget_exceeded",
|
|
53
|
+
dimension: outcome.dimension,
|
|
54
|
+
limit: outcome.limit,
|
|
55
|
+
used: outcome.used,
|
|
56
|
+
requested: outcome.requested,
|
|
57
|
+
message: `effect budget for ${outcome.dimension} exhausted: ${outcome.used} of ${outcome.limit} used, ${outcome.requested} more requested`,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Adds up what a dry run said it would do, so preflight can compare it to the cap. */
|
|
61
|
+
export const totalPlannedEffects = (planned: readonly { consumes?: WorkflowEffectCharge }[]): WorkflowEffectBudget => {
|
|
62
|
+
const total: WorkflowEffectBudget = {};
|
|
63
|
+
for (const effect of planned) {
|
|
64
|
+
validateWorkflowEffectBudget(effect.consumes ?? {}, "workflow effect charge");
|
|
65
|
+
for (const [dimension, amount] of Object.entries(effect.consumes ?? {})) {
|
|
66
|
+
total[dimension] = (total[dimension] ?? 0) + amount;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return total;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Whether a planned total fits, without touching the database. Used by preflight. */
|
|
73
|
+
export const checkEffectBudget = (budget: WorkflowEffectBudget, planned: WorkflowEffectBudget): WorkflowBudgetOutcome => {
|
|
74
|
+
validateWorkflowEffectBudget(budget);
|
|
75
|
+
validateWorkflowEffectBudget(planned, "planned workflow effect charge");
|
|
76
|
+
for (const [dimension, requested] of Object.entries(planned)) {
|
|
77
|
+
const limit = budget[dimension];
|
|
78
|
+
if (limit !== undefined && requested > limit) {
|
|
79
|
+
return { state: "exceeded", dimension, limit, used: 0, requested };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { state: "ok", used: planned };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Charges a run's budget for an effect it is about to perform.
|
|
87
|
+
*
|
|
88
|
+
* One statement, so two workers on child runs of the same parent cannot both
|
|
89
|
+
* read "99 of 100 used" and both proceed. A dimension the budget does not
|
|
90
|
+
* mention is uncapped and still counted, because the count is what the operator
|
|
91
|
+
* looks at afterwards.
|
|
92
|
+
*/
|
|
93
|
+
export const chargeWorkflowEffectBudget = async (
|
|
94
|
+
runId: string,
|
|
95
|
+
charge: WorkflowEffectCharge,
|
|
96
|
+
options: { db?: SQL } = {},
|
|
97
|
+
): Promise<WorkflowBudgetOutcome> => {
|
|
98
|
+
validateWorkflowEffectBudget(charge, "workflow effect charge");
|
|
99
|
+
if (Object.keys(charge).length === 0) return { state: "ok", used: {} };
|
|
100
|
+
return withTransaction(options.db, async (tx) => {
|
|
101
|
+
const [row] = await tx<{ effects_used: WorkflowEffectBudget; effect_budget: WorkflowEffectBudget }[]>`
|
|
102
|
+
SELECT r.effects_used, v.effect_budget
|
|
103
|
+
FROM workflows.run AS r
|
|
104
|
+
JOIN workflows.version AS v ON v.id = r.workflow_version_id
|
|
105
|
+
WHERE r.id = ${runId}::uuid
|
|
106
|
+
FOR UPDATE OF r
|
|
107
|
+
`;
|
|
108
|
+
if (!row) throw new Error(`workflow run ${runId} does not exist`);
|
|
109
|
+
validateWorkflowEffectBudget(row.effect_budget);
|
|
110
|
+
validateWorkflowEffectBudget(row.effects_used, "workflow effects used");
|
|
111
|
+
|
|
112
|
+
const next: WorkflowEffectBudget = { ...row.effects_used };
|
|
113
|
+
for (const [dimension, amount] of Object.entries(charge)) {
|
|
114
|
+
const used = next[dimension] ?? 0;
|
|
115
|
+
const limit = row.effect_budget[dimension];
|
|
116
|
+
if (limit !== undefined && used + amount > limit) {
|
|
117
|
+
return { state: "exceeded", dimension, limit, used, requested: amount };
|
|
118
|
+
}
|
|
119
|
+
next[dimension] = used + amount;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await tx`UPDATE workflows.run SET effects_used = ${next}, updated_at = now() WHERE id = ${runId}::uuid`;
|
|
123
|
+
return { state: "ok", used: next };
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Charges the *parent* when a run is one of many children.
|
|
129
|
+
*
|
|
130
|
+
* A budget that only applied per child would be no budget at all: fanning out
|
|
131
|
+
* over ten thousand records would authorise ten thousand times the cap. The
|
|
132
|
+
* root run of a fan-out owns the allowance.
|
|
133
|
+
*/
|
|
134
|
+
export const budgetRootRunId = async (runId: string, options: { db?: SQL } = {}): Promise<string> => {
|
|
135
|
+
const db = options.db ?? sql;
|
|
136
|
+
const [row] = await db<{ root: string }[]>`
|
|
137
|
+
WITH RECURSIVE ancestry AS (
|
|
138
|
+
SELECT id, parent_run_id FROM workflows.run WHERE id = ${runId}::uuid
|
|
139
|
+
UNION ALL
|
|
140
|
+
SELECT r.id, r.parent_run_id FROM workflows.run AS r JOIN ancestry AS a ON r.id = a.parent_run_id
|
|
141
|
+
)
|
|
142
|
+
SELECT id AS root FROM ancestry WHERE parent_run_id IS NULL
|
|
143
|
+
`;
|
|
144
|
+
return row?.root ?? runId;
|
|
145
|
+
};
|