@schlessera/brain-ui-server 0.33.1 → 0.34.1

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/src/app.ts CHANGED
@@ -170,7 +170,8 @@ export function createApp(options: CreateAppOptions = {}): BrainUiApp {
170
170
  assertPasskeyConfig(config.webauthn);
171
171
  // A missing (or unrecognized) agent backend refuses to boot HERE, not on the
172
172
  // first turn — otherwise /api/health reports healthy while every turn is
173
- // guaranteed to fail. Resolution only; the module still loads lazily.
173
+ // guaranteed to fail. Descriptor loading and profile validation happen here;
174
+ // backend construction and model discovery remain lazy.
174
175
  // Skipped when the embedder injects its own registry.
175
176
  if (!options.registry) assertBackendResolvable(config.agent);
176
177
 
package/src/config/env.ts CHANGED
@@ -447,12 +447,12 @@ export interface AgentConfig {
447
447
  confirmBashPatterns: string[] | null;
448
448
  claudeCodePath: string;
449
449
  defaultModel: string;
450
- /** Raw BRAIN_UI_CLAUDE_PROFILES JSON, parsed lazily by the registry. */
450
+ /** Raw BRAIN_UI_CLAUDE_PROFILES JSON, parsed at boot and again by the registry. */
451
451
  profilesJson: string | null;
452
452
  /**
453
- * Raw BRAIN_UI_PI_PROFILES JSON, parsed by the registry. When set, the pi
454
- * backend runs alongside the Claude backend and these profiles join the
455
- * picker.
453
+ * Raw BRAIN_UI_PI_PROFILES JSON, parsed at boot and again by the registry.
454
+ * When set, the pi backend runs alongside the Claude backend and these
455
+ * profiles join the picker.
456
456
  */
457
457
  piProfilesJson: string | null;
458
458
  modelDiscovery: boolean;
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { Hono } from "hono";
18
18
  import type { AgentConfig } from "../config/env.js";
19
- import { loadBackendModule, parsePiProfiles } from "../agent/backend.js";
19
+ import { loadBackendDescriptor, loadBackendModule } from "../agent/backend.js";
20
20
  import { readJsonBody } from "../middleware/body-limit.js";
21
21
  import { requireJson } from "../middleware/origin.js";
22
22
 
@@ -64,10 +64,15 @@ export function createPiAuthRoutes(deps: PiAuthRoutesDeps): Hono {
64
64
  * Vendors the deployment actually configured — the only providers this
65
65
  * surface may touch. Empty when pi is not in play at all.
66
66
  */
67
- function allowedProviders(): string[] {
68
- const fromProfiles = parsePiProfiles(agent.piProfilesJson, agent.profilesJson).map(
69
- (profile) => profile.vendor
70
- );
67
+ async function allowedProviders(): Promise<string[]> {
68
+ const descriptor = await loadBackendDescriptor("pi", deps.importer);
69
+ const parsed = descriptor.profileSchema.parse(agent.piProfilesJson, {
70
+ occupiedProfiles: [],
71
+ });
72
+ if (!parsed.ok) throw new Error(parsed.errors[0]?.message ?? "Invalid pi profiles.");
73
+ const fromProfiles = parsed.profiles
74
+ .map((profile) => profile.vendor)
75
+ .filter((vendor): vendor is string => typeof vendor === "string");
71
76
  return [...new Set(fromProfiles)];
72
77
  }
73
78
 
@@ -101,7 +106,7 @@ export function createPiAuthRoutes(deps: PiAuthRoutesDeps): Hono {
101
106
  // Not-configured is a normal state, not an error: the client hides the
102
107
  // whole card on an empty list.
103
108
  if (!piConfigured()) return c.json({ providers: [] });
104
- const providers = allowedProviders();
109
+ const providers = await allowedProviders();
105
110
  if (providers.length === 0) return c.json({ providers: [] });
106
111
  const auth = await getAuth();
107
112
  return c.json({ providers: await auth.status(providers) });
@@ -116,7 +121,7 @@ export function createPiAuthRoutes(deps: PiAuthRoutesDeps): Hono {
116
121
  if (!piConfigured()) {
117
122
  return c.json({ error: "The pi backend is not configured." }, 409);
118
123
  }
119
- if (!providerId || !allowedProviders().includes(providerId)) {
124
+ if (!providerId || !(await allowedProviders()).includes(providerId)) {
120
125
  return c.json({ error: "Unknown provider." }, 400);
121
126
  }
122
127
  const auth = await getAuth();
@@ -149,7 +154,7 @@ export function createPiAuthRoutes(deps: PiAuthRoutesDeps): Hono {
149
154
  providerId?: unknown;
150
155
  } | null;
151
156
  const providerId = typeof body?.providerId === "string" ? body.providerId : "";
152
- if (!providerId || !allowedProviders().includes(providerId)) {
157
+ if (!providerId || !(await allowedProviders()).includes(providerId)) {
153
158
  return c.json({ error: "Unknown provider." }, 400);
154
159
  }
155
160
  const auth = await getAuth();
@@ -44,7 +44,7 @@ import {
44
44
  } from "@schlessera/brain-ui-sdk/server";
45
45
  import type { AgentConfig } from "../config/env.js";
46
46
  import { resolveWebSearchEnv } from "../config/env.js";
47
- import { loadBackendModule, parsePiProfiles } from "../agent/backend.js";
47
+ import { loadBackendDescriptor, loadBackendModule } from "../agent/backend.js";
48
48
 
49
49
  /** One toggleable provider, as the Settings UI renders it. */
50
50
  export interface WebSearchProviderView {
@@ -142,17 +142,19 @@ export function createWebSearchRoutes(deps: WebSearchRoutesDeps): Hono {
142
142
  * createApp refuses to boot on it — but a throw would take the whole
143
143
  * settings card down over a label, so it degrades to an empty list.
144
144
  */
145
- const appliesTo = (): string[] => {
145
+ const appliesTo = async (): Promise<string[]> => {
146
146
  try {
147
- return parsePiProfiles(agent.piProfilesJson ?? null, agent.profilesJson ?? null).map(
148
- (p) => p.label
149
- );
147
+ const descriptor = await loadBackendDescriptor("pi", deps.importer);
148
+ const parsed = descriptor.profileSchema.parse(agent.piProfilesJson ?? null, {
149
+ occupiedProfiles: [],
150
+ });
151
+ return parsed.ok ? parsed.profiles.map((profile) => profile.label) : [];
150
152
  } catch {
151
153
  return [];
152
154
  }
153
155
  };
154
156
 
155
- function view(): WebSearchConfigView {
157
+ async function view(): Promise<WebSearchConfigView> {
156
158
  const config = readConfig(configPath());
157
159
  const override = readWebSearchOverride(config);
158
160
  const order = orderByCost(readWebSearchRouting(config));
@@ -161,7 +163,7 @@ export function createWebSearchRoutes(deps: WebSearchRoutesDeps): Hono {
161
163
  configured: true,
162
164
  order,
163
165
  overriddenBy: override,
164
- appliesTo: appliesTo(),
166
+ appliesTo: await appliesTo(),
165
167
  providers: WEB_SEARCH_PROVIDERS.map((p) => ({
166
168
  id: p.id,
167
169
  label: p.label,
@@ -180,13 +182,13 @@ export function createWebSearchRoutes(deps: WebSearchRoutesDeps): Hono {
180
182
  }
181
183
 
182
184
  return new Hono()
183
- .get("/web-search", (c) => {
185
+ .get("/web-search", async (c) => {
184
186
  // Not-configured is a normal state, not an error: the client hides the
185
187
  // whole card (same convention as /pi-auth/providers).
186
188
  if (!piConfigured()) {
187
189
  return c.json({ configured: false, order: [], overriddenBy: null, appliesTo: [], providers: [] });
188
190
  }
189
- return c.json(view());
191
+ return c.json(await view());
190
192
  })
191
193
  .put("/web-search", requireJson(), async (c) => {
192
194
  if (!piConfigured()) {
@@ -317,6 +319,6 @@ export function createWebSearchRoutes(deps: WebSearchRoutesDeps): Hono {
317
319
  /* config written; cache clearing is an optimization */
318
320
  }
319
321
 
320
- return c.json(view());
322
+ return c.json(await view());
321
323
  });
322
324
  }