@agent-native/core 0.173.2-nightly-20260825163448 → 0.174.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.
Files changed (73) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/factory/AGENTS.md +1 -1
  3. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +23 -8
  4. package/corpus/templates/factory/actions/delete-factory.ts +174 -0
  5. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +28 -20
  6. package/corpus/templates/factory/actions/poll-github-sources.ts +6 -0
  7. package/corpus/templates/factory/actions/poll-sentry-errors.ts +16 -1
  8. package/corpus/templates/factory/actions/poll-slack-channel.ts +12 -0
  9. package/corpus/templates/factory/actions/start-builder-for-item.ts +74 -42
  10. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +117 -1
  11. package/corpus/templates/factory/app/i18n/en-US.ts +12 -0
  12. package/corpus/templates/factory/app/routes/factory.tsx +6 -1
  13. package/corpus/templates/factory/changelog/2026-08-24-slack-polling-accepts-the-agentnative-bot-handle.md +6 -0
  14. package/corpus/templates/factory/changelog/2026-08-24-you-can-delete-a-user-created-factory-from-settings.md +6 -0
  15. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +6 -0
  16. package/corpus/templates/factory/server/lib/factory-scope.ts +45 -1
  17. package/corpus/templates/factory/server/lib/require-factory-automation.ts +5 -0
  18. package/corpus/templates/factory/server/triage/slack-client.ts +10 -2
  19. package/corpus/templates/plan/server/plugins/core-routes.ts +6 -1
  20. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +4 -1
  21. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -2
  22. package/dist/app-config/app-identity.d.ts +20 -0
  23. package/dist/app-config/app-identity.js +41 -0
  24. package/dist/app-config/app.d.ts +2 -0
  25. package/dist/app-config/app.js +14 -0
  26. package/dist/app-config/observability.d.ts +27 -0
  27. package/dist/app-config/observability.js +76 -0
  28. package/dist/app-config/schema.d.ts +13 -0
  29. package/dist/app-config/schema.js +2 -0
  30. package/dist/app-config/store.d.ts +14 -4
  31. package/dist/app-config/store.js +18 -3
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/routes.d.ts +1 -1
  34. package/dist/email-catalog/actions/list-email-log.js +2 -2
  35. package/dist/email-catalog/actions/list-transactional-emails.js +2 -2
  36. package/dist/email-catalog/registry.js +3 -3
  37. package/dist/mcp/connect-route.js +15 -2
  38. package/dist/notifications/routes.d.ts +3 -3
  39. package/dist/observability/index.d.ts +1 -2
  40. package/dist/observability/index.js +0 -1
  41. package/dist/observability/posthog-ai.js +11 -3
  42. package/dist/observability/sentiment.d.ts +0 -1
  43. package/dist/observability/sentiment.js +8 -5
  44. package/dist/observability/traces.js +6 -12
  45. package/dist/observability/types.d.ts +0 -7
  46. package/dist/observability/types.js +1 -12
  47. package/dist/progress/routes.d.ts +1 -1
  48. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  49. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  50. package/dist/resources/handlers.d.ts +1 -1
  51. package/dist/secrets/routes.d.ts +3 -3
  52. package/dist/server/agent-chat/plugin-options.d.ts +17 -1
  53. package/dist/server/agent-chat-plugin.d.ts +10 -0
  54. package/dist/server/agent-chat-plugin.js +75 -67
  55. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  56. package/dist/server/core-routes/mcp-connect-options.d.ts +70 -0
  57. package/dist/server/core-routes/mcp-connect-options.js +77 -0
  58. package/dist/server/core-routes-plugin.d.ts +15 -11
  59. package/dist/server/core-routes-plugin.js +8 -7
  60. package/dist/server/edge.d.ts +1 -0
  61. package/dist/server/email-template.js +2 -2
  62. package/dist/server/email-templates.js +7 -7
  63. package/dist/server/email.js +4 -4
  64. package/dist/server/google-oauth.js +2 -2
  65. package/dist/server/http-response-telemetry.js +4 -4
  66. package/dist/server/identity-sso.js +1 -2
  67. package/dist/server/index.d.ts +1 -0
  68. package/dist/server/realtime-token.d.ts +1 -1
  69. package/dist/server/social-og-image.js +2 -2
  70. package/docs/content/observability.mdx +25 -31
  71. package/package.json +3 -3
  72. package/dist/server/app-name.d.ts +0 -33
  73. package/dist/server/app-name.js +0 -82
@@ -6,11 +6,21 @@ import { useT } from "@agent-native/core/client/i18n";
6
6
  import { buildSettingsRoute } from "@agent-native/core/client/navigation";
7
7
  import { SettingsGroup, SettingsRow } from "@agent-native/core/client/settings";
8
8
  import { ActionQueryError } from "@agent-native/dispatch/components";
9
- import { IconLoader2 } from "@tabler/icons-react";
9
+ import { IconLoader2, IconTrash } from "@tabler/icons-react";
10
10
  import { useCallback, useEffect, useRef, useState } from "react";
11
11
  import { Link } from "react-router";
12
12
  import { toast } from "sonner";
13
13
 
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogHeader,
22
+ AlertDialogTitle,
23
+ } from "@/components/ui/alert-dialog";
14
24
  import { Button } from "@/components/ui/button";
15
25
  import { Input } from "@/components/ui/input";
16
26
  import { Switch } from "@/components/ui/switch";
@@ -107,8 +117,12 @@ function formatAutomationDate(value: string | number | null | undefined) {
107
117
 
108
118
  export function FactorySettingsView({
109
119
  factoryId = "product-feedback",
120
+ factoryName,
121
+ onDeleted,
110
122
  }: {
111
123
  factoryId?: string;
124
+ factoryName: string;
125
+ onDeleted: () => void;
112
126
  }) {
113
127
  const t = useT();
114
128
  const [workspace, setWorkspace] = useState<"primary" | "secondary">(
@@ -128,6 +142,8 @@ export function FactorySettingsView({
128
142
  useState(true);
129
143
  const [automationFailureAlertEmail, setAutomationFailureAlertEmail] =
130
144
  useState("");
145
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
146
+ const [deleteConfirmation, setDeleteConfirmation] = useState("");
131
147
  const query = useActionQuery("get-triage-config", { factoryId });
132
148
  const schedulerHealthQuery = useActionQuery<FactoryAutomationHealth>(
133
149
  "get-factory-automation-health",
@@ -138,6 +154,9 @@ export function FactorySettingsView({
138
154
  const hydratedRef = useRef(false);
139
155
  const dirtyRef = useRef(false);
140
156
  const mutation = useActionMutation("save-triage-config");
157
+ const deleteMutation = useActionMutation("delete-factory", {
158
+ method: "DELETE",
159
+ });
141
160
 
142
161
  const applyForm = useCallback((state: TriageFormState) => {
143
162
  setWorkspace(state.workspace);
@@ -235,6 +254,24 @@ export function FactorySettingsView({
235
254
  }
236
255
  };
237
256
 
257
+ const deleteFactory = async () => {
258
+ try {
259
+ await deleteMutation.mutateAsync({
260
+ factoryId,
261
+ confirmName: deleteConfirmation,
262
+ });
263
+ toast.success(t("factoryRoute.factoryDeleted"));
264
+ setDeleteDialogOpen(false);
265
+ onDeleted();
266
+ } catch (error) {
267
+ toast.error(
268
+ error instanceof Error
269
+ ? error.message
270
+ : t("factoryRoute.factoryDeleteFailed"),
271
+ );
272
+ }
273
+ };
274
+
238
275
  if (query.isLoading) {
239
276
  return (
240
277
  <div className="mx-auto w-full max-w-3xl space-y-6 p-4 lg:p-6">
@@ -516,6 +553,85 @@ export function FactorySettingsView({
516
553
  error={schedulerHealthQuery.error}
517
554
  t={t}
518
555
  />
556
+
557
+ {factoryId !== "product-feedback" ? (
558
+ <SettingsGroup title={t("factoryRoute.dangerZone")}>
559
+ <SettingsRow
560
+ label={t("factoryRoute.deleteFactory")}
561
+ description={t("factoryRoute.deleteFactoryDescription")}
562
+ control={
563
+ <Button
564
+ type="button"
565
+ variant="destructive"
566
+ onClick={() => setDeleteDialogOpen(true)}
567
+ disabled={deleteMutation.isPending}
568
+ >
569
+ <IconTrash />
570
+ {t("factoryRoute.deleteFactory")}
571
+ </Button>
572
+ }
573
+ />
574
+ </SettingsGroup>
575
+ ) : null}
576
+
577
+ <AlertDialog
578
+ open={deleteDialogOpen}
579
+ onOpenChange={(open) => {
580
+ if (deleteMutation.isPending) return;
581
+ setDeleteDialogOpen(open);
582
+ if (!open) setDeleteConfirmation("");
583
+ }}
584
+ >
585
+ <AlertDialogContent>
586
+ <AlertDialogHeader>
587
+ <AlertDialogTitle>
588
+ {t("factoryRoute.deleteFactoryTitle", { name: factoryName })}
589
+ </AlertDialogTitle>
590
+ <AlertDialogDescription>
591
+ {t("factoryRoute.deleteFactoryWarning")}
592
+ </AlertDialogDescription>
593
+ </AlertDialogHeader>
594
+ <div className="grid gap-2">
595
+ <label
596
+ htmlFor="delete-factory-confirmation"
597
+ className="text-sm font-medium"
598
+ >
599
+ {t("factoryRoute.deleteFactoryConfirmation", {
600
+ name: factoryName,
601
+ })}
602
+ </label>
603
+ <Input
604
+ id="delete-factory-confirmation"
605
+ value={deleteConfirmation}
606
+ onChange={(event) => setDeleteConfirmation(event.target.value)}
607
+ autoComplete="off"
608
+ autoFocus
609
+ />
610
+ </div>
611
+ <AlertDialogFooter>
612
+ <AlertDialogCancel disabled={deleteMutation.isPending}>
613
+ {t("factoryRoute.deleteFactoryCancel")}
614
+ </AlertDialogCancel>
615
+ <AlertDialogAction
616
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
617
+ disabled={
618
+ deleteConfirmation !== factoryName || deleteMutation.isPending
619
+ }
620
+ onClick={(event) => {
621
+ event.preventDefault();
622
+ void deleteFactory();
623
+ }}
624
+ >
625
+ {deleteMutation.isPending ? (
626
+ <IconLoader2 className="animate-spin" />
627
+ ) : (
628
+ <IconTrash />
629
+ )}
630
+ {t("factoryRoute.deleteFactoryConfirm")}
631
+ </AlertDialogAction>
632
+ </AlertDialogFooter>
633
+ </AlertDialogContent>
634
+ </AlertDialog>
519
635
  </div>
520
636
  );
521
637
  }
@@ -194,6 +194,18 @@ const messages = {
194
194
  automationsTab: "Automations",
195
195
  agentsTab: "Agents",
196
196
  factorySettings: "Settings",
197
+ dangerZone: "Danger Zone",
198
+ deleteFactory: "Delete factory",
199
+ deleteFactoryDescription:
200
+ "Permanently remove this factory, its map, queue, rules, activity, settings, polling, and automations. This action is irreversible.",
201
+ deleteFactoryTitle: "Delete {{name}}?",
202
+ deleteFactoryWarning:
203
+ "Work already running in GitHub, Slack, or Builder will not be cancelled.",
204
+ deleteFactoryConfirmation: "Type {{name}} to confirm",
205
+ deleteFactoryCancel: "Cancel",
206
+ deleteFactoryConfirm: "Delete factory",
207
+ factoryDeleted: "Factory deleted.",
208
+ factoryDeleteFailed: "Could not delete the factory.",
197
209
  agenticAppsTitle: "Agentic apps",
198
210
  agenticAppsEmpty: "No agentic apps in this workspace yet.",
199
211
  createApp: "Create app",
@@ -752,7 +752,12 @@ export default function FactoryRoute() {
752
752
  ) : activeTab === "rules" ? (
753
753
  <RulesView factoryId={factoryId} t={t} />
754
754
  ) : activeTab === "settings" ? (
755
- <FactorySettingsView key={factoryId} factoryId={factoryId} />
755
+ <FactorySettingsView
756
+ key={factoryId}
757
+ factoryId={factoryId}
758
+ factoryName={graphData?.factory.name ?? graph.name}
759
+ onDeleted={goToFactoryList}
760
+ />
756
761
  ) : activeTab === "automations" ? (
757
762
  <AutomationsView key={factoryId} factoryId={factoryId} t={t} />
758
763
  ) : activeTab === "audit" ? (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-24
4
+ ---
5
+
6
+ Slack polling now recognizes the Agent Native bot when Slack reports its handle as @agentnative.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-24
4
+ ---
5
+
6
+ You can permanently delete a user-created Factory from Settings after typing its name.
@@ -1,4 +1,8 @@
1
1
  import { getDb } from "../db/index.js";
2
+ import {
3
+ DEFAULT_FACTORY_ID,
4
+ readFactoryDefinition,
5
+ } from "../factory-graph/store.js";
2
6
  import {
3
7
  ensureFactoryAutomations,
4
8
  syncFactoryAutomationEnabledStates,
@@ -11,6 +15,8 @@ export async function repairFactoryAutomationsFromConfig(
11
15
  orgId: string,
12
16
  factoryId: string,
13
17
  ): Promise<void> {
18
+ const factory = await readFactoryDefinition(orgId, factoryId);
19
+ if (!factory && factoryId !== DEFAULT_FACTORY_ID) return;
14
20
  const config = await readTriageConfigRow(getDb(), orgId, factoryId);
15
21
  if (!config) return;
16
22
  const enabledNames = resolveEnabledAutomationsFromSavedConfig({
@@ -1,4 +1,4 @@
1
- import { and, eq, type AnyColumn, type SQL } from "drizzle-orm";
1
+ import { and, eq, exists, type AnyColumn, type SQL } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
 
4
4
  import type { getDb } from "../db/index.js";
@@ -134,6 +134,50 @@ export function triageConfigUpdateRowId(
134
134
 
135
135
  type Db = ReturnType<typeof getDb>;
136
136
 
137
+ export function factoryStillPresent(
138
+ db: Db,
139
+ orgId: string,
140
+ factoryId: string,
141
+ ): SQL | undefined {
142
+ if (factoryId === DEFAULT_FACTORY_ID) return undefined;
143
+ return exists(
144
+ db
145
+ .select({ id: factoryDefinitions.id })
146
+ .from(factoryDefinitions)
147
+ .where(
148
+ and(
149
+ eq(factoryDefinitions.id, factoryId),
150
+ eq(factoryDefinitions.orgId, orgId),
151
+ ),
152
+ ),
153
+ );
154
+ }
155
+
156
+ export async function requireExistingFactory(
157
+ db: Db,
158
+ orgId: string,
159
+ factoryId: string,
160
+ ): Promise<void> {
161
+ // product-feedback is virtual until first save and cannot be deleted, so a
162
+ // missing definition row is not a deletion fence.
163
+ if (factoryId === DEFAULT_FACTORY_ID) return;
164
+ const row = (
165
+ await db
166
+ .select({ id: factoryDefinitions.id })
167
+ .from(factoryDefinitions)
168
+ .where(
169
+ and(
170
+ eq(factoryDefinitions.id, factoryId),
171
+ eq(factoryDefinitions.orgId, orgId),
172
+ ),
173
+ )
174
+ .limit(1)
175
+ )[0];
176
+ if (!row) {
177
+ throw new Error("Factory not found.");
178
+ }
179
+ }
180
+
137
181
  export async function readTriageConfigRow(
138
182
  db: Db,
139
183
  orgId: string,
@@ -1,9 +1,11 @@
1
1
  import type { ActionRunContext } from "@agent-native/core/action";
2
2
  import { listAutomationDefinitions } from "@agent-native/core/triggers";
3
3
 
4
+ import { getDb } from "../db/index.js";
4
5
  import {
5
6
  factoryAutomationLeafName,
6
7
  readAutomationFactoryId,
8
+ requireExistingFactory,
7
9
  } from "./factory-scope.js";
8
10
  import type { WorkspaceMemberIdentity } from "./require-workspace-member.js";
9
11
 
@@ -79,4 +81,7 @@ export async function requireFactoryAutomation(
79
81
  "The action was not invoked by a governed Factory automation.",
80
82
  );
81
83
  }
84
+ if (expectedFactoryId) {
85
+ await requireExistingFactory(getDb(), identity.orgId, expectedFactoryId);
86
+ }
82
87
  }
@@ -26,6 +26,15 @@ export interface SlackReaderIdentity {
26
26
  orgId?: string | null;
27
27
  }
28
28
 
29
+ const AGENT_NATIVE_SLACK_USER_NAMES = new Set(["agent-native", "agentnative"]);
30
+
31
+ /** Slack reports the bot as @agent-native or @agentnative; do not treat hyphen padding as the same handle. */
32
+ export function isAgentNativeSlackUserName(value: string): boolean {
33
+ return AGENT_NATIVE_SLACK_USER_NAMES.has(
34
+ value.trim().replace(/^@/, "").toLowerCase(),
35
+ );
36
+ }
37
+
29
38
  function createTokenResolver({
30
39
  ownerEmail,
31
40
  orgId,
@@ -53,8 +62,7 @@ export function createSlackReader(identity: SlackReaderIdentity) {
53
62
  const existing = identities.get(workspace);
54
63
  if (existing) return existing;
55
64
  const auth = await readAuthTest(workspace, tokenResolver);
56
- const userName = auth.userName.trim().replace(/^@/, "").toLowerCase();
57
- if (userName !== "agent-native") {
65
+ if (!isAgentNativeSlackUserName(auth.userName)) {
58
66
  throw new Error(
59
67
  `Slack credential authenticated as @${auth.userName}, not @agent-native.`,
60
68
  );
@@ -4,7 +4,12 @@ import { resolvePlanAnonymousOwner } from "../lib/public-plans.js";
4
4
 
5
5
  export default createCoreRoutesPlugin({
6
6
  anonymousOwner: resolvePlanAnonymousOwner,
7
- mcpConnectServerName: "plan",
7
+ // Plan's published MCP server id is the bare `plan`, not the derived
8
+ // `agent-native-plan`: `.agents/plugins/agent-native-visual-plans/.mcp.json`
9
+ // ships it, and the CLI's config writers key existing client entries by it.
10
+ // Dropping this override would write a second entry on the next connect
11
+ // instead of updating the one already in a user's config.
12
+ mcp: { serverName: "plan" },
8
13
  envKeys: [
9
14
  { key: "DATABASE_URL", label: "Database URL", required: false },
10
15
  {
@@ -4541,6 +4541,7 @@ export default function SlideEditor({
4541
4541
  selected && !isSlideCanvasShell(selected) ? selected : null,
4542
4542
  clicked && !isSlideCanvasShell(clicked) ? clicked : null,
4543
4543
  );
4544
+ const editableTextBlock = findSmartBlock(target, slideContent);
4544
4545
  const pointerIntent = resolveSlidesCanvasPointerIntent({
4545
4546
  hasSelectedObject: dragTarget !== null,
4546
4547
  targetWithinSelectedObject: dragTarget?.contains(target) ?? false,
@@ -4554,7 +4555,9 @@ export default function SlideEditor({
4554
4555
  e.clientX,
4555
4556
  e.clientY,
4556
4557
  ),
4557
- targetIsEditableText: Boolean(findSmartBlock(target, slideContent)),
4558
+ targetIsEditableText: Boolean(
4559
+ editableTextBlock && !isSlideCanvasShell(editableTextBlock),
4560
+ ),
4558
4561
  });
4559
4562
  if (
4560
4563
  dragTarget &&
@@ -146,8 +146,10 @@ export function resolveSlidesCanvasPointerIntent({
146
146
  }): SlidesCanvasPointerIntent {
147
147
  if (
148
148
  hasSelectedObject &&
149
- (targetWithinSelectedObject || targetContainsSelectedObject) &&
150
- pointerWithinMoveBand
149
+ pointerWithinMoveBand &&
150
+ (targetWithinSelectedObject ||
151
+ targetContainsSelectedObject ||
152
+ !targetIsEditableText)
151
153
  ) {
152
154
  return "move-object-perimeter";
153
155
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Fill this app's branding from the first-party template table.
3
+ *
4
+ * Runs after the schema parses, over already-resolved values — it is a pure
5
+ * lookup, not a layer. Reading package.json from disk here is what the earlier
6
+ * version did, and it put a synchronous `fs.readFileSync` inside `getAppConfig()`,
7
+ * which every request path calls. The repo already treats a package.json read as
8
+ * a development-only fallback (`server/cookie-namespace.ts` uses one solely in
9
+ * its non-production branch), so the input here is the declared `packageName`
10
+ * field and its `npm_package_name` alias.
11
+ *
12
+ * Only fills what is still unset, so an explicit `APP_NAME` or a
13
+ * `defineAppConfig()` value always wins.
14
+ *
15
+ * `slug` selects the per-app transactional email sender on agent-native.com, so
16
+ * it is only ever a name the template table already contains — an arbitrary
17
+ * package name cannot mint a mailbox there.
18
+ */
19
+ import type { AppConfig } from "./schema.js";
20
+ export declare function deriveAppIdentity(app: AppConfig["app"]): AppConfig["app"];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Fill this app's branding from the first-party template table.
3
+ *
4
+ * Runs after the schema parses, over already-resolved values — it is a pure
5
+ * lookup, not a layer. Reading package.json from disk here is what the earlier
6
+ * version did, and it put a synchronous `fs.readFileSync` inside `getAppConfig()`,
7
+ * which every request path calls. The repo already treats a package.json read as
8
+ * a development-only fallback (`server/cookie-namespace.ts` uses one solely in
9
+ * its non-production branch), so the input here is the declared `packageName`
10
+ * field and its `npm_package_name` alias.
11
+ *
12
+ * Only fills what is still unset, so an explicit `APP_NAME` or a
13
+ * `defineAppConfig()` value always wins.
14
+ *
15
+ * `slug` selects the per-app transactional email sender on agent-native.com, so
16
+ * it is only ever a name the template table already contains — an arbitrary
17
+ * package name cannot mint a mailbox there.
18
+ */
19
+ import { TEMPLATES } from "../cli/templates-meta.js";
20
+ function titlecase(s) {
21
+ return s
22
+ .split(/[-_\s]+/)
23
+ .filter(Boolean)
24
+ .map((w) => w[0].toUpperCase() + w.slice(1))
25
+ .join(" ");
26
+ }
27
+ export function deriveAppIdentity(app) {
28
+ if (app.name && app.slug && app.description)
29
+ return app;
30
+ const template = app.packageName
31
+ ? TEMPLATES.find((t) => t.name === app.packageName)
32
+ : undefined;
33
+ if (!template)
34
+ return app;
35
+ return {
36
+ ...app,
37
+ name: app.name ?? (template.label || titlecase(template.name)),
38
+ slug: app.slug ?? template.name,
39
+ description: app.description ?? template.hint ?? undefined,
40
+ };
41
+ }
@@ -32,4 +32,6 @@ export declare const appConfig: z.ZodObject<{
32
32
  url: z.ZodOptional<z.ZodString>;
33
33
  packageName: z.ZodOptional<z.ZodString>;
34
34
  template: z.ZodOptional<z.ZodString>;
35
+ slug: z.ZodOptional<z.ZodString>;
36
+ description: z.ZodOptional<z.ZodString>;
35
37
  }, z.core.$strip>;
@@ -112,4 +112,18 @@ export const appConfig = z.object({
112
112
  env: ["VITE_AGENT_NATIVE_TEMPLATE"],
113
113
  doc: "First-party template this app was generated from.",
114
114
  }),
115
+ // ── package.json-derived branding ───────────────────────────────────────
116
+ //
117
+ // Filled by the `package` layer (the lowest one), which matches this app's
118
+ // package.json against the first-party template table. Deliberately no `env`
119
+ // alias on either: `slug` selects the per-app mailbox on agent-native.com, so
120
+ // it must not be settable by an ambient string on the host. The layer is the
121
+ // only writer, and it only ever emits a name the template table already
122
+ // contains.
123
+ slug: z.string().min(1).optional().meta({
124
+ doc: "First-party template slug, matched from package.json. Selects the per-app transactional email sender.",
125
+ }),
126
+ description: z.string().min(1).optional().meta({
127
+ doc: "One-line description of the app, from first-party template metadata.",
128
+ }),
115
129
  });
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Agent trace capture.
4
+ *
5
+ * These were an `observability-config` settings row, which nothing in core ever
6
+ * wrote — the only "UI" was a snippet in the docs telling app authors to write
7
+ * it from their own code, and reading it put a database round-trip on the agent
8
+ * hot path inside a catch that made an outage indistinguishable from "never
9
+ * configured". They are deployment configuration, not a runtime preference.
10
+ *
11
+ * The three `inferredSentiment*` fields are declared here but still pass
12
+ * through `resolveInferredSentimentConfig`, which layers a hosted-vs-self-hosted
13
+ * derivation and an asymmetric "explicit false always wins" rule on top. Neither
14
+ * is expressible as a declared default, so that resolver stays the top layer
15
+ * rather than being flattened into these fields.
16
+ */
17
+ export declare const observabilityConfig: z.ZodObject<{
18
+ enabled: z.ZodDefault<z.ZodBoolean>;
19
+ capturePrompts: z.ZodDefault<z.ZodBoolean>;
20
+ captureToolArgs: z.ZodDefault<z.ZodBoolean>;
21
+ captureToolResults: z.ZodDefault<z.ZodBoolean>;
22
+ captureLlmSpans: z.ZodDefault<z.ZodBoolean>;
23
+ evalSampleRate: z.ZodDefault<z.ZodNumber>;
24
+ inferredSentimentEnabled: z.ZodOptional<z.ZodBoolean>;
25
+ inferredSentimentSampleRate: z.ZodOptional<z.ZodNumber>;
26
+ inferredSentimentModel: z.ZodDefault<z.ZodString>;
27
+ }, z.core.$strip>;
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Agent trace capture.
4
+ *
5
+ * These were an `observability-config` settings row, which nothing in core ever
6
+ * wrote — the only "UI" was a snippet in the docs telling app authors to write
7
+ * it from their own code, and reading it put a database round-trip on the agent
8
+ * hot path inside a catch that made an outage indistinguishable from "never
9
+ * configured". They are deployment configuration, not a runtime preference.
10
+ *
11
+ * The three `inferredSentiment*` fields are declared here but still pass
12
+ * through `resolveInferredSentimentConfig`, which layers a hosted-vs-self-hosted
13
+ * derivation and an asymmetric "explicit false always wins" rule on top. Neither
14
+ * is expressible as a declared default, so that resolver stays the top layer
15
+ * rather than being flattened into these fields.
16
+ */
17
+ export const observabilityConfig = z.object({
18
+ enabled: z
19
+ .boolean()
20
+ .default(true)
21
+ .meta({
22
+ env: ["AGENT_NATIVE_OBSERVABILITY"],
23
+ doc: "Capture agent run, model call, and tool call traces.",
24
+ }),
25
+ // Message bodies are user data, and a trace store is not a place to put them
26
+ // without a decision. Each of these three defaults to off for that reason.
27
+ capturePrompts: z
28
+ .boolean()
29
+ .default(false)
30
+ .meta({
31
+ env: ["AGENT_NATIVE_OBSERVABILITY_CAPTURE_PROMPTS"],
32
+ doc: "Include prompt and completion content on exported spans.",
33
+ }),
34
+ captureToolArgs: z
35
+ .boolean()
36
+ .default(false)
37
+ .meta({
38
+ env: ["AGENT_NATIVE_OBSERVABILITY_CAPTURE_TOOL_ARGS"],
39
+ doc: "Include action input arguments on tool spans.",
40
+ }),
41
+ captureToolResults: z
42
+ .boolean()
43
+ .default(false)
44
+ .meta({
45
+ env: ["AGENT_NATIVE_OBSERVABILITY_CAPTURE_TOOL_RESULTS"],
46
+ doc: "Include tool results and error text on tool spans.",
47
+ }),
48
+ captureLlmSpans: z
49
+ .boolean()
50
+ .default(true)
51
+ .meta({
52
+ env: ["AGENT_NATIVE_OBSERVABILITY_CAPTURE_LLM_SPANS"],
53
+ doc: "Emit one span per tool call alongside the run's trace.",
54
+ }),
55
+ evalSampleRate: z
56
+ .number()
57
+ .min(0)
58
+ .max(1)
59
+ .default(0)
60
+ .meta({
61
+ env: ["AGENT_NATIVE_OBSERVABILITY_EVAL_SAMPLE_RATE"],
62
+ doc: "Fraction of runs given an LLM-as-judge eval, 0 to 1.",
63
+ }),
64
+ // No env aliases: `resolveInferredSentimentConfig` owns the env step for
65
+ // these three, and declaring the alias here as well would give one value two
66
+ // resolvers — the thing this schema exists to remove.
67
+ inferredSentimentEnabled: z.boolean().optional().meta({
68
+ doc: "Classify the raw user message as positive, negative, or neutral. Defaults on for first-party hosted deployments only.",
69
+ }),
70
+ inferredSentimentSampleRate: z.number().min(0).max(1).optional().meta({
71
+ doc: "Deterministic fraction of eligible user messages to classify, 0 to 1.",
72
+ }),
73
+ inferredSentimentModel: z.string().min(1).default("gpt-5-6-luna").meta({
74
+ doc: "Model used by the managed sentiment classifier.",
75
+ }),
76
+ });
@@ -35,6 +35,8 @@ export declare const appConfigSchema: z.ZodObject<{
35
35
  url: z.ZodOptional<z.ZodString>;
36
36
  packageName: z.ZodOptional<z.ZodString>;
37
37
  template: z.ZodOptional<z.ZodString>;
38
+ slug: z.ZodOptional<z.ZodString>;
39
+ description: z.ZodOptional<z.ZodString>;
38
40
  }, z.core.$strip>>;
39
41
  auth: z.ZodPrefault<z.ZodObject<{
40
42
  disableDesktopSsoFallbackInDevelopment: z.ZodDefault<z.ZodBoolean>;
@@ -48,6 +50,17 @@ export declare const appConfigSchema: z.ZodObject<{
48
50
  releaseMigrations: z.ZodDefault<z.ZodBoolean>;
49
51
  betaSchemaOwner: z.ZodOptional<z.ZodString>;
50
52
  }, z.core.$strip>>;
53
+ observability: z.ZodPrefault<z.ZodObject<{
54
+ enabled: z.ZodDefault<z.ZodBoolean>;
55
+ capturePrompts: z.ZodDefault<z.ZodBoolean>;
56
+ captureToolArgs: z.ZodDefault<z.ZodBoolean>;
57
+ captureToolResults: z.ZodDefault<z.ZodBoolean>;
58
+ captureLlmSpans: z.ZodDefault<z.ZodBoolean>;
59
+ evalSampleRate: z.ZodDefault<z.ZodNumber>;
60
+ inferredSentimentEnabled: z.ZodOptional<z.ZodBoolean>;
61
+ inferredSentimentSampleRate: z.ZodOptional<z.ZodNumber>;
62
+ inferredSentimentModel: z.ZodDefault<z.ZodString>;
63
+ }, z.core.$strip>>;
51
64
  plugins: z.ZodPrefault<z.ZodObject<{
52
65
  disabled: z.ZodDefault<z.ZodArray<z.ZodEnum<{
53
66
  "agent-chat": "agent-chat";
@@ -5,6 +5,7 @@ import { appConfig } from "./app.js";
5
5
  import { authConfig } from "./auth.js";
6
6
  import { integrationsConfig } from "./integrations.js";
7
7
  import { migrationConfig } from "./migration.js";
8
+ import { observabilityConfig } from "./observability.js";
8
9
  import { pluginsConfig } from "./plugins.js";
9
10
  import { privateBlobConfig } from "./private-blob.js";
10
11
  import { workspaceConfig } from "./workspace.js";
@@ -31,6 +32,7 @@ export const appConfigSchema = z.object({
31
32
  auth: authConfig.prefault({}),
32
33
  integrations: integrationsConfig.prefault({}),
33
34
  migration: migrationConfig.prefault({}),
35
+ observability: observabilityConfig.prefault({}),
34
36
  plugins: pluginsConfig.prefault({}),
35
37
  privateBlob: privateBlobConfig.prefault({}),
36
38
  workspace: workspaceConfig.prefault({}),
@@ -24,11 +24,21 @@ export declare function setAppConfigLayer(layer: AppConfigLayer, config: AppConf
24
24
  /**
25
25
  * Sets this app's server configuration.
26
26
  *
27
- * Call it from a server plugin. Values given here beat any environment
28
- * variable aliased to the same field. Calling it more than once merges, so an
29
- * app can split its configuration by domain across plugin files.
27
+ * Values given here beat any environment variable aliased to the same field.
28
+ * Calling it more than once merges, so an app can split its configuration by
29
+ * domain across plugin files.
30
+ *
31
+ * Returns a Nitro plugin so the canonical home is `server/plugins/config.ts`
32
+ * with `export default defineAppConfig({ … })` — the same shape as every other
33
+ * framework plugin an app already writes. Before that, the docs said "call it
34
+ * from a server plugin" without saying what such a file looks like, and no app
35
+ * in this repo ever did.
36
+ *
37
+ * The layer is applied here, at module load, not when the returned plugin runs:
38
+ * configuration has to be resolved before another plugin reads it, and plugin
39
+ * invocation order is not something an app should have to reason about.
30
40
  */
31
- export declare function defineAppConfig(config: AppConfigInput): void;
41
+ export declare function defineAppConfig(config: AppConfigInput): () => void;
32
42
  /**
33
43
  * The resolved configuration, with declared defaults applied.
34
44
  *