@checkstack/ai-backend 0.1.3 → 0.1.5

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,100 @@
1
1
  # @checkstack/ai-backend
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 56e7c75: Hide navigation, actions and links that the current user cannot use, so anonymous
8
+ and read-only users no longer see entries that lead to "Access Denied" or to
9
+ actions the server would reject.
10
+
11
+ - **Sidebar**: a nav entry can now declare a dynamic `nav.isVisible({ accessRules, isAuthenticated })` predicate (in addition to the static `accessRule`). A group whose every entry is filtered out is no longer rendered. The filtering/grouping logic is extracted to a pure, unit-tested helper.
12
+ - **Infrastructure**: its sidebar entry is shown only when the user can READ at least one contributed tab (queue, cache, …), instead of always (it previously had no static rule because tabs are contributed at runtime).
13
+ - **Notification Settings**: hidden from anonymous users - notifications are per-user, so an anonymous visitor can't have any.
14
+ - **Anomaly Mute / Suppress**: the "Mute" / "Mute all" controls (a per-user preference) are hidden from anonymous visitors; the "Suppress" control is gated on `anomalyAccess.feed.manage`. Both were previously always visible.
15
+ - **Dashboard**: the "Open Catalog" actions (which open the manage-only Catalog config page) are hidden from users without `catalogAccess.system.manage`, and the "View catalog" link is gated on `catalogAccess.system.read`.
16
+ - **Dashboard status signals**: the per-system status rows contributed by plugins (`SystemSignalsSlot`) now render as a LINK only when the user can open the target, and as plain text otherwise. `SystemSignal` gains an optional `accessRule`; the healthcheck, anomaly, and dependency fillers set it for their gated targets (check-history / assignments / dependency-map). Signals pointing at ungated pages (incident / maintenance / SLO detail) stay links.
17
+ - **Plugin Manager**: the "Install plugin" button (which opens the install-gated page) is hidden from users with only `plugin` view access.
18
+ - **Satellites**: the page is entirely manage-gated, but its route/sidebar entry was gated on `read`, so read-only users saw the nav item and hit "Access Denied" on click. The route and nav entry now require `satellite.manage`.
19
+
20
+ The `@checkstack/ai-backend` bump is only the regenerated bundled docs index
21
+ (the frontend routing guide gained the `nav.isVisible` section); no code change.
22
+
23
+ **BREAKING (`@checkstack/frontend-api`):** the `AccessApi` interface gains a
24
+ required `useIsAuthenticated()` method. Custom `AccessApi` implementations must
25
+ add it (it returns `{ loading, isAuthenticated }`). The built-in auth
26
+ implementation and the no-auth fallback already do. `NavEntry` also gains an
27
+ optional `isVisible` predicate (purely additive).
28
+
29
+ - Updated dependencies [0626782]
30
+ - Updated dependencies [56e7c75]
31
+ - @checkstack/backend-api@0.21.5
32
+ - @checkstack/common@0.15.0
33
+ - @checkstack/ai-common@0.1.3
34
+ - @checkstack/integration-backend@0.4.5
35
+ - @checkstack/sdk@0.100.1
36
+
37
+ ## 0.1.4
38
+
39
+ ### Patch Changes
40
+
41
+ - b50916d: Fix "Date cannot be represented in JSON Schema" crashing the AI chat. Zod v4's
42
+ `toJSONSchema()` throws on `z.date()` (and even `z.coerce.date()`) by default,
43
+ and the chat hit this in TWO places:
44
+
45
+ - **`@checkstack/backend-api`** `toJsonSchema()` (the OpenAPI generator and AI
46
+ tool-introspection / MCP substrate) called it with no options.
47
+ - **`@checkstack/ai-backend`** the agent loop hands the Vercel AI SDK the raw
48
+ Zod tool input, and the SDK runs its OWN `toJSONSchema()` (throwing) to build
49
+ the model-facing tool schema - so a single date field in any tool input
50
+ crashed every chat turn (the whole tool list is projected before the model is
51
+ called).
52
+
53
+ Both now render dates as `{ type: "string", format: "date-time" }` (their wire
54
+ shape) and degrade other unrepresentable types to `{}` instead of throwing.
55
+
56
+ For the model boundary, a single `dateSafeModelSchema()` helper hands the SDK a
57
+ ready-made date-safe schema plus a validator that COERCES the ISO strings the
58
+ model emits back into real `Date`s before parsing with the original schema
59
+ (refinements and the downstream RPC client, which expects `Date`s, keep
60
+ working). A single `toModelSchema()` entry point applies this at EVERY point a
61
+ schema is handed to the model - chat tool inputs, the headless agent runner's
62
+ tool inputs (the automation "AI Action"), and `generateObject` structured
63
+ output - gated so non-date schemas are untouched, so individual tool / agent
64
+ definitions never special-case dates. Regression tests cover the converter, the
65
+ AI tool serializer, and the model-schema generation + coercion helper, including
66
+ the full inbound round-trip with the exact ISO shape a live model emits
67
+ (`...T22:00:00Z`, no milliseconds).
68
+
69
+ **Timezone correctness.** Because the model produces dates as text, the chat now
70
+ enforces an unambiguous wire contract: a date-time tool argument MUST be RFC 3339
71
+ with an explicit timezone offset. Zone-less (`2026-07-01T22:00:00`) and date-only
72
+ (`2026-07-01`) values are rejected with a model-readable error (the model
73
+ self-repairs), instead of being silently interpreted in the pod's local zone -
74
+ which would resolve the same string to different instants across pods. To resolve
75
+ an operator's bare "22:00", the browser's IANA timezone is sent with every chat
76
+ turn and folded into the system prompt, so each operator's times are interpreted
77
+ in their own zone by default. When no browser zone is available (a headless
78
+ automation AI Action), the reference zone falls back to the host/container
79
+ timezone (`TZ`), not UTC. A format-matrix test covers every common shape a model
80
+ might emit. The chat UI shows the operator which timezone is in use, and the
81
+ `TZ` override is documented for operators.
82
+
83
+ **Current time in context.** The model has no clock, so the system prompt now
84
+ includes the current instant (UTC plus the reference-zone wall clock), letting it
85
+ resolve relative dates like "today at 10:00" without asking. Applied to both the
86
+ chat and the headless agent runner, computed per turn/run so it is never stale.
87
+
88
+ **Less-strict topic classifier.** The chat's off-topic pre-classifier was
89
+ refusing legitimate requests like "create a maintenance" because maintenances
90
+ (and several other domains) were not listed. The classifier now enumerates the
91
+ full domain set and treats any create/list/update/delete action on a platform
92
+ resource as on-topic by default.
93
+
94
+ - Updated dependencies [b50916d]
95
+ - @checkstack/backend-api@0.21.4
96
+ - @checkstack/integration-backend@0.4.4
97
+
3
98
  ## 0.1.3
4
99
 
5
100
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ai-backend",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -16,12 +16,12 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@ai-sdk/openai-compatible": "^2.0.48",
19
- "@checkstack/ai-common": "0.1.2",
20
- "@checkstack/backend-api": "0.21.3",
21
- "@checkstack/common": "0.14.1",
19
+ "@checkstack/ai-common": "0.1.3",
20
+ "@checkstack/backend-api": "0.21.5",
21
+ "@checkstack/common": "0.15.0",
22
22
  "@checkstack/drizzle-helper": "0.0.5",
23
- "@checkstack/integration-backend": "0.4.3",
24
- "@checkstack/sdk": "0.98.1",
23
+ "@checkstack/integration-backend": "0.4.5",
24
+ "@checkstack/sdk": "0.100.1",
25
25
  "@orpc/client": "^1.14.4",
26
26
  "@orpc/contract": "^1.14.4",
27
27
  "@orpc/server": "^1.14.4",
@@ -31,7 +31,7 @@
31
31
  "zod": "^4.2.1"
32
32
  },
33
33
  "devDependencies": {
34
- "@checkstack/scripts": "0.6.0",
34
+ "@checkstack/scripts": "0.6.1",
35
35
  "@checkstack/tsconfig": "0.0.7",
36
36
  "@types/node": "^20.0.0",
37
37
  "@types/pg": "^8.20.0",
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it, mock } from "bun:test";
2
+ import { asSchema } from "ai";
2
3
  import { z } from "zod";
3
4
  import type { AuthUser, RpcClient } from "@checkstack/backend-api";
4
5
  import type { OpenAiCompatibleConnection } from "@checkstack/ai-common";
@@ -108,6 +109,55 @@ describe("createAgentRunner", () => {
108
109
  expect(result.toolCalls).toEqual([{ tool: "plugin.read", ok: true }]);
109
110
  });
110
111
 
112
+ it("hands the model a date-safe schema for tools with Date inputs (no throw)", async () => {
113
+ // Regression: the AI Action (headless agent runner) builds its OWN tools.
114
+ // A `z.date()` input would make the SDK's Zod->JSON-Schema conversion throw
115
+ // "Date cannot be represented...", crashing the action - the same bug as the
116
+ // chat. The runner must gate date inputs through dateSafeModelSchema too.
117
+ const registry = createAiToolRegistry();
118
+ registry.register({
119
+ name: "plugin.history",
120
+ description: "history",
121
+ effect: "read",
122
+ input: z.object({ since: z.date() }),
123
+ requiredAccessRules: [],
124
+ execute: async () => ({ ok: true }),
125
+ } as RegisteredAiTool);
126
+ const resolver = createAiToolResolver({ registry });
127
+
128
+ let offeredSchema: unknown;
129
+ const generateText = mock(
130
+ async (args: {
131
+ tools?: Record<string, { inputSchema: unknown }>;
132
+ }) => {
133
+ const t = (args.tools ?? {})["plugin.history"];
134
+ // Exactly what the SDK does internally to build the model request; this
135
+ // threw before the fix.
136
+ offeredSchema = await asSchema(t.inputSchema as never).jsonSchema;
137
+ return { text: "ok", usage: {} };
138
+ },
139
+ );
140
+
141
+ const runner = createAgentRunner({
142
+ resolver,
143
+ resolveConnection: async () => connection,
144
+ modelFns: { generateText: generateText as never },
145
+ });
146
+
147
+ await runner({
148
+ principal,
149
+ rpcClient,
150
+ connectionId: "conn-1",
151
+ prompt: "go",
152
+ });
153
+
154
+ const props = (
155
+ offeredSchema as { properties: Record<string, Record<string, unknown>> }
156
+ ).properties;
157
+ expect(props.since?.type).toBe("string");
158
+ expect(props.since?.format).toBe("date-time");
159
+ });
160
+
111
161
  it("offers a projected read tool and routes it through the principal's client", async () => {
112
162
  const registry = createAiToolRegistry();
113
163
  registry.register({
@@ -30,6 +30,8 @@ import {
30
30
  type LanguageModel,
31
31
  } from "ai";
32
32
  import { z } from "zod";
33
+ import { toModelSchema } from "./chat/model-schema";
34
+ import { buildDateTimeContext } from "./chat/system-prompt";
33
35
  import {
34
36
  createServiceRef,
35
37
  type AuthUser,
@@ -201,7 +203,8 @@ export function createAgentRunner({
201
203
 
202
204
  sdkTools[t.name] = aiTool({
203
205
  description: t.description,
204
- inputSchema: t.input as z.ZodType,
206
+ // Single model-boundary date handling, same as the chat tool path.
207
+ inputSchema: toModelSchema(t.input as z.ZodType),
205
208
  execute: async (input: unknown) => {
206
209
  try {
207
210
  const result = await invoke(input);
@@ -237,9 +240,13 @@ export function createAgentRunner({
237
240
  });
238
241
  }
239
242
 
243
+ // Append the date/time context at call time (NOT module load) so the model
244
+ // gets the CURRENT instant and the host-zone wire contract. Headless: no
245
+ // operator, so the reference zone is the host/container TZ.
246
+ const dateContext = buildDateTimeContext({ audience: "headless" });
240
247
  const { text } = await gen({
241
248
  model: languageModel,
242
- system: systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
249
+ system: `${systemPrompt ?? DEFAULT_SYSTEM_PROMPT} ${dateContext}`,
243
250
  prompt,
244
251
  tools: sdkTools,
245
252
  stopWhen: stepCountIs(maxSteps ?? DEFAULT_MAX_STEPS),
@@ -249,7 +256,10 @@ export function createAgentRunner({
249
256
  if (outputSchema) {
250
257
  const res = await genObj({
251
258
  model: languageModel,
252
- schema: outputSchema,
259
+ // Same single model-boundary date handling as the tool path: the
260
+ // structured-output schema's dates must serialize AND the model's ISO
261
+ // strings coerce back to Date.
262
+ schema: toModelSchema(outputSchema),
253
263
  system:
254
264
  "Produce the structured result from the analysis below. Use only information present in it; do not invent values.",
255
265
  prompt: `Task: ${prompt}\n\n--- Analysis ---\n${text}`,
@@ -10,6 +10,8 @@ const ChatTurnBodySchema = z.object({
10
10
  connectionId: z.string(),
11
11
  model: z.string().optional(),
12
12
  message: z.string().min(1),
13
+ /** Browser IANA timezone, used to resolve bare times the operator types. */
14
+ timeZone: z.string().optional(),
13
15
  });
14
16
 
15
17
  /**
@@ -25,6 +27,8 @@ const ChatDecisionBodySchema = z.object({
25
27
  token: z.string().min(1),
26
28
  kind: z.enum(["apply", "decline"]),
27
29
  }),
30
+ /** Browser IANA timezone, used to resolve bare times the operator types. */
31
+ timeZone: z.string().optional(),
28
32
  });
29
33
 
30
34
  /** A /chat POST is either a new user turn or a confirm-card decision turn. */
@@ -91,6 +95,7 @@ export function createChatRequestHandler({
91
95
  forwardHeaders,
92
96
  token: body.decision.token,
93
97
  decision: body.decision.kind,
98
+ timeZone: body.timeZone,
94
99
  });
95
100
  }
96
101
  return await chatService.streamTurn({
@@ -100,6 +105,7 @@ export function createChatRequestHandler({
100
105
  model: body.model,
101
106
  forwardHeaders,
102
107
  userText: body.message,
108
+ timeZone: body.timeZone,
103
109
  });
104
110
  } catch (error) {
105
111
  return Response.json(
@@ -41,6 +41,7 @@ import {
41
41
  type AgentToolCallbacks,
42
42
  } from "./sdk-tools";
43
43
  import type { ChatReadInvoker } from "./read-invoker";
44
+ import { buildChatSystemPrompt } from "./system-prompt";
44
45
  import { createUserScopedRpcClient } from "../user-rpc-client";
45
46
 
46
47
  type AiDatabase = SafeDatabase<typeof schema>;
@@ -200,6 +201,8 @@ export interface ChatTurnInput {
200
201
  forwardHeaders: Record<string, string>;
201
202
  /** The user's new message text. */
202
203
  userText: string;
204
+ /** The operator's IANA timezone (browser-detected) for resolving bare times. */
205
+ timeZone?: string;
203
206
  }
204
207
 
205
208
  /**
@@ -220,25 +223,10 @@ export interface ChatDecisionInput {
220
223
  token: string;
221
224
  /** Whether the operator applied or declined the card. */
222
225
  decision: DecisionKind;
226
+ /** The operator's IANA timezone (browser-detected) for resolving bare times. */
227
+ timeZone?: string;
223
228
  }
224
229
 
225
- const SYSTEM_PROMPT =
226
- "You are Checkstack's built-in assistant. You ONLY help operators run " +
227
- "Checkstack: incidents, health checks, anomalies, automations, and the " +
228
- "monitoring and operations of THIS platform. Use the provided tools to read " +
229
- "live data. For any change to the platform, call the appropriate tool: " +
230
- "depending on the conversation's permission mode it either returns a " +
231
- "confirmation card the operator must approve, or applies immediately and " +
232
- "returns the applied result. Never claim a change took effect until the tool " +
233
- "result confirms it (an applied result, or the operator approving the card). " +
234
- "Call each change tool ONCE per request: a confirm-card result means the " +
235
- "proposal succeeded and is awaiting the operator - do NOT call the tool again " +
236
- "to retry; just tell the operator you are waiting for their decision. " +
237
- "Politely DECLINE anything unrelated to operating Checkstack " +
238
- "(general coding help, writing, or general knowledge) with a one-line " +
239
- "redirect back to Checkstack monitoring and operations. Be concise and " +
240
- "engineering-focused.";
241
-
242
230
  /** Max agent steps (tool-call round trips) per turn. */
243
231
  const MAX_STEPS = 8;
244
232
 
@@ -532,6 +520,7 @@ export function createChatService({
532
520
  languageModel,
533
521
  recordUsage,
534
522
  modelMessages,
523
+ timeZone,
535
524
  }: {
536
525
  principal: AuthUser;
537
526
  conversation: { permissionMode: AiPermissionMode };
@@ -541,6 +530,8 @@ export function createChatService({
541
530
  languageModel: ReturnType<typeof buildLanguageModel>;
542
531
  recordUsage: (usage: LanguageModelUsage) => Promise<void>;
543
532
  modelMessages: ModelMessage[];
533
+ /** The operator's IANA timezone (from the browser), folded into the prompt. */
534
+ timeZone?: string;
544
535
  }): Response => {
545
536
  // Build the SDK tools from the resolver-allowed set only. The model is never
546
537
  // offered a tool the principal cannot use. Tool callbacks (budget + audit +
@@ -568,7 +559,7 @@ export function createChatService({
568
559
 
569
560
  const result = streamText({
570
561
  model: languageModel,
571
- system: SYSTEM_PROMPT,
562
+ system: buildChatSystemPrompt({ timeZone }),
572
563
  // Defensively normalize: drop empty-content rows and merge consecutive
573
564
  // same-role messages so a failed prior turn (which persists no assistant
574
565
  // reply, leaving consecutive `user` rows) cannot poison the history into a
@@ -680,6 +671,7 @@ export function createChatService({
680
671
  model,
681
672
  forwardHeaders,
682
673
  userText,
674
+ timeZone,
683
675
  } = input;
684
676
 
685
677
  // Ownership: the conversation MUST belong to the principal.
@@ -810,6 +802,7 @@ export function createChatService({
810
802
  languageModel,
811
803
  recordUsage,
812
804
  modelMessages,
805
+ timeZone,
813
806
  });
814
807
  },
815
808
 
@@ -831,6 +824,7 @@ export function createChatService({
831
824
  forwardHeaders,
832
825
  token,
833
826
  decision,
827
+ timeZone,
834
828
  } = input;
835
829
 
836
830
  const conversation = await loadOwnedConversation({
@@ -915,6 +909,7 @@ export function createChatService({
915
909
  languageModel,
916
910
  recordUsage,
917
911
  modelMessages,
912
+ timeZone,
918
913
  });
919
914
  },
920
915
  };
@@ -48,6 +48,17 @@ describe("buildClassifierPrompt", () => {
48
48
  expect(system).toMatch(/clearly unrelated|CLEARLY unrelated/i);
49
49
  });
50
50
 
51
+ test("system prompt names maintenances and a CRUD-action allowance as ON_TOPIC", () => {
52
+ // Regression for the real bug: "Create a maintenance" was refused because
53
+ // maintenances were not listed and there was no generic action allowance.
54
+ const { system } = buildClassifierPrompt({
55
+ userText: "Create a maintenance",
56
+ });
57
+ expect(system.toLowerCase()).toContain("maintenance");
58
+ // Any create/list/update/delete request must be ON_TOPIC by default.
59
+ expect(system).toMatch(/create[^.]*list[^.]*update[^.]*delete/i);
60
+ });
61
+
51
62
  test("system prompt retains the 'when in doubt' ON_TOPIC default", () => {
52
63
  const { system } = buildClassifierPrompt({ userText: "???" });
53
64
  expect(system).toMatch(/when in doubt.*on_topic/i);
@@ -19,18 +19,25 @@ export type ClassifierVerdict = "ON_TOPIC" | "OFF_TOPIC";
19
19
  * against any decoration regardless.
20
20
  */
21
21
  const CLASSIFIER_SYSTEM_PROMPT =
22
- "You are a topical classifier for Checkstack, an incident, health-check, " +
23
- "anomaly, automation, and monitoring/operations platform. Decide whether the " +
22
+ "You are a topical classifier for Checkstack, an operations platform covering " +
23
+ "incidents, health checks, anomalies, automations, maintenances/maintenance " +
24
+ "windows, dependencies, systems and services, notifications, SLOs, " +
25
+ "integrations, on-call, and general monitoring/operations. Decide whether the " +
24
26
  "user's message is ON_TOPIC or OFF_TOPIC. " +
25
- "ON_TOPIC includes: operating or reasoning about Checkstack (incidents, " +
26
- "health checks, anomalies, automations, monitoring, on-call, the platform's " +
27
- "data and configuration); meta/capability questions about the assistant itself " +
28
- "(\"what can you do\", \"who are you\", \"help\", \"what features do you have\"); " +
29
- "greetings and conversational openers (\"hi\", \"hello\", \"hey\"); " +
27
+ "ON_TOPIC includes: operating or reasoning about Checkstack or any of its " +
28
+ "resources and configuration; meta/capability questions about the assistant " +
29
+ "itself (\"what can you do\", \"who are you\", \"help\", \"what features do you " +
30
+ "have\"); greetings and conversational openers (\"hi\", \"hello\", \"hey\"); " +
30
31
  "how-to or conceptual questions about using Checkstack features or workflows " +
31
32
  "(\"how do health checks work\", \"how do I create an automation\"). " +
32
- "OFF_TOPIC means CLEARLY unrelated requests: general coding help unrelated to " +
33
- "Checkstack, creative writing, and general trivia or knowledge questions. " +
33
+ "IMPORTANT: any request to create, add, list, show, view, find, update, edit, " +
34
+ "schedule, start, stop, resolve, acknowledge, or delete something is ON_TOPIC " +
35
+ "by default - it is almost certainly an action on a platform resource (e.g. " +
36
+ "\"create a maintenance\", \"list incidents\", \"schedule downtime\"), EVEN IF " +
37
+ "the resource type is not named in the list above. " +
38
+ "OFF_TOPIC means ONLY requests that are CLEARLY unrelated to operating this " +
39
+ "platform: general-purpose coding help, creative writing, math homework, and " +
40
+ "general trivia or knowledge questions. " +
34
41
  "When in doubt, reply ON_TOPIC. Reply with the token only.";
35
42
 
36
43
  /**