@dbx-tools/appkit-mastra 0.1.74 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,10 +13,14 @@ is a one-line import change.
13
13
 
14
14
  The implementation lives under `src/`: agent registration in
15
15
  [`agents.ts`](src/agents.ts), the plugin + routes in
16
- [`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), Model
17
- Serving resolution in [`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts),
18
- Genie tooling in [`genie.ts`](src/genie.ts), and the chart pipeline in
19
- [`chart.ts`](src/chart.ts).
16
+ [`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), thread
17
+ history / listing in [`history.ts`](src/history.ts) /
18
+ [`threads.ts`](src/threads.ts), Model Serving resolution in
19
+ [`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts), Genie
20
+ tooling in [`genie.ts`](src/genie.ts), the chart pipeline in
21
+ [`chart.ts`](src/chart.ts), and MLflow feedback logging in
22
+ [`mlflow.ts`](src/mlflow.ts) (over the shared REST helper in
23
+ [`rest.ts`](src/rest.ts)).
20
24
 
21
25
  ## Quick start
22
26
 
@@ -113,6 +117,9 @@ management (registered alongside `/route/history`):
113
117
  - `DELETE /route/threads` - delete the targeted thread (id from the
114
118
  thread-selection header). Ownership is enforced server-side: a thread
115
119
  is only removed when it belongs to the caller.
120
+ - `PATCH /route/threads` - rename the targeted thread (id from the
121
+ thread-selection header) to the `{ title }` in the JSON body. Same
122
+ ownership enforcement; existing thread metadata is preserved.
116
123
 
117
124
  Each thread is auto-titled from its opening turn (Mastra's
118
125
  `generateTitle`), but titling runs on the **small / fast chat tier**
@@ -267,6 +274,35 @@ entry settles. Cache entries carry a 1h TTL; after expiry the route
267
274
  placement contract are in [`chart.ts`](src/chart.ts); the wire shapes
268
275
  are in [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
269
276
 
277
+ ## Feedback (MLflow)
278
+
279
+ When MLflow tracing is wired for the deployment, the plugin lets users
280
+ rate an assistant turn (thumbs up / down) and leave a comment, logged as
281
+ a HUMAN [assessment](https://mlflow.org/docs/latest/genai/assessments/)
282
+ on that turn's trace and attributed to the signed-in (OBO) user.
283
+
284
+ Enablement is auto-detected: it turns on when an OTLP exporter endpoint
285
+ (`OTEL_EXPORTER_OTLP_ENDPOINT` / `..._TRACES_ENDPOINT`) **and** an MLflow
286
+ experiment (`MLFLOW_EXPERIMENT_ID` / `MLFLOW_EXPERIMENT_NAME`) are
287
+ configured - the two signals that traces actually materialize in MLflow.
288
+ Set `config.feedback` to `true` / `false` to force it on or off
289
+ regardless of the env.
290
+
291
+ ```ts
292
+ mastra({ agents: support, feedback: true }); // force-enable
293
+ ```
294
+
295
+ Under the hood the server derives MLflow's trace id from the active
296
+ OpenTelemetry span (`tr-<hex(otelTraceId)>`) and stamps it on each
297
+ turn's response (`MLFLOW_TRACE_ID_HEADER`); the client sends it back to
298
+ `POST ${basePath}/route/feedback`, which posts the assessment to the
299
+ Databricks MLflow REST API. Trace export is asynchronous, so a
300
+ just-finished trace may not exist yet - the log call retries briefly on
301
+ "not found" and otherwise fails softly (the chat stays usable). The
302
+ current state is published to the client as
303
+ `clientConfig().feedbackEnabled`; see [`mlflow.ts`](src/mlflow.ts) and
304
+ the UI wiring in [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui).
305
+
270
306
  ## Model resolution
271
307
 
272
308
  Each agent call resolves a model lazily (so concurrent requests keep
@@ -338,6 +374,36 @@ resolves its model and tools as the calling user. The resolved endpoint
338
374
  paths are available in-process via
339
375
  `appkitUtils.require(this.context, mastra).exports().getMcp()`.
340
376
 
377
+ ## Client API surface (`apiAccess`)
378
+
379
+ `@mastra/express` registers its full management route table under the
380
+ plugin mount - agent inference *plus* admin / mutating routes (direct
381
+ tool execution, workflow control, raw memory read/write, telemetry,
382
+ logs, scores). AppKit authenticates every request as the OBO user, but
383
+ does not restrict *which* of those a browser client may call.
384
+
385
+ `config.apiAccess` gates that surface at the plugin's dispatch point:
386
+
387
+ - `"scoped"` (default): only the routes the chat client needs are
388
+ dispatched to Mastra - agent inference (`stream` / `generate` /
389
+ `network`), read-only agent metadata (`GET /agents`,
390
+ `GET /agents/:id`), this plugin's own OBO- and resource-scoped
391
+ `/route/*` routes (history / threads / feedback), and, when `mcp` is
392
+ enabled, the MCP transport. Everything else is refused with `403`
393
+ before it reaches Mastra.
394
+ - `"full"`: dispatch the entire stock Mastra API. Use only for a trusted
395
+ first-party console that genuinely needs the management surface.
396
+
397
+ ```ts
398
+ mastra({ agents: support }); // scoped by default
399
+ mastra({ agents: support, apiAccess: "full" }); // opt into the full API
400
+ ```
401
+
402
+ The gate is a pure allowlist (`isMastraRequestAllowed` in
403
+ [`server.ts`](src/server.ts)); the browser chat client only ever uses
404
+ the agent stream and the `/route/*` routes, so the default breaks
405
+ nothing.
406
+
341
407
  ## License
342
408
 
343
409
  Apache-2.0
package/dist/index.d.ts CHANGED
@@ -642,6 +642,28 @@ interface MastraPluginConfig extends BasePluginConfig {
642
642
  * calling user.
643
643
  */
644
644
  mcp?: boolean | MastraMcpConfig;
645
+ /**
646
+ * How much of the stock `@mastra/express` management API is reachable
647
+ * through the plugin mount. `@mastra/express` registers its full route
648
+ * table (agent inference plus admin / mutating routes: direct tool
649
+ * execution, workflow control, raw memory read/write, telemetry, logs,
650
+ * scores). AppKit already authenticates every request as the OBO user,
651
+ * but nothing there restricts *which* of those operations the browser
652
+ * client may invoke.
653
+ *
654
+ * - `"scoped"` (default): only the routes the chat client legitimately
655
+ * needs are dispatched to Mastra - agent inference
656
+ * (`stream` / `generate` / `network`), read-only agent metadata, this
657
+ * plugin's own OBO- and resource-scoped `/route/*` routes (history /
658
+ * threads), and, when {@link mcp} is enabled, the MCP transport.
659
+ * Everything else (tool execution, workflow control, raw memory,
660
+ * telemetry, logs, scores, and other mutations) is rejected with
661
+ * `403` before it reaches Mastra.
662
+ * - `"full"`: dispatch the entire stock Mastra API. Use only for a
663
+ * trusted first-party console that genuinely needs the management
664
+ * surface.
665
+ */
666
+ apiAccess?: "scoped" | "full";
645
667
  }
646
668
  //#endregion
647
669
  //#region packages/appkit-mastra/src/memory.d.ts
@@ -1310,13 +1332,17 @@ declare function buildMcpServer(opts: {
1310
1332
  }): ResolvedMcp | null;
1311
1333
  //#endregion
1312
1334
  //#region packages/appkit-mastra/src/server.d.ts
1335
+ /**
1336
+ * `@mastra/express` subclass that stamps `RequestContext` with the
1337
+ * AppKit user, resource id, and a thread id backed by an HTTP-only
1338
+ * session cookie (`appkit_<plugin-name>_session_id`).
1339
+ */
1313
1340
  declare class MastraServer$1 extends MastraServer {
1314
1341
  private config;
1315
1342
  private log;
1316
1343
  /**
1317
- * Whether to stamp the MLflow trace-id header on responses. Mirrors
1318
- * the plugin's feedback gate: the explicit `config.feedback` override
1319
- * wins, else auto-detect from the MLflow tracing env.
1344
+ * Whether to stamp the MLflow trace-id header on responses. Shares the
1345
+ * plugin's feedback gate via {@link resolveFeedbackEnabled}.
1320
1346
  */
1321
1347
  private feedbackEnabled;
1322
1348
  constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServer>);
@@ -1486,10 +1512,9 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
1486
1512
  };
1487
1513
  clientConfig(): Record<string, unknown>;
1488
1514
  /**
1489
- * Whether user feedback can be logged to MLflow. The explicit
1490
- * `config.feedback` override wins; otherwise auto-detect from the
1491
- * MLflow tracing env ({@link mlflowEnabled}). Gates both the client
1492
- * config flag and the feedback route so the two never disagree.
1515
+ * Whether user feedback can be logged to MLflow. Delegates to
1516
+ * {@link resolveFeedbackEnabled} so the client-config flag and the
1517
+ * feedback route share the same gate as the server's trace-id header.
1493
1518
  */
1494
1519
  private feedbackEnabled;
1495
1520
  injectRoutes(router: IAppRouter): void;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ChartSchema, ChartTypeSchema, DEFAULT_COMMENT_NAME, DEFAULT_FEEDBACK_NAME, MASTRA_ROUTES, MLFLOW_TRACE_ID_HEADER, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, MastraFeedbackRequestSchema, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
1
+ import { ChartSchema, ChartTypeSchema, DEFAULT_COMMENT_NAME, DEFAULT_FEEDBACK_NAME, MASTRA_ROUTES, MLFLOW_TRACE_ID_HEADER, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, MastraFeedbackRequestSchema, MastraUpdateThreadRequestSchema, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
2
  import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, ModelClass as ModelClass$1, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
3
3
  import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils } from "@dbx-tools/shared";
4
4
  import { Agent } from "@mastra/core/agent";
@@ -226,8 +226,8 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
226
226
  *
227
227
  * Safe to call from any hot path: {@link commonUtils.memoize} ensures
228
228
  * the wrapper is installed at most once per process, so subsequent
229
- * calls collapse to a single cached promise even when
230
- * {@link buildModel} fires on every agent step.
229
+ * calls are a no-op even when {@link buildModel} fires on every agent
230
+ * step.
231
231
  */
232
232
  const setupFetchInterceptor = commonUtils.memoize(() => {
233
233
  const log = logUtils.logger("mastra/llm");
@@ -849,8 +849,63 @@ function buildRenderDataTool(config) {
849
849
  });
850
850
  }
851
851
 
852
+ //#endregion
853
+ //#region packages/appkit-mastra/src/rest.ts
854
+ /**
855
+ * Resolve the workspace host + an authenticated header set off the
856
+ * client and issue a `fetch` against `path` (mounted on the host).
857
+ * Runs as whatever identity the client carries - the per-request OBO
858
+ * user when called from a request scope, the service principal
859
+ * otherwise.
860
+ */
861
+ async function databricksFetch(client, path, init) {
862
+ const host = (await client.config.getHost()).toString();
863
+ const headers = new Headers({
864
+ "Content-Type": "application/json",
865
+ ...init.headers
866
+ });
867
+ await client.config.authenticate(headers);
868
+ const url = new URL(path, host).toString();
869
+ return fetch(url, {
870
+ method: init.method,
871
+ headers,
872
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
873
+ ...init.signal ? { signal: init.signal } : {}
874
+ });
875
+ }
876
+ /** Read a response body as text, swallowing read errors (returns `""`). */
877
+ async function readResponseText(res) {
878
+ try {
879
+ return await res.text();
880
+ } catch {
881
+ return "";
882
+ }
883
+ }
884
+ /** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
885
+ async function readResponseJson(res) {
886
+ const text = await readResponseText(res);
887
+ if (!text) return {};
888
+ try {
889
+ return JSON.parse(text);
890
+ } catch {
891
+ return {};
892
+ }
893
+ }
894
+
852
895
  //#endregion
853
896
  //#region packages/appkit-mastra/src/connectors/managed-memory/client.ts
897
+ /**
898
+ * Thin REST client over the Databricks Unity Catalog Managed Agent
899
+ * Memory API (Beta). There is no TS SDK for memory-stores yet, so this
900
+ * issues authenticated requests through the shared {@link databricksFetch}
901
+ * primitive (`rest.ts`) as whatever identity the caller's client carries
902
+ * - the per-request OBO user for tools / recall, the service principal
903
+ * for setup-time probes.
904
+ *
905
+ * Responses are parsed defensively: the Beta wire shape may still drift,
906
+ * so the search parser tolerates missing / renamed entry fields and the
907
+ * store probe maps a 404 to `null` rather than throwing.
908
+ */
854
909
  /** Base path for the Unity Catalog memory-stores REST surface. */
855
910
  const MEMORY_STORES_PATH = "/api/2.1/unity-catalog/memory-stores";
856
911
  /**
@@ -883,32 +938,14 @@ function parseStoreName(fullName) {
883
938
  };
884
939
  }
885
940
  /**
886
- * Resolve the workspace host and an authenticated header set off the
887
- * client, then issue a `fetch`. Returns the raw `Response` so callers
888
- * decide how to handle status codes (the store probe wants the 404, the
889
- * mutating calls want a throw).
890
- */
891
- async function rawFetch(client, path, init) {
892
- const host = (await client.config.getHost()).toString();
893
- const headers = new Headers({ "Content-Type": "application/json" });
894
- await client.config.authenticate(headers);
895
- const url = new URL(path, host).toString();
896
- return fetch(url, {
897
- method: init.method,
898
- headers,
899
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
900
- ...init.signal ? { signal: init.signal } : {}
901
- });
902
- }
903
- /**
904
941
  * Issue a request and parse the JSON body, throwing
905
942
  * {@link ManagedMemoryError} on any non-2xx status. Used by the calls
906
943
  * where anything but success is a real error (create / write / search).
907
944
  */
908
945
  async function requestJson(client, path, init) {
909
- const res = await rawFetch(client, path, init);
910
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText$1(res));
911
- return safeJson$1(res);
946
+ const res = await databricksFetch(client, path, init);
947
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
948
+ return readResponseJson(res);
912
949
  }
913
950
  /**
914
951
  * Fetch a store by full name. Returns the raw store payload when it
@@ -917,13 +954,13 @@ async function requestJson(client, path, init) {
917
954
  * Any other non-2xx status throws.
918
955
  */
919
956
  async function getStore(client, fullName, signal) {
920
- const res = await rawFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
957
+ const res = await databricksFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
921
958
  method: "GET",
922
959
  ...signal ? { signal } : {}
923
960
  });
924
961
  if (res.status === 404) return null;
925
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText$1(res));
926
- return safeJson$1(res);
962
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
963
+ return readResponseJson(res);
927
964
  }
928
965
  /**
929
966
  * Probe for the store and create it when missing. Returns `true` when
@@ -990,10 +1027,14 @@ function parseEntries(body) {
990
1027
  for (const raw of list) {
991
1028
  if (!raw || typeof raw !== "object") continue;
992
1029
  const obj = raw;
993
- const contents = firstString(obj["contents"], obj["content"], obj["text"]);
1030
+ const contents = stringUtils.firstNonEmpty([
1031
+ obj["contents"],
1032
+ obj["content"],
1033
+ obj["text"]
1034
+ ]);
994
1035
  if (!contents) continue;
995
- const path = firstString(obj["path"]);
996
- const description = firstString(obj["description"], obj["summary"]);
1036
+ const path = stringUtils.firstNonEmpty(obj["path"]);
1037
+ const description = stringUtils.firstNonEmpty([obj["description"], obj["summary"]]);
997
1038
  const score = typeof obj["score"] === "number" ? obj["score"] : void 0;
998
1039
  out.push({
999
1040
  contents,
@@ -1004,28 +1045,6 @@ function parseEntries(body) {
1004
1045
  }
1005
1046
  return out;
1006
1047
  }
1007
- /** Return the first argument that is a non-empty string, else undefined. */
1008
- function firstString(...values) {
1009
- for (const v of values) if (typeof v === "string" && v.trim() !== "") return v;
1010
- }
1011
- /** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
1012
- async function safeJson$1(res) {
1013
- const text = await safeText$1(res);
1014
- if (!text) return {};
1015
- try {
1016
- return JSON.parse(text);
1017
- } catch {
1018
- return {};
1019
- }
1020
- }
1021
- /** Read a response body as text, swallowing read errors. */
1022
- async function safeText$1(res) {
1023
- try {
1024
- return await res.text();
1025
- } catch {
1026
- return "";
1027
- }
1028
- }
1029
1048
 
1030
1049
  //#endregion
1031
1050
  //#region packages/appkit-mastra/src/connectors/managed-memory/context.ts
@@ -2898,6 +2917,27 @@ function buildMcpServer(opts) {
2898
2917
  };
2899
2918
  }
2900
2919
 
2920
+ //#endregion
2921
+ //#region packages/appkit-mastra/src/pagination.ts
2922
+ /** Coerce / clamp a `perPage` value, falling back to `bounds.fallback`. */
2923
+ function clampPerPage(value, bounds) {
2924
+ if (value === void 0 || Number.isNaN(value)) return bounds.fallback;
2925
+ const n = Math.trunc(value);
2926
+ if (n <= 0) return bounds.fallback;
2927
+ return Math.min(n, bounds.max);
2928
+ }
2929
+ /**
2930
+ * Coerce a Hono query value into a non-negative integer. Returns
2931
+ * `undefined` for empty / non-numeric / negative inputs so the caller
2932
+ * can apply its built-in defaults.
2933
+ */
2934
+ function parseIntParam(value) {
2935
+ if (!value) return void 0;
2936
+ const n = Number(value);
2937
+ if (!Number.isFinite(n) || n < 0) return void 0;
2938
+ return Math.trunc(n);
2939
+ }
2940
+
2901
2941
  //#endregion
2902
2942
  //#region packages/appkit-mastra/src/history.ts
2903
2943
  const log$4 = logUtils.logger("mastra/history");
@@ -2921,7 +2961,10 @@ const MAX_PER_PAGE$1 = 200;
2921
2961
  * without sorting locally.
2922
2962
  */
2923
2963
  async function loadHistory(opts) {
2924
- const perPage = clampPerPage$1(opts.perPage);
2964
+ const perPage = clampPerPage(opts.perPage, {
2965
+ fallback: DEFAULT_PER_PAGE$1,
2966
+ max: MAX_PER_PAGE$1
2967
+ });
2925
2968
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
2926
2969
  const memory = await opts.agent.getMemory();
2927
2970
  if (!memory) {
@@ -3070,8 +3113,8 @@ function historyRoute(options) {
3070
3113
  agent: ctx.agent,
3071
3114
  threadId: ctx.threadId,
3072
3115
  ...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
3073
- page: parseIntParam$1(c.req.query("page")),
3074
- perPage: parseIntParam$1(c.req.query("perPage"))
3116
+ page: parseIntParam(c.req.query("page")),
3117
+ perPage: parseIntParam(c.req.query("perPage"))
3075
3118
  });
3076
3119
  return c.json(payload);
3077
3120
  }
@@ -3094,13 +3137,6 @@ function historyRoute(options) {
3094
3137
  }
3095
3138
  })];
3096
3139
  }
3097
- /** Coerce / clamp `perPage`; falls back to the page-size default. */
3098
- function clampPerPage$1(value) {
3099
- if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE$1;
3100
- const n = Math.trunc(value);
3101
- if (n <= 0) return DEFAULT_PER_PAGE$1;
3102
- return Math.min(n, MAX_PER_PAGE$1);
3103
- }
3104
3140
  /**
3105
3141
  * Sort messages oldest-first by `createdAt`, falling back to whatever
3106
3142
  * order the storage returned them in. The native `recall` call honors
@@ -3120,155 +3156,6 @@ function toEpoch(value) {
3120
3156
  }
3121
3157
  return 0;
3122
3158
  }
3123
- /**
3124
- * Coerce a Hono query value into a non-negative integer. Returns
3125
- * `undefined` for empty / non-numeric / negative inputs so
3126
- * {@link loadHistory} can apply its built-in defaults.
3127
- */
3128
- function parseIntParam$1(value) {
3129
- if (!value) return void 0;
3130
- const n = Number(value);
3131
- if (!Number.isFinite(n) || n < 0) return void 0;
3132
- return Math.trunc(n);
3133
- }
3134
-
3135
- //#endregion
3136
- //#region packages/appkit-mastra/src/rest.ts
3137
- /**
3138
- * Resolve the workspace host + an authenticated header set off the
3139
- * client and issue a `fetch` against `path` (mounted on the host).
3140
- * Runs as whatever identity the client carries - the per-request OBO
3141
- * user when called from a request scope, the service principal
3142
- * otherwise.
3143
- */
3144
- async function databricksFetch(client, path, init) {
3145
- const host = (await client.config.getHost()).toString();
3146
- const headers = new Headers({
3147
- "Content-Type": "application/json",
3148
- ...init.headers
3149
- });
3150
- await client.config.authenticate(headers);
3151
- const url = new URL(path, host).toString();
3152
- return fetch(url, {
3153
- method: init.method,
3154
- headers,
3155
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
3156
- ...init.signal ? { signal: init.signal } : {}
3157
- });
3158
- }
3159
-
3160
- //#endregion
3161
- //#region packages/appkit-mastra/src/mlflow.ts
3162
- /**
3163
- * MLflow user-feedback logging: detect whether MLflow tracing is wired
3164
- * for this deployment, and log a thumbs / comment as a trace
3165
- * *assessment* via the Databricks MLflow REST API.
3166
- *
3167
- * Feedback attaches to a trace, and the plugin's spans reach MLflow
3168
- * through the same OTel pipeline as every other AppKit span (see
3169
- * `observability.ts`). MLflow derives its trace id from the OpenTelemetry
3170
- * trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
3171
- * trace id on each turn's response and the client sends it back here.
3172
- *
3173
- * There is no MLflow JS SDK, so this posts to the assessments REST
3174
- * endpoint directly using the OBO-scoped workspace client (the feedback
3175
- * is thus attributed to the signed-in user). Trace export is
3176
- * asynchronous, so the just-finished trace may not exist in MLflow yet
3177
- * when the user reacts; the log call retries briefly on "not found"
3178
- * before giving up softly.
3179
- */
3180
- const log$3 = logUtils.logger("mastra/mlflow");
3181
- /** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
3182
- const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
3183
- /** Number of times to retry a "trace not found" response before giving up. */
3184
- const NOT_FOUND_RETRIES = 3;
3185
- /** Base backoff between "trace not found" retries, in ms (grows linearly). */
3186
- const NOT_FOUND_BACKOFF_MS = 1200;
3187
- /**
3188
- * Whether MLflow feedback logging is available for this deployment.
3189
- *
3190
- * Enabled when an OTLP exporter endpoint is configured (traces are
3191
- * actually shipped somewhere) AND an MLflow experiment is named - the
3192
- * two signals that the OTLP backend is MLflow and traces will
3193
- * materialize there. Both are standard env vars, so no plugin config is
3194
- * required; a deployment opts in simply by wiring MLflow tracing.
3195
- */
3196
- function mlflowEnabled() {
3197
- const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
3198
- const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
3199
- return hasExporter && hasExperiment;
3200
- }
3201
- /**
3202
- * Log a HUMAN feedback assessment to a trace. Returns the created
3203
- * assessment id on success, or `undefined` when the trace can't be
3204
- * found (even after retrying for export lag) or the request otherwise
3205
- * fails - callers surface that as a soft "not recorded" rather than an
3206
- * error, keeping the chat usable.
3207
- */
3208
- async function logFeedback(client, params) {
3209
- const hasValue = params.value !== void 0;
3210
- const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
3211
- const value = hasValue ? params.value : params.comment;
3212
- const body = { assessment: {
3213
- trace_id: params.traceId,
3214
- assessment_name: name,
3215
- source: {
3216
- source_type: "HUMAN",
3217
- source_id: params.sourceId?.trim() || "user"
3218
- },
3219
- feedback: { value },
3220
- ...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
3221
- } };
3222
- for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
3223
- let res;
3224
- try {
3225
- res = await databricksFetch(client, assessmentsPath(params.traceId), {
3226
- method: "POST",
3227
- body
3228
- });
3229
- } catch (err) {
3230
- log$3.warn("feedback request failed", {
3231
- traceId: params.traceId,
3232
- error: commonUtils.errorMessage(err)
3233
- });
3234
- return;
3235
- }
3236
- if (res.ok) {
3237
- const parsed = await safeJson(res);
3238
- const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
3239
- return typeof assessmentId === "string" ? assessmentId : "";
3240
- }
3241
- if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
3242
- await delay(NOT_FOUND_BACKOFF_MS * (attempt + 1));
3243
- continue;
3244
- }
3245
- log$3.warn("feedback not recorded", {
3246
- traceId: params.traceId,
3247
- status: res.status,
3248
- body: await safeText(res)
3249
- });
3250
- return;
3251
- }
3252
- }
3253
- function delay(ms) {
3254
- return new Promise((resolve) => setTimeout(resolve, ms));
3255
- }
3256
- async function safeJson(res) {
3257
- const text = await safeText(res);
3258
- if (!text) return {};
3259
- try {
3260
- return JSON.parse(text);
3261
- } catch {
3262
- return {};
3263
- }
3264
- }
3265
- async function safeText(res) {
3266
- try {
3267
- return await res.text();
3268
- } catch {
3269
- return "";
3270
- }
3271
- }
3272
3159
 
3273
3160
  //#endregion
3274
3161
  //#region packages/appkit-mastra/src/memory.ts
@@ -3300,7 +3187,7 @@ async function safeText(res) {
3300
3187
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
3301
3188
  * is registered); per-agent settings cascade on top of that.
3302
3189
  */
3303
- const log$2 = logUtils.logger("mastra/memory");
3190
+ const log$3 = logUtils.logger("mastra/memory");
3304
3191
  /**
3305
3192
  * Build a dedicated **service-principal** Lakebase pool for Mastra
3306
3193
  * memory from the lakebase plugin's resolved SP pg config.
@@ -3392,10 +3279,10 @@ var MemoryBuilder = class {
3392
3279
  const storage = this.buildStorage(agentId, storageSetting);
3393
3280
  const vector = this.buildVector(memorySetting);
3394
3281
  if (!storage && !vector) {
3395
- log$2.debug("agent:stateless", { agentId });
3282
+ log$3.debug("agent:stateless", { agentId });
3396
3283
  return;
3397
3284
  }
3398
- log$2.debug("agent:configured", {
3285
+ log$3.debug("agent:configured", {
3399
3286
  agentId,
3400
3287
  storage: storage !== void 0,
3401
3288
  vector: vector !== void 0,
@@ -3507,6 +3394,110 @@ function withId(value, fallback) {
3507
3394
  };
3508
3395
  }
3509
3396
 
3397
+ //#endregion
3398
+ //#region packages/appkit-mastra/src/mlflow.ts
3399
+ /**
3400
+ * MLflow user-feedback logging: detect whether MLflow tracing is wired
3401
+ * for this deployment, and log a thumbs / comment as a trace
3402
+ * *assessment* via the Databricks MLflow REST API.
3403
+ *
3404
+ * Feedback attaches to a trace, and the plugin's spans reach MLflow
3405
+ * through the same OTel pipeline as every other AppKit span (see
3406
+ * `observability.ts`). MLflow derives its trace id from the OpenTelemetry
3407
+ * trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
3408
+ * trace id on each turn's response and the client sends it back here.
3409
+ *
3410
+ * There is no MLflow JS SDK, so this posts to the assessments REST
3411
+ * endpoint directly using the OBO-scoped workspace client (the feedback
3412
+ * is thus attributed to the signed-in user). Trace export is
3413
+ * asynchronous, so the just-finished trace may not exist in MLflow yet
3414
+ * when the user reacts; the log call retries briefly on "not found"
3415
+ * before giving up softly.
3416
+ */
3417
+ const log$2 = logUtils.logger("mastra/mlflow");
3418
+ /** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
3419
+ const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
3420
+ /** Number of times to retry a "trace not found" response before giving up. */
3421
+ const NOT_FOUND_RETRIES = 3;
3422
+ /** Base backoff between "trace not found" retries, in ms (grows linearly). */
3423
+ const NOT_FOUND_BACKOFF_MS = 1200;
3424
+ /**
3425
+ * Whether MLflow feedback logging is available for this deployment.
3426
+ *
3427
+ * Enabled when an OTLP exporter endpoint is configured (traces are
3428
+ * actually shipped somewhere) AND an MLflow experiment is named - the
3429
+ * two signals that the OTLP backend is MLflow and traces will
3430
+ * materialize there. Both are standard env vars, so no plugin config is
3431
+ * required; a deployment opts in simply by wiring MLflow tracing.
3432
+ */
3433
+ function mlflowEnabled() {
3434
+ const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
3435
+ const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
3436
+ return hasExporter && hasExperiment;
3437
+ }
3438
+ /**
3439
+ * Resolve whether user feedback is enabled from the plugin's optional
3440
+ * `config.feedback` override: the explicit boolean wins, otherwise fall
3441
+ * back to auto-detecting MLflow tracing ({@link mlflowEnabled}). Shared
3442
+ * by the plugin's client-config gate and the server's trace-id header so
3443
+ * the two never disagree.
3444
+ */
3445
+ function resolveFeedbackEnabled(explicit) {
3446
+ return explicit ?? mlflowEnabled();
3447
+ }
3448
+ /**
3449
+ * Log a HUMAN feedback assessment to a trace. Returns the created
3450
+ * assessment id on success, or `undefined` when the trace can't be
3451
+ * found (even after retrying for export lag) or the request otherwise
3452
+ * fails - callers surface that as a soft "not recorded" rather than an
3453
+ * error, keeping the chat usable.
3454
+ */
3455
+ async function logFeedback(client, params) {
3456
+ const hasValue = params.value !== void 0;
3457
+ const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
3458
+ const value = hasValue ? params.value : params.comment;
3459
+ const body = { assessment: {
3460
+ trace_id: params.traceId,
3461
+ assessment_name: name,
3462
+ source: {
3463
+ source_type: "HUMAN",
3464
+ source_id: params.sourceId?.trim() || "user"
3465
+ },
3466
+ feedback: { value },
3467
+ ...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
3468
+ } };
3469
+ for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
3470
+ let res;
3471
+ try {
3472
+ res = await databricksFetch(client, assessmentsPath(params.traceId), {
3473
+ method: "POST",
3474
+ body
3475
+ });
3476
+ } catch (err) {
3477
+ log$2.warn("feedback request failed", {
3478
+ traceId: params.traceId,
3479
+ error: commonUtils.errorMessage(err)
3480
+ });
3481
+ return;
3482
+ }
3483
+ if (res.ok) {
3484
+ const parsed = await readResponseJson(res);
3485
+ const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
3486
+ return typeof assessmentId === "string" ? assessmentId : "";
3487
+ }
3488
+ if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
3489
+ await commonUtils.sleep(NOT_FOUND_BACKOFF_MS * (attempt + 1));
3490
+ continue;
3491
+ }
3492
+ log$2.warn("feedback not recorded", {
3493
+ traceId: params.traceId,
3494
+ status: res.status,
3495
+ body: await readResponseText(res)
3496
+ });
3497
+ return;
3498
+ }
3499
+ }
3500
+
3510
3501
  //#endregion
3511
3502
  //#region packages/appkit-mastra/src/observability.ts
3512
3503
  /**
@@ -3581,30 +3572,29 @@ async function buildObservability(options) {
3581
3572
  * behind an Express mount point.
3582
3573
  */
3583
3574
  /**
3584
- * `@mastra/express` subclass that stamps `RequestContext` with the
3585
- * AppKit user, resource id, and a thread id backed by an HTTP-only
3586
- * session cookie (`appkit_<plugin-name>_session_id`).
3587
- */
3588
- /**
3589
3575
  * OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
3590
3576
  * `trace.getActiveSpan()` returns a non-recording span with this id
3591
3577
  * when no SDK is registered, which must never be surfaced as a trace to
3592
3578
  * attach feedback to.
3593
3579
  */
3594
3580
  const INVALID_TRACE_ID = "0".repeat(32);
3581
+ /**
3582
+ * `@mastra/express` subclass that stamps `RequestContext` with the
3583
+ * AppKit user, resource id, and a thread id backed by an HTTP-only
3584
+ * session cookie (`appkit_<plugin-name>_session_id`).
3585
+ */
3595
3586
  var MastraServer$1 = class extends MastraServer {
3596
3587
  log;
3597
3588
  /**
3598
- * Whether to stamp the MLflow trace-id header on responses. Mirrors
3599
- * the plugin's feedback gate: the explicit `config.feedback` override
3600
- * wins, else auto-detect from the MLflow tracing env.
3589
+ * Whether to stamp the MLflow trace-id header on responses. Shares the
3590
+ * plugin's feedback gate via {@link resolveFeedbackEnabled}.
3601
3591
  */
3602
3592
  feedbackEnabled;
3603
3593
  constructor(config, ...args) {
3604
3594
  super(...args);
3605
3595
  this.config = config;
3606
3596
  this.log = logUtils.logger(config);
3607
- this.feedbackEnabled = config.feedback ?? mlflowEnabled();
3597
+ this.feedbackEnabled = resolveFeedbackEnabled(config.feedback);
3608
3598
  }
3609
3599
  registerAuthMiddleware() {
3610
3600
  super.registerAuthMiddleware();
@@ -3744,6 +3734,37 @@ var MastraServer$1 = class extends MastraServer {
3744
3734
  }
3745
3735
  };
3746
3736
  /**
3737
+ * Mount-relative agent inference paths (`/agents/:id/<verb>...`). Matched
3738
+ * by prefix on the verb so future variants (`streamVNext`, `stream/vnext`,
3739
+ * `generateVNext`) stay covered without a code change. These are the only
3740
+ * *writes* the browser client is allowed to make against stock Mastra.
3741
+ */
3742
+ const AGENT_INFERENCE = /^\/agents\/[^/]+\/(stream|generate|network)/i;
3743
+ /** Mount-relative read-only agent metadata (`/agents`, `/agents/:id`). */
3744
+ const AGENT_METADATA = /^\/agents(\/[^/]+)?$/;
3745
+ /**
3746
+ * Whether a request to the stock `@mastra/express` sub-app should be
3747
+ * dispatched, given the configured {@link MastraApiGateOptions.access}.
3748
+ *
3749
+ * `path` is mount-relative (what the plugin's catch-all sees, e.g.
3750
+ * `/agents/x/stream`, `/route/history/x`, `/mcp/...`). In `"scoped"`
3751
+ * mode the allowlist is deliberately tight - the chat client only ever
3752
+ * needs agent inference, read-only agent metadata, this plugin's own
3753
+ * OBO/resource-scoped `/route/*` routes, and (when enabled) MCP - so the
3754
+ * whole admin / mutating / bulk-export surface Mastra also exposes is
3755
+ * denied by default rather than enumerated.
3756
+ */
3757
+ function isMastraRequestAllowed(method, path, opts) {
3758
+ if (opts.access === "full") return true;
3759
+ const p = path.startsWith("/") ? path : `/${path}`;
3760
+ if (p === "/route" || p.startsWith("/route/")) return true;
3761
+ if (opts.mcpEnabled && (p === "/mcp" || p.startsWith("/mcp/"))) return true;
3762
+ const m = method.toUpperCase();
3763
+ if (m === "POST" && AGENT_INFERENCE.test(p)) return true;
3764
+ if (m === "GET" && AGENT_METADATA.test(p)) return true;
3765
+ return false;
3766
+ }
3767
+ /**
3747
3768
  * Patches around `@mastra/express`'s custom-route dispatcher so the
3748
3769
  * plugin's custom API routes (e.g. `historyRoute`) work when
3749
3770
  * `MastraServer` is hosted on an Express subapp mounted under a parent
@@ -3767,6 +3788,26 @@ function attachRoutePatchMiddleware(app) {
3767
3788
 
3768
3789
  //#endregion
3769
3790
  //#region packages/appkit-mastra/src/threads.ts
3791
+ /**
3792
+ * Conversation-thread listing exposed as a Mastra custom API route.
3793
+ *
3794
+ * Backed entirely by native Mastra: looks up the active agent by id,
3795
+ * asks its `Memory` instance to `listThreads` filtered to the caller's
3796
+ * resource, and shapes each into the JSON-safe {@link MastraThread}
3797
+ * wire type. A sibling `DELETE` removes a single named thread.
3798
+ *
3799
+ * A sibling `DELETE` removes a single named thread; a `PATCH` renames
3800
+ * one ({@link renameThread}).
3801
+ *
3802
+ * Like {@link historyRoute} this registers through Mastra's
3803
+ * `registerApiRoute` so it shares the `MastraServer` auth-middleware
3804
+ * pipeline (in `./server.ts`), which has already stamped the resource
3805
+ * id (`MASTRA_RESOURCE_ID_KEY`) and the targeted thread id
3806
+ * (`MASTRA_THREAD_ID_KEY`, resolved from the thread-selection header /
3807
+ * cookie) on `RequestContext` by the time a handler runs. Resource
3808
+ * scoping lives here so a caller can only ever see, rename, or delete
3809
+ * its own threads; no cookie or user lookups happen in this module.
3810
+ */
3770
3811
  const log = logUtils.logger("mastra/threads");
3771
3812
  /** Default threads page size. */
3772
3813
  const DEFAULT_PER_PAGE = 30;
@@ -3782,7 +3823,10 @@ const MAX_PER_PAGE = 200;
3782
3823
  * empty page so callers don't have to special-case stateless agents.
3783
3824
  */
3784
3825
  async function listThreads(opts) {
3785
- const perPage = clampPerPage(opts.perPage);
3826
+ const perPage = clampPerPage(opts.perPage, {
3827
+ fallback: DEFAULT_PER_PAGE,
3828
+ max: MAX_PER_PAGE
3829
+ });
3786
3830
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
3787
3831
  const memory = await opts.agent.getMemory();
3788
3832
  if (!memory) {
@@ -3858,7 +3902,47 @@ async function deleteThread(opts) {
3858
3902
  return { deleted: true };
3859
3903
  }
3860
3904
  /**
3861
- * Register the `<path>` Mastra custom API route. Handles two methods
3905
+ * Rename a single thread, returning the updated wire thread.
3906
+ *
3907
+ * Ownership is enforced the same way {@link deleteThread} enforces it:
3908
+ * the title is only changed when the thread belongs to the calling
3909
+ * resource, so a client can't rename another user's conversation by
3910
+ * guessing its id. Existing thread `metadata` is preserved untouched
3911
+ * (Mastra's `updateThread` replaces the row, so it must be passed back
3912
+ * in). Returns `null` when the thread doesn't exist, isn't owned by the
3913
+ * caller, or the agent has no memory configured, letting the route map
3914
+ * that to a 404.
3915
+ */
3916
+ async function renameThread(opts) {
3917
+ const memory = await opts.agent.getMemory();
3918
+ if (!memory) {
3919
+ log.debug("rename:no-memory", { agentId: opts.agent.id });
3920
+ return null;
3921
+ }
3922
+ const existing = await memory.getThreadById({ threadId: opts.threadId });
3923
+ if (!existing || existing.resourceId !== opts.resourceId) {
3924
+ log.debug("rename:not-owned", {
3925
+ agentId: opts.agent.id,
3926
+ threadId: opts.threadId,
3927
+ found: existing !== null
3928
+ });
3929
+ return null;
3930
+ }
3931
+ const startedAt = Date.now();
3932
+ const updated = await memory.updateThread({
3933
+ id: opts.threadId,
3934
+ title: opts.title,
3935
+ metadata: existing.metadata ?? {}
3936
+ });
3937
+ log.info("rename:done", {
3938
+ agentId: opts.agent.id,
3939
+ threadId: opts.threadId,
3940
+ elapsedMs: Date.now() - startedAt
3941
+ });
3942
+ return toWireThread(updated);
3943
+ }
3944
+ /**
3945
+ * Register the `<path>` Mastra custom API route. Handles three methods
3862
3946
  * on the same mount:
3863
3947
  *
3864
3948
  * - `GET`: a page of the resource's conversation threads
@@ -3868,6 +3952,10 @@ async function deleteThread(opts) {
3868
3952
  * `RequestContext` (the auth middleware resolves it the same way it
3869
3953
  * does for streaming and history), so the client deletes any of its
3870
3954
  * threads by stamping the target id - no separate path param.
3955
+ * - `PATCH`: rename the thread named by the thread-selection header /
3956
+ * `?threadId=` query to the `{ title }` in the JSON body
3957
+ * ({@link renameThread}). Targets a thread the same way `DELETE`
3958
+ * does; 404s when the thread isn't owned by the caller.
3871
3959
  *
3872
3960
  * Follows the `@mastra/ai-sdk` agent-binding convention: pass `agent`
3873
3961
  * for a fixed-agent mount, or include `:agentId` in the path for
@@ -3894,40 +3982,67 @@ function threadsRoute(options) {
3894
3982
  resourceId
3895
3983
  };
3896
3984
  };
3897
- return [registerApiRoute(path, {
3898
- method: "GET",
3899
- handler: async (c) => {
3900
- const ctx = resolveContext(c);
3901
- if ("error" in ctx) return ctx.error;
3902
- const payload = await listThreads({
3903
- agent: ctx.agent,
3904
- resourceId: ctx.resourceId,
3905
- page: parseIntParam(c.req.query("page")),
3906
- perPage: parseIntParam(c.req.query("perPage"))
3907
- });
3908
- return c.json(payload);
3909
- }
3910
- }), registerApiRoute(path, {
3911
- method: "DELETE",
3912
- handler: async (c) => {
3913
- const ctx = resolveContext(c);
3914
- if ("error" in ctx) return ctx.error;
3915
- const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
3916
- if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
3917
- const { deleted } = await deleteThread({
3918
- agent: ctx.agent,
3919
- threadId,
3920
- resourceId: ctx.resourceId
3921
- });
3922
- const payload = {
3923
- ok: true,
3924
- agentId: ctx.agentId,
3925
- threadId,
3926
- deleted
3927
- };
3928
- return c.json(payload);
3929
- }
3930
- })];
3985
+ return [
3986
+ registerApiRoute(path, {
3987
+ method: "GET",
3988
+ handler: async (c) => {
3989
+ const ctx = resolveContext(c);
3990
+ if ("error" in ctx) return ctx.error;
3991
+ const payload = await listThreads({
3992
+ agent: ctx.agent,
3993
+ resourceId: ctx.resourceId,
3994
+ page: parseIntParam(c.req.query("page")),
3995
+ perPage: parseIntParam(c.req.query("perPage"))
3996
+ });
3997
+ return c.json(payload);
3998
+ }
3999
+ }),
4000
+ registerApiRoute(path, {
4001
+ method: "DELETE",
4002
+ handler: async (c) => {
4003
+ const ctx = resolveContext(c);
4004
+ if ("error" in ctx) return ctx.error;
4005
+ const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
4006
+ if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
4007
+ const { deleted } = await deleteThread({
4008
+ agent: ctx.agent,
4009
+ threadId,
4010
+ resourceId: ctx.resourceId
4011
+ });
4012
+ const payload = {
4013
+ ok: true,
4014
+ agentId: ctx.agentId,
4015
+ threadId,
4016
+ deleted
4017
+ };
4018
+ return c.json(payload);
4019
+ }
4020
+ }),
4021
+ registerApiRoute(path, {
4022
+ method: "PATCH",
4023
+ handler: async (c) => {
4024
+ const ctx = resolveContext(c);
4025
+ if ("error" in ctx) return ctx.error;
4026
+ const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
4027
+ if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
4028
+ const body = MastraUpdateThreadRequestSchema.safeParse(await c.req.json());
4029
+ if (!body.success) return c.json({ error: body.error.message }, 400);
4030
+ const thread = await renameThread({
4031
+ agent: ctx.agent,
4032
+ threadId,
4033
+ resourceId: ctx.resourceId,
4034
+ title: body.data.title
4035
+ });
4036
+ if (!thread) return c.json({ error: `Unknown thread "${threadId}"` }, 404);
4037
+ const payload = {
4038
+ ok: true,
4039
+ agentId: ctx.agentId,
4040
+ thread
4041
+ };
4042
+ return c.json(payload);
4043
+ }
4044
+ })
4045
+ ];
3931
4046
  }
3932
4047
  /** Coerce a Mastra `StorageThreadType` into the JSON-safe wire shape. */
3933
4048
  function toWireThread(thread) {
@@ -3946,24 +4061,6 @@ function toIso(value) {
3946
4061
  const parsed = new Date(value);
3947
4062
  return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date(0)).toISOString() : parsed.toISOString();
3948
4063
  }
3949
- /** Coerce / clamp `perPage`; falls back to the page-size default. */
3950
- function clampPerPage(value) {
3951
- if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
3952
- const n = Math.trunc(value);
3953
- if (n <= 0) return DEFAULT_PER_PAGE;
3954
- return Math.min(n, MAX_PER_PAGE);
3955
- }
3956
- /**
3957
- * Coerce a Hono query value into a non-negative integer. Returns
3958
- * `undefined` for empty / non-numeric / negative inputs so
3959
- * {@link listThreads} can apply its built-in defaults.
3960
- */
3961
- function parseIntParam(value) {
3962
- if (!value) return void 0;
3963
- const n = Number(value);
3964
- if (!Number.isFinite(n) || n < 0) return void 0;
3965
- return Math.trunc(n);
3966
- }
3967
4064
 
3968
4065
  //#endregion
3969
4066
  //#region packages/appkit-mastra/src/plugin.ts
@@ -3994,8 +4091,13 @@ function parseIntParam(value) {
3994
4091
  * Mastra agent routes, the plugin registers `/route/history`
3995
4092
  * (load + clear a thread's messages), `/route/threads` (list the
3996
4093
  * caller's conversations + delete one), `/models`, `/suggestions`,
3997
- * and the generic `/embed/:type/:id` resolver for inline chart /
3998
- * data markers.
4094
+ * `/route/feedback` (log a thumbs / comment to MLflow when feedback
4095
+ * is enabled), and the generic `/embed/:type/:id` resolver for inline
4096
+ * chart / data markers. The stock `@mastra/express` surface is gated
4097
+ * by `config.apiAccess` (default `"scoped"`): only agent inference,
4098
+ * read-only agent metadata, the `/route/*` routes, and (when enabled)
4099
+ * MCP are dispatched to Mastra; admin / mutating / bulk-export routes
4100
+ * are refused with `403`. See {@link isMastraRequestAllowed}.
3999
4101
  * - MCP: opt in with `config.mcp` to expose the agents (and optionally
4000
4102
  * tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
4001
4103
  * instance via `mcpServers`, so `@mastra/express` serves the stock
@@ -4116,13 +4218,12 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4116
4218
  };
4117
4219
  }
4118
4220
  /**
4119
- * Whether user feedback can be logged to MLflow. The explicit
4120
- * `config.feedback` override wins; otherwise auto-detect from the
4121
- * MLflow tracing env ({@link mlflowEnabled}). Gates both the client
4122
- * config flag and the feedback route so the two never disagree.
4221
+ * Whether user feedback can be logged to MLflow. Delegates to
4222
+ * {@link resolveFeedbackEnabled} so the client-config flag and the
4223
+ * feedback route share the same gate as the server's trace-id header.
4123
4224
  */
4124
4225
  feedbackEnabled() {
4125
- return this.config.feedback ?? mlflowEnabled();
4226
+ return resolveFeedbackEnabled(this.config.feedback);
4126
4227
  }
4127
4228
  injectRoutes(router) {
4128
4229
  router.get(MASTRA_ROUTES.models, (req, res, next) => {
@@ -4196,6 +4297,13 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4196
4297
  });
4197
4298
  router.use((req, res, next) => {
4198
4299
  if (!this.mastraApp) return res.status(503).end();
4300
+ if (!isMastraRequestAllowed(req.method, req.path, {
4301
+ access: this.config.apiAccess ?? "scoped",
4302
+ mcpEnabled: Boolean(this.config.mcp)
4303
+ })) {
4304
+ res.status(403).json({ error: "Endpoint not exposed to the client (apiAccess=scoped)" });
4305
+ return;
4306
+ }
4199
4307
  return this.userScopedSelf(req).dispatchMastra(req, res, next);
4200
4308
  });
4201
4309
  }
@@ -4402,6 +4510,7 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4402
4510
  "/route/threads",
4403
4511
  "/models",
4404
4512
  "/suggestions",
4513
+ "/route/feedback",
4405
4514
  "/embed/:type/:id"
4406
4515
  ],
4407
4516
  instanceStorage: instanceStorage !== void 0,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dbx-tools/appkit-mastra",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "dependencies": {
5
5
  "@databricks/sdk-experimental": "^0.17",
6
- "@dbx-tools/appkit-mastra-shared": "0.1.74",
7
- "@dbx-tools/genie": "0.1.74",
8
- "@dbx-tools/genie-shared": "0.1.74",
9
- "@dbx-tools/model": "0.1.74",
10
- "@dbx-tools/shared": "0.1.74",
6
+ "@dbx-tools/appkit-mastra-shared": "0.1.76",
7
+ "@dbx-tools/genie": "0.1.76",
8
+ "@dbx-tools/genie-shared": "0.1.76",
9
+ "@dbx-tools/model": "0.1.76",
10
+ "@dbx-tools/shared": "0.1.76",
11
11
  "@mastra/ai-sdk": "^1",
12
12
  "@mastra/core": "^1",
13
13
  "@mastra/express": "^1",