@checkstack/automation-frontend 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,149 @@
1
1
  # @checkstack/automation-frontend
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9dcc848: Plugin-owned AI tools: every domain plugin contributes its own AI tools (chat assistant + automation AI action), and `ai-backend` is platform-only.
8
+
9
+ Every plugin-specific AI tool is owned by the plugin whose domain it acts on, registered via that plugin's own `aiToolExtensionPoint` / `aiToolProjectionExtensionPoint` from its init - the same path an external plugin author uses. `ai-backend` no longer imports or depends on any capability plugin's `*-common`; the dependency direction is strictly plugin -> ai-platform. Pure helpers (`computeFieldDiff`, capability-summary, `ScriptContextKind`) live in `@checkstack/ai-common`.
10
+
11
+ Tools shipped:
12
+
13
+ - Health checks and automations: full CRUD - `healthcheck.propose` / `automation.propose` and `*.update` (`mutate`, deep-validated) and `*.delete` (`destructive`, always confirm-gated). `healthcheck.propose`'s dry-run calls the new deep `validateConfiguration` so propose-time validation matches apply-time. Assertions are validated against the collector's result schema and the canonical operator vocabulary. Capability-catalog tools (`ai.listCapabilities`, `ai.getCapabilitySchema`), script context tools (`ai.getScriptContext`, `ai.testScript`), and notify-subscriber tools (`healthcheck.notifySystemSubscribers` / `...GroupSubscribers`).
14
+ - Catalog: `catalog.createSystem` / `updateSystem` / `createGroup` / `updateGroup` (`mutate`), `catalog.deleteSystem` / `deleteGroup` (`destructive`), membership tools (`mutate`), plus `catalog.listSystems` / `listGroups` read projections.
15
+ - Incident: `incident.create` / `update` / `addUpdate` / `resolve` / `addLink` (`mutate`), `incident.delete` / `removeLink` (`destructive`), and `incident.get` / `incident.list` read projections.
16
+ - Maintenance: `maintenance.create` / `update` / `addUpdate` / `close` / `addLink` (`mutate`), `maintenance.delete` / `removeLink` (`destructive`), and `maintenance.list` / `get` read projections.
17
+ - Read projections for SLO (`slo.listObjectives`), dependency (`dependency.list`), incident (`incident.list`), healthcheck (`healthcheck.status`), and anomaly (`anomaly.explain`), each gated by the source procedure's own access rule and routed as the principal.
18
+ - Documentation grounding: `ai.searchDocs` / `ai.getDoc` over a build-time bundled docs index (BM25-ish ranking), so the assistant grounds how-to answers in Checkstack's own docs offline.
19
+ - URL introspection: `ai.probeUrl`, an SSRF-guarded read tool the assistant uses to inspect a real endpoint before drafting a health check. Update tools compute a before -> after field diff rendered on the confirm card (approve mode) or an "Applied" card (auto mode), so a change is never silent.
20
+
21
+ `ai_analyze` automation action (automation-backend, with an editor connection picker + audited tool calls): runs a bounded AI agent on the run context as the automation's `runAs` service account, so it can never exceed that identity's permissions; destructive tools are never offered; mutating tools auto-apply through the service account's client. Produces an `automation.analysis` artifact downstream actions can branch on. The agent loop is exposed as a headless `aiAgentRunnerRef` service so automation-backend can drive it without depending on ai-backend.
22
+
23
+ `notification.notifyForSubscription` is now callable by user / application principals holding `notification.send` (previously service-only). Every tool routes through the user-scoped client, so handler-side authorization is enforced exactly as a direct UI/RPC action; the resolver gate plus the propose/apply re-check at propose AND apply are the additional authority. A systemic authz regression test asserts every registered tool falls into exactly one safe authorization category.
24
+
25
+ A new `ai_transport` enum value `automation` records the AI action's tool calls in the `ai_tool_calls` audit log. No new durable state beyond that; each tool is a thin, deterministic wrapper over an existing RPC, so every pod behaves identically.
26
+
27
+ This is a beta minor.
28
+
29
+ - 9dcc848: Automations now run as a configured service account, removing implicit god-mode from the dispatch path.
30
+
31
+ BREAKING: every automation must declare a `runAs` application (service account). Previously every automation action ran as the trusted service client, bypassing all access-rule, per-resource, and team-scope checks - so an automation could touch any team's data. Now each automation runs as a bounded `application` principal, and every data-access call an action makes is authorized exactly as that identity. An automation with no `runAs` fails to run with a clear error rather than falling back to the trusted client; legacy automations must be assigned a service account before they run again.
32
+
33
+ What changed:
34
+
35
+ - New top-level field `runAs` on automations (a `run_as_application_id` column + create/update inputs; `AutomationSchema.runAs`). Required on create; GitOps sets it via the `run-as` metadata label.
36
+ - A new `coreServices.rpcClientAs(applicationId)` mints a short-lived, backend-signed app-principal token; the auth service resolves it LIVE to an `application` principal (reusing `enrichApplicationPrincipal`), so it flows through full `autoAuthMiddleware` enforcement. The dispatch engine threads this client into every action's `execute` as the required `context.rpcClient`.
37
+ - Bind authority (anti-escalation): a user may only bind an application whose access rules are a subset of their own (`isApplicationBindable`); `getBindableApplications` lists only bindable apps, and the create/update handlers enforce the check.
38
+ - `notification.sendTransactional` moves from service-only to access-gated (`notification.send`, a new access rule), so an automation's `runAs` can call the built-in `notify_user` / `notification.send` actions; trusted services still bypass via short-circuit.
39
+ - A "Run as (Service Account)" picker in the automation editor, populated from `getBindableApplications` (server-side filtered to bindable apps), seeding from the loaded `runAs` on edit and passing it into create + update. First-class teaching UX: an inline info banner, a blocked Save with an inline hint until one is chosen, and an empty state linking to the Applications admin + docs when none are bindable.
40
+
41
+ State and scale: `runAs` resolution is a pure read over shared tables; the app-principal token is self-contained and verified statelessly, so the per-run client is correct under horizontal scale.
42
+
43
+ This is a beta minor.
44
+
45
+ - 9dcc848: Cut initial-load JS: lazy plugin contributions, a hardened lazy-by-default contribution contract, on-demand Monaco, and a lighter icon/chart load.
46
+
47
+ - Lazy plugin route pages: each plugin's route `element` references a `React.lazy`-wrapped page rendered inside a shared `<Suspense>` boundary. Plugins still register synchronously, so nav, slots, commands, API factories, and `foreignSignals` are available on first paint. This moves ~37 route-page chunks (~600 KB) out of the entry; the entry chunk drops from ~2.4 MB to ~190 KB. Auth flow pages stay eager. The `@checkstack/scripts` scaffold template generates lazy route pages too.
48
+ - Hardened contribution contract (BREAKING, frontend plugin contract): plugins declare contributions lazily and let the framework own code-splitting, Suspense, and per-plugin error isolation. Routes use `load: () => import("./Page").then((m) => ({ default: m.Page }))` instead of `element: <Page />` (`element` is still accepted for the rare page that must paint without a chunk fetch; provide exactly one). Slot extensions accept either an eager `component` or a lazy `load`; new `getLazyContribution` + `ExtensionComponent` exports from `@checkstack/frontend-api` render either kind. This also fixes runtime-installed plugins: `ExtensionSlot` subscribes to the plugin registry, and the API registry rebuilds when the plugin set changes (`getPlugins()` returns an immutable snapshot via `useSyncExternalStore`). A per-plugin error boundary contains a bad contribution.
49
+ - On-demand Monaco: the `@checkstack/ui` barrel no longer pulls the `@codingame/*` / `monaco-languageclient` stack into the initial load. `CodeEditor` lazy-loads its Monaco-backed editor behind `React.lazy` + Suspense, `validateTypeScriptSources` imports the editor API via in-body `await import(...)`, and the "vscode services ready" signal moved to a Monaco-free module. The ~10 MB editor body loads only when a `CodeEditor` mounts. A `react-vendor` `manualChunks` split was added for stable vendor caching.
50
+ - lucide-react 1.x + lighter icons/charts (BREAKING for icon consumers): lucide-react unified from three drifting ranges to `^1.17.0`. lucide v1 removed brand icons, so the GitHub/GitLab marks are vendored in `@checkstack/ui` (`GithubIcon`, `GitlabIcon`, `brandIcons`); a new `IconName` type (`LucideIconName | BrandIconName`) in `@checkstack/common` is canonical, accepted by `AuthStrategy.icon` and the card components, so data-driven brand names keep working. `DynamicIcon` no longer eagerly imports lucide's ~1600-icon map (~1 MB) - it lives in a `React.lazy` `iconRegistry` chunk fetched on first data-driven render, while statically named-imported icons tree-shake normally. The recharts-backed health-check charts (~300 KB) and the `HealthCheckSystemOverview` drawer leave the initial load.
51
+
52
+ BREAKING CHANGES:
53
+
54
+ - Frontend plugin contract: routes/slot contributions are lazy-by-default (`load` instead of `element`/eager elements) as described above.
55
+ - Any external consumer importing a brand icon from `lucide-react` (e.g. `import { Github } from "lucide-react"`) must switch to the vendored `@checkstack/ui` brand icons or a custom SVG.
56
+
57
+ This is a beta minor.
58
+
59
+ - 9dcc848: Add the auto-generated, version-pinned `@checkstack/sdk` package + codegen, and serve its types live to the in-app editor.
60
+
61
+ - A new committed workspace package `@checkstack/sdk`, generated from the platform's source of truth by `scripts/generate-sdk.ts` (`generate:sdk` / `generate:sdk:check`): a fully-typed oRPC client (`createCheckstackClient`) over the REST surface with one `InferClient` per plugin contract, real script-authoring helpers (`@checkstack/sdk/healthcheck`, `@checkstack/sdk/integration`) whose runtime body is the same identity function the in-app runner injects, per-subpath `.d.ts` under the package `exports` map, and an editor-only ambient bundle. A `generate:sdk:check` CI guard fails when the committed SDK files drift from a fresh generation. The `@checkstack/sdk` version is stamped from `@checkstack/release` and MUST NOT appear in a changeset (a guard enforces this); the `@checkstack/release` bump here advances the release version so the generated SDK can be published later. The generated client also normalizes its base URL without a backtracking-prone regex, closing a CodeQL `js/polynomial-redos` finding.
62
+ - Live editor type injection: a new version-keyed route `GET /api/script-packages/sdk-types/:releaseVersion` (raw handler in `@checkstack/script-packages-backend`) serves the generated SDK editor bundle with `Cache-Control: private, max-age=1y, immutable`; the pure path-build/parse module lives in `@checkstack/script-packages-common`, shared by backend and frontend. A mismatched version returns `409` so the editor refetches and never serves stale types after an upgrade. The frontend `useSdkTypeInjection` hook fetches the bundle once per session and mounts it into Monaco via `addExtraLib`. Schema-narrowed `context.config` / `context.event.payload` editor types stay local; the package-resolving module declarations come from the one published `@checkstack/sdk` source.
63
+
64
+ BREAKING CHANGES: the script-authoring import surface moves from the bare `@checkstack/healthcheck` / `@checkstack/integration` virtual modules to the `@checkstack/sdk/healthcheck` / `@checkstack/sdk/integration` subpaths of the published `@checkstack/sdk` package. The old bare-name imports no longer resolve (an old import now errors in the editor, surfacing the migration). Existing scripts must update the module specifier:
65
+
66
+ - import { defineHealthCheck } from "@checkstack/healthcheck";
67
+ + import { defineHealthCheck } from "@checkstack/sdk/healthcheck";
68
+
69
+ - import { defineIntegration } from "@checkstack/integration";
70
+ + import { defineIntegration } from "@checkstack/sdk/integration";
71
+
72
+ The helper names and their runtime behaviour are unchanged - only the module specifier moves. The global (no-import) helper form continues to work unchanged.
73
+
74
+ This is a beta minor.
75
+
76
+ - 9dcc848: Align workspace dependency versions and migrate React Router to v7.
77
+
78
+ BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
79
+
80
+ Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
81
+
82
+ Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
83
+
84
+ Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
85
+
86
+ ### Patch Changes
87
+
88
+ - 9dcc848: Assorted bug fixes and small hardening across the platform.
89
+
90
+ - announcement-backend: `updateAnnouncement` now invalidates the active-announcements and admin-list caches (it was missing the `invalidateAllActive` / `invalidateListAll` calls), so an edited announcement no longer stays stale up to the 45s TTL.
91
+ - anomaly-backend: anomaly/drift state transitions (confirmations, recoveries, self-resolutions) now log at `debug` instead of info/warn - they are already surfaced via the `ANOMALY_STATE_CHANGED` signal, so logging them louder just added noise; genuine failure paths stay `warn`.
92
+ - backend: the `/api/:pluginId/*` dispatcher now populates `requestHeaders` on the per-request RPC context, so a handler that re-enters the router as the originating user (e.g. an AI tool's user-scoped client) can forward the caller's session cookie / bearer - previously the loopback failed with "Authentication required". Guarded by a real end-to-end integration test. The HTTP server idle timeout is also raised (default 255s, configurable via `CHECKSTACK_SERVER_IDLE_TIMEOUT_SECONDS`, clamped 0-255, reset on each streamed chunk) so long AI chat SSE turns are not severed mid-stream.
93
+ - backend: a request for an unknown plugin id (`/api/<unknown>/...`) now returns `404 Not Found` instead of `500` (and logs at warn, not error, since it is a client request) - an unknown _procedure_ on a known plugin already 404'd. The in-app docs namespace `/checkstack/*` now serves Starlight's own `404.html` with a real 404 status for a missing doc, instead of falling through to the SPA catch-all and 200-ing the app shell. Both guarded by tests.
94
+ - automation-common: remove polynomial-time backtracking from `toShellEnvKey`'s underscore-trim (CodeQL `js/polynomial-redos`); a negative look-behind anchors the trailing run, keeping the trim linear.
95
+ - common + script-packages-common: the pure transport-safe sandbox-policy schema (`sandboxPolicySchema` and its sub-schemas + inferred types) moved to `@checkstack/common` (the neutral base), removing two inverted deps that existed only to reach the shape; `@checkstack/backend-api` continues to re-export it. The schema is no longer exported from `@checkstack/script-packages-common`. Pure refactor, no behavior change.
96
+ - catalog-backend: reject duplicate system names (a `CONFLICT` on create/rename, enforced by a pre-write check AND a new DB unique index on `systems.name`, migration 0004 which first resolves pre-existing duplicates by suffixing).
97
+ - catalog-frontend: detail-page cleanups (use `<NotFound />` not `<AccessDenied />` on the not-found branch, a readable key/value metadata list via `normalizeMetadata`, runtime locale via `formatDate`); and stop the browse view re-rendering on every health report (adopt a new statuses report only when a value actually changed, via `healthStatusesEqual`, so rows stay stable and interactive).
98
+ - healthcheck-backend: fix the daily-rollup retention step failing with an `ON CONFLICT` mismatch (SQLSTATE 42P10) after `environmentId` joined the `health_check_aggregates` unique constraint - the rollup now groups by (day, environmentId, sourceId) and uses a single exported conflict-target constant (`DAILY_AGGREGATE_CONFLICT_TARGET`) kept in lock-step with the schema by a unit test.
99
+ - automation-frontend: the service-account picker's "Learn more" links are now absolute URLs to the deployed Astro docs site (they 404ed as in-app relative paths). The Monaco script editor double-init crash is fixed (serialized cold init, a guarded `monacoGuard` accessor, theme/type effects gated on `apiReady`).
100
+ - auth-frontend: bound the desktop user-menu popover height (`max-h-[var(--radix-popover-content-available-height)]` + `overflow-y-auto`) so it no longer clips on short viewports, and fold the standalone `Account > Profile` item into a focusable name/email header (`profileHref` on `UserMenu`); the now-empty `Account` group no longer renders.
101
+ - satellite-frontend: picked up via the sidebar-nav migration (account-only user menu).
102
+
103
+ (Related UI fixes - the Monaco editor following the app theme, the `DynamicOptionsField` no-flash fix, the shared `Spinner`, GFM tables, and the user-menu popover bound - land their `@checkstack/ui` bump in the UI/perf changesets where `@checkstack/ui` is already minored.)
104
+
105
+ This is a beta patch.
106
+
107
+ - 9dcc848: Move primary navigation into a left sidebar, and serve the user guide in-app.
108
+
109
+ Feature navigation (a ~20-item user-menu dropdown) now lives in a persistent left sidebar (a slide-over drawer on mobile), grouped by section with the active route highlighted; the user menu keeps only account actions. A route opts into the sidebar with new `nav` metadata (`{ group, icon, label?, order?, accessRule? }`) on its registration, co-located with path + access + title. The sidebar filters entries with the same access check as page guards. `@checkstack/common` gains `isAccessRuleSatisfied` and a centralized set of in-app doc slugs (`APP_DOC_SLUGS` + `docsPath`, with a test asserting each resolves to a real docs page); `@checkstack/auth-frontend` exports `useAccessRules`.
110
+
111
+ The backend now serves the Astro Starlight docs build same-origin at `/checkstack/*` (the same artifact deployed to GitHub Pages), so the user guide is available inside the app including for self-hosted / air-gapped installs (served verbatim, no rebuild, no link rewriting; from `CHECKSTACK_DOCS_DIST`, before the SPA catch-all, degrading gracefully when absent; the Docker image builds and ships `docs/dist`; Vite proxies `/checkstack` in dev). The "Docs" link is a shell-owned external sidebar entry under the Documentation group (book icon), opening `/checkstack/user-guide/` in a new tab; the group renders even when no plugin route contributes to it.
112
+
113
+ BREAKING (plugin authors): `UserMenuItemsSlot` is no longer the way to add navigation - registering a top user-menu item no longer surfaces it anywhere. Add `nav` to the page's route instead. `UserMenuItemsBottomSlot` (account items) is unchanged. All bundled plugins have been migrated.
114
+
115
+ This is a beta minor.
116
+
117
+ - Updated dependencies [9dcc848]
118
+ - Updated dependencies [9dcc848]
119
+ - Updated dependencies [9dcc848]
120
+ - Updated dependencies [9dcc848]
121
+ - Updated dependencies [9dcc848]
122
+ - Updated dependencies [9dcc848]
123
+ - Updated dependencies [9dcc848]
124
+ - Updated dependencies [9dcc848]
125
+ - Updated dependencies [9dcc848]
126
+ - Updated dependencies [9dcc848]
127
+ - Updated dependencies [9dcc848]
128
+ - Updated dependencies [9dcc848]
129
+ - Updated dependencies [9dcc848]
130
+ - Updated dependencies [9dcc848]
131
+ - Updated dependencies [9dcc848]
132
+ - Updated dependencies [9dcc848]
133
+ - Updated dependencies [9dcc848]
134
+ - @checkstack/ui@1.13.0
135
+ - @checkstack/auth-common@0.8.0
136
+ - @checkstack/automation-common@0.4.0
137
+ - @checkstack/catalog-common@2.3.0
138
+ - @checkstack/common@0.13.0
139
+ - @checkstack/template-engine@0.4.0
140
+ - @checkstack/frontend-api@0.7.0
141
+ - @checkstack/gitops-frontend@0.5.0
142
+ - @checkstack/script-packages-frontend@0.3.0
143
+ - @checkstack/secrets-frontend@0.2.0
144
+ - @checkstack/integration-common@0.7.0
145
+ - @checkstack/signal-frontend@0.2.0
146
+
3
147
  ## 0.3.0
4
148
 
5
149
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/automation-frontend",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,24 +13,25 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/automation-common": "0.2.0",
16
+ "@checkstack/auth-common": "0.7.2",
17
+ "@checkstack/automation-common": "0.3.0",
17
18
  "@checkstack/catalog-common": "2.2.3",
18
19
  "@checkstack/common": "0.12.0",
19
20
  "@checkstack/frontend-api": "0.6.0",
20
- "@checkstack/gitops-frontend": "0.4.6",
21
+ "@checkstack/gitops-frontend": "0.4.7",
21
22
  "@checkstack/integration-common": "0.6.0",
22
- "@checkstack/script-packages-frontend": "0.1.0",
23
- "@checkstack/secrets-frontend": "0.0.1",
23
+ "@checkstack/script-packages-frontend": "0.2.0",
24
+ "@checkstack/secrets-frontend": "0.1.0",
24
25
  "@checkstack/signal-frontend": "0.1.5",
25
- "@checkstack/template-engine": "0.2.0",
26
- "@checkstack/ui": "1.11.0",
26
+ "@checkstack/template-engine": "0.3.0",
27
+ "@checkstack/ui": "1.12.0",
27
28
  "@dnd-kit/core": "^6.3.1",
28
29
  "@dnd-kit/sortable": "^8.0.0",
29
30
  "@dnd-kit/utilities": "^3.2.2",
30
- "date-fns": "^4.1.0",
31
- "lucide-react": "^0.344.0",
32
- "react": "^18.2.0",
33
- "react-router-dom": "^6.22.0",
31
+ "date-fns": "^4.4.0",
32
+ "lucide-react": "^1.17.0",
33
+ "react": "^18.3.1",
34
+ "react-router-dom": "^7.16.0",
34
35
  "yaml": "^2.6.1"
35
36
  },
36
37
  "devDependencies": {
@@ -0,0 +1,170 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { ExternalLink, KeyRound, ShieldCheck } from "lucide-react";
4
+ import { usePluginClient } from "@checkstack/frontend-api";
5
+ import { AuthApi, authRoutes } from "@checkstack/auth-common";
6
+ import { resolveRoute, APP_DOC_SLUGS, docsPath } from "@checkstack/common";
7
+ import {
8
+ Label,
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ InfoBanner,
15
+ InfoBannerIcon,
16
+ InfoBannerContent,
17
+ InfoBannerDescription,
18
+ EmptyState,
19
+ Button,
20
+ } from "@checkstack/ui";
21
+
22
+ /**
23
+ * Deep-links into the in-app user guide. The backend serves the Astro Starlight
24
+ * docs build same-origin at `/checkstack/*`, so these are same-origin paths
25
+ * opened in a new tab. They are static doc pages, not app routes - hence a plain
26
+ * anchor (below), never a react-router `<Link>`. Slugs are centralised in
27
+ * `APP_DOC_SLUGS` and guarded by `docs-links.test.ts` against renames.
28
+ */
29
+ const DOCS_TEAMS_AND_ACCESS = docsPath(APP_DOC_SLUGS.teamsAndAccess);
30
+ const DOCS_SERVICE_ACCOUNTS = docsPath(APP_DOC_SLUGS.apiKeys);
31
+
32
+ /** Route to the Applications admin tab in auth settings. */
33
+ const APPLICATIONS_ADMIN_ROUTE = `${resolveRoute(
34
+ authRoutes.routes.settings,
35
+ )}?tab=applications`;
36
+
37
+ export interface RunAsServiceAccountPickerProps {
38
+ /** Selected application id, or empty string when nothing is chosen yet. */
39
+ value: string;
40
+ /** Called with the chosen application id. */
41
+ onValueChange: (next: string) => void;
42
+ /** When false, the picker is read-only (matches the page's `canManage`). */
43
+ disabled?: boolean;
44
+ /** Show the inline "required" validation hint. */
45
+ showError?: boolean;
46
+ }
47
+
48
+ /**
49
+ * "Run as (Service Account)" picker for the automation editor.
50
+ *
51
+ * Populated from the auth plugin's `getBindableApplications` query, which
52
+ * server-side filters to exactly the applications THIS user is allowed to
53
+ * bind (their access rules are a subset of the caller's). The automation
54
+ * then runs with the chosen application's permissions, so picking one is
55
+ * required.
56
+ *
57
+ * Teaching UX:
58
+ * - An info banner explains what a service account is and why it is
59
+ * required.
60
+ * - When no applications are bindable, an `EmptyState` points the user to
61
+ * the Applications admin and the docs instead of leaving them stuck.
62
+ */
63
+ export const RunAsServiceAccountPicker: React.FC<
64
+ RunAsServiceAccountPickerProps
65
+ > = ({ value, onValueChange, disabled, showError }) => {
66
+ const authClient = usePluginClient(AuthApi);
67
+ const { data: applications = [], isLoading } =
68
+ authClient.getBindableApplications.useQuery({});
69
+
70
+ const hasNoApplications = !isLoading && applications.length === 0;
71
+
72
+ return (
73
+ <div className="space-y-2">
74
+ <div className="space-y-1">
75
+ <Label htmlFor="runAs">Run as (Service Account)</Label>
76
+ <Select
77
+ value={value || undefined}
78
+ onValueChange={onValueChange}
79
+ disabled={disabled || hasNoApplications || isLoading}
80
+ >
81
+ <SelectTrigger
82
+ id="runAs"
83
+ aria-invalid={showError ? true : undefined}
84
+ className={showError ? "border-destructive" : undefined}
85
+ >
86
+ <SelectValue
87
+ placeholder={
88
+ isLoading ? "Loading service accounts..." : "Select a service account"
89
+ }
90
+ />
91
+ </SelectTrigger>
92
+ <SelectContent>
93
+ {applications.map((app) => (
94
+ <SelectItem key={app.id} value={app.id}>
95
+ <span className="flex flex-col text-left">
96
+ <span className="truncate">{app.name}</span>
97
+ {app.description ? (
98
+ <span className="text-xs text-muted-foreground truncate">
99
+ {app.description}
100
+ </span>
101
+ ) : null}
102
+ </span>
103
+ </SelectItem>
104
+ ))}
105
+ </SelectContent>
106
+ </Select>
107
+ {showError && !hasNoApplications && (
108
+ <p className="text-xs text-destructive">
109
+ A service account is required.
110
+ </p>
111
+ )}
112
+ </div>
113
+
114
+ {hasNoApplications ? (
115
+ <EmptyState
116
+ icon={<KeyRound className="h-7 w-7" />}
117
+ title="No service accounts available"
118
+ description={
119
+ <>
120
+ An automation runs with a service account's permissions, but you
121
+ do not have any to bind yet. Create or get access to one in the
122
+ Applications admin, then come back here.
123
+ </>
124
+ }
125
+ actions={
126
+ <>
127
+ <Link to={APPLICATIONS_ADMIN_ROUTE}>
128
+ <Button size="sm" variant="outline">
129
+ Open Applications admin
130
+ </Button>
131
+ </Link>
132
+ <a
133
+ href={DOCS_SERVICE_ACCOUNTS}
134
+ target="_blank"
135
+ rel="noreferrer"
136
+ >
137
+ <Button size="sm" variant="ghost">
138
+ Learn more
139
+ <ExternalLink className="ml-1 h-3.5 w-3.5" />
140
+ </Button>
141
+ </a>
142
+ </>
143
+ }
144
+ />
145
+ ) : (
146
+ <InfoBanner variant="info">
147
+ <InfoBannerIcon>
148
+ <ShieldCheck className="h-4 w-4" />
149
+ </InfoBannerIcon>
150
+ <InfoBannerContent>
151
+ <InfoBannerDescription>
152
+ A service account is an application the automation runs as. It
153
+ executes with that account's permissions (assigned via roles), so
154
+ picking one is required.{" "}
155
+ <a
156
+ href={DOCS_TEAMS_AND_ACCESS}
157
+ target="_blank"
158
+ rel="noreferrer"
159
+ className="inline-flex items-center gap-0.5 underline underline-offset-2 hover:no-underline"
160
+ >
161
+ Learn more
162
+ <ExternalLink className="h-3 w-3" />
163
+ </a>
164
+ </InfoBannerDescription>
165
+ </InfoBannerContent>
166
+ </InfoBanner>
167
+ )}
168
+ </div>
169
+ );
170
+ };
@@ -47,6 +47,10 @@ export const ScriptServicesBooter: React.FC = () => {
47
47
  minHeight="1px"
48
48
  allowPopout={false}
49
49
  readOnly
50
+ // Never let this hidden editor be the sole cold-init driver: a hidden
51
+ // editor's monaco-vscode init may never complete. It defers to a
52
+ // visible editor, then attaches once services are up.
53
+ deferInit
50
54
  />
51
55
  </div>
52
56
  );
@@ -34,7 +34,10 @@ import { ItemPicker } from "./ItemPicker";
34
34
  import { ConditionEditor } from "./ConditionEditor";
35
35
  import { useConnectionOptionResolvers } from "./useConnectionOptionResolvers";
36
36
  import { automationScriptTestRenderer } from "./ScriptTestRenderer";
37
- import { useScriptPackageTypeAcquisition } from "@checkstack/script-packages-frontend";
37
+ import {
38
+ useScriptPackageTypeAcquisition,
39
+ useSdkTypeInjection,
40
+ } from "@checkstack/script-packages-frontend";
38
41
  import { useSecretNames } from "@checkstack/secrets-frontend";
39
42
  import { generateSecretEnvTypes, secretEnvEnvNames } from "../script-context";
40
43
 
@@ -85,6 +88,8 @@ export const ProviderActionBody: React.FC<{
85
88
  // name itself) before any module is registered.
86
89
  const { acquireTypes, acquireResetKey, importablePackages } =
87
90
  useScriptPackageTypeAcquisition();
91
+ // SDK editor types so `@checkstack/sdk/integration` imports resolve.
92
+ const { sdkTypes, sdkTypesResetKey } = useSdkTypeInjection();
88
93
  // Secret names (never values) for the secret -> env mapping editor's
89
94
  // ${{ secrets.* }} autocomplete.
90
95
  const { secretNames } = useSecretNames();
@@ -179,6 +184,8 @@ export const ProviderActionBody: React.FC<{
179
184
  secretNames={secretNames}
180
185
  acquireTypes={acquireTypes}
181
186
  acquireResetKey={acquireResetKey}
187
+ sdkTypes={sdkTypes}
188
+ sdkTypesResetKey={sdkTypesResetKey}
182
189
  importablePackages={importablePackages}
183
190
  />
184
191
  </div>
package/src/index.tsx CHANGED
@@ -1,19 +1,10 @@
1
- import {
2
- createFrontendPlugin,
3
- createSlotExtension,
4
- UserMenuItemsSlot,
5
- } from "@checkstack/frontend-api";
1
+ import { createFrontendPlugin } from "@checkstack/frontend-api";
6
2
  import {
7
3
  automationRoutes,
8
4
  pluginMetadata,
9
5
  automationAccess,
10
6
  } from "@checkstack/automation-common";
11
- import { AutomationListPage } from "./pages/AutomationListPage";
12
- import { AutomationEditPage } from "./pages/AutomationEditPage";
13
- import { RunsPage } from "./pages/RunsPage";
14
- import { RunDetailPage } from "./pages/RunDetailPage";
15
- import { TemplatePlaygroundPage } from "./pages/TemplatePlaygroundPage";
16
- import { AutomationMenuItems } from "./components/AutomationMenuItems";
7
+ import { Workflow } from "lucide-react";
17
8
 
18
9
  export {
19
10
  generateAutomationContextTypes,
@@ -52,46 +43,55 @@ export default createFrontendPlugin({
52
43
  routes: [
53
44
  {
54
45
  route: automationRoutes.routes.list,
55
- element: <AutomationListPage />,
46
+ load: () =>
47
+ import("./pages/AutomationListPage").then((m) => ({
48
+ default: m.AutomationListPage,
49
+ })),
56
50
  title: "Automations",
57
51
  accessRule: automationAccess.read,
52
+ nav: { group: "Automation", icon: Workflow },
58
53
  },
59
54
  {
60
55
  route: automationRoutes.routes.create,
61
- element: <AutomationEditPage />,
56
+ load: () =>
57
+ import("./pages/AutomationEditPage").then((m) => ({
58
+ default: m.AutomationEditPage,
59
+ })),
62
60
  title: "New automation",
63
61
  accessRule: automationAccess.manage,
64
62
  },
65
63
  {
66
64
  route: automationRoutes.routes.edit,
67
- element: <AutomationEditPage />,
65
+ load: () =>
66
+ import("./pages/AutomationEditPage").then((m) => ({
67
+ default: m.AutomationEditPage,
68
+ })),
68
69
  title: "Edit automation",
69
70
  accessRule: automationAccess.read,
70
71
  },
71
72
  {
72
73
  route: automationRoutes.routes.runs,
73
- element: <RunsPage />,
74
+ load: () => import("./pages/RunsPage").then((m) => ({ default: m.RunsPage })),
74
75
  title: "Run history",
75
76
  accessRule: automationAccess.read,
76
77
  },
77
78
  {
78
79
  route: automationRoutes.routes.runDetail,
79
- element: <RunDetailPage />,
80
+ load: () =>
81
+ import("./pages/RunDetailPage").then((m) => ({
82
+ default: m.RunDetailPage,
83
+ })),
80
84
  title: "Run details",
81
85
  accessRule: automationAccess.read,
82
86
  },
83
87
  {
84
88
  route: automationRoutes.routes.playground,
85
- element: <TemplatePlaygroundPage />,
89
+ load: () =>
90
+ import("./pages/TemplatePlaygroundPage").then((m) => ({
91
+ default: m.TemplatePlaygroundPage,
92
+ })),
86
93
  title: "Template playground",
87
94
  accessRule: automationAccess.read,
88
95
  },
89
96
  ],
90
- extensions: [
91
- createSlotExtension(UserMenuItemsSlot, {
92
- id: "automation.user-menu.items",
93
- component: AutomationMenuItems,
94
- metadata: { group: "Automation" },
95
- }),
96
- ],
97
97
  });
@@ -57,6 +57,7 @@ import { assignDefaultTriggerIds } from "../editor/trigger-helpers";
57
57
  import { computeYamlMarkers } from "../editor/yaml-markers";
58
58
  import { partitionIssues } from "../editor/editor-validation";
59
59
  import { AutomationGroupCombobox } from "../components/AutomationGroupCombobox";
60
+ import { RunAsServiceAccountPicker } from "../components/RunAsServiceAccountPicker";
60
61
 
61
62
  const STARTER_DEFINITION: AutomationDefinition = {
62
63
  name: "New Automation",
@@ -133,6 +134,9 @@ const AutomationEditContent: React.FC = () => {
133
134
  const [description, setDescription] = React.useState("");
134
135
  // Empty string means "Ungrouped" — sent as `null` on save to clear it.
135
136
  const [group, setGroup] = React.useState("");
137
+ // Service account (Application id) the automation runs as. Empty means
138
+ // "not chosen yet" — save is blocked until one is selected.
139
+ const [runAsApplicationId, setRunAsApplicationId] = React.useState("");
136
140
  const [statusEnabled, setStatusEnabled] = React.useState(true);
137
141
 
138
142
  // Existing group values for the picker's "pick existing" suggestions.
@@ -172,6 +176,7 @@ const AutomationEditContent: React.FC = () => {
172
176
  setName(a.name);
173
177
  setDescription(a.description ?? "");
174
178
  setGroup(a.group ?? "");
179
+ setRunAsApplicationId(a.runAs ?? "");
175
180
  setStatusEnabled(a.status === "enabled");
176
181
  setDefinition(normalized);
177
182
  setYamlText(stringifyYaml(normalized));
@@ -337,6 +342,7 @@ const AutomationEditContent: React.FC = () => {
337
342
  name,
338
343
  description: description || undefined,
339
344
  group: trimmedGroup || undefined,
345
+ runAs: runAsApplicationId,
340
346
  status: statusEnabled ? "enabled" : "disabled",
341
347
  definition: merged,
342
348
  });
@@ -346,6 +352,7 @@ const AutomationEditContent: React.FC = () => {
346
352
  name,
347
353
  description: description || undefined,
348
354
  group: trimmedGroup || null,
355
+ runAs: runAsApplicationId,
349
356
  status: statusEnabled ? "enabled" : "disabled",
350
357
  definition: merged,
351
358
  });
@@ -393,8 +400,15 @@ const AutomationEditContent: React.FC = () => {
393
400
  // "Input validation failed" toast. Validate it here so the Name field can
394
401
  // surface the error and Save can be disabled instead.
395
402
  const nameError = name.trim().length === 0 ? "Name is required" : undefined;
403
+ // A service account is required: the automation runs with its permissions,
404
+ // so the backend rejects a save without one. Block save + hint inline.
405
+ const runAsError =
406
+ runAsApplicationId.trim().length === 0
407
+ ? "A service account is required"
408
+ : undefined;
396
409
  const isSaving = createMutation.isPending || updateMutation.isPending;
397
- const canSave = !nameError && validationErrors.length === 0 && !isSaving;
410
+ const canSave =
411
+ !nameError && !runAsError && validationErrors.length === 0 && !isSaving;
398
412
 
399
413
  return (
400
414
  <PageLayout
@@ -505,6 +519,12 @@ const AutomationEditContent: React.FC = () => {
505
519
  Optional. Organises the automations list into sections.
506
520
  </p>
507
521
  </div>
522
+ <RunAsServiceAccountPicker
523
+ value={runAsApplicationId}
524
+ onValueChange={setRunAsApplicationId}
525
+ disabled={!canManage}
526
+ showError={!!runAsError}
527
+ />
508
528
  <div className="flex items-center justify-between">
509
529
  <Label htmlFor="enabled">Enabled</Label>
510
530
  <Toggle
@@ -26,6 +26,7 @@ function make({
26
26
  id,
27
27
  name: `Automation ${id}`,
28
28
  group,
29
+ runAs: null,
29
30
  status: "enabled",
30
31
  definition: DEFINITION,
31
32
  createdAt: new Date("2026-01-01T00:00:00Z"),
package/tsconfig.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "src"
5
5
  ],
6
6
  "references": [
7
+ {
8
+ "path": "../auth-common"
9
+ },
7
10
  {
8
11
  "path": "../automation-common"
9
12
  },
@@ -1,37 +0,0 @@
1
- import React from "react";
2
- import { Link } from "react-router-dom";
3
- import { Workflow } from "lucide-react";
4
- import type { UserMenuItemsContext } from "@checkstack/frontend-api";
5
- import { DropdownMenuItem } from "@checkstack/ui";
6
- import { resolveRoute } from "@checkstack/common";
7
- import {
8
- automationRoutes,
9
- automationAccess,
10
- pluginMetadata,
11
- } from "@checkstack/automation-common";
12
-
13
- /**
14
- * "Automations" entry in the user menu. Only renders for users with
15
- * `automation.read` access — mirrors `incident-frontend`'s pattern of
16
- * gating the menu item rather than the underlying route (the route is
17
- * also access-gated by `createFrontendPlugin`, but hiding the link is
18
- * the cleaner UX for unauthorised users).
19
- */
20
- export const AutomationMenuItems = ({
21
- accessRules: userPerms,
22
- }: UserMenuItemsContext) => {
23
- const qualifiedId = `${pluginMetadata.pluginId}.${automationAccess.read.id}`;
24
- const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
25
-
26
- if (!canRead) {
27
- return <React.Fragment />;
28
- }
29
-
30
- return (
31
- <Link to={resolveRoute(automationRoutes.routes.list)}>
32
- <DropdownMenuItem icon={<Workflow className="w-4 h-4" />}>
33
- Automations
34
- </DropdownMenuItem>
35
- </Link>
36
- );
37
- };