@anvia/studio 1.3.18 → 1.3.19

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
@@ -37,8 +37,17 @@ await new Studio([agent]).serve({ port: 4021 });
37
37
  - Knowledge-graph exploration and browser sandbox viewing with human takeover.
38
38
  - Optional SQLite session storage and graceful server shutdown.
39
39
 
40
+ The knowledge inspector returns HTTP 400 `bad_request` with message `Invalid cursor` for invalid
41
+ static-context cursors or when a dynamic context/tool store raises `VectorInspectCursorError`.
42
+
40
43
  Session storage is in memory by default; configure SQLite to persist it across restarts. Studio is intended for local development. Add application authentication and authorization when exposing it remotely.
41
44
 
45
+ Studio rejects HTTP requests with an untrusted Host or Origin hostname. Loopback hosts are
46
+ trusted by default, as is an explicitly configured non-wildcard bind `hostname`. When binding
47
+ to `0.0.0.0` or `::`, set `allowedHosts` to the hostnames clients use, for example
48
+ `serve({ hostname: "0.0.0.0", allowedHosts: ["studio.lan"] })`. This allowlist also applies to
49
+ Origin hosts; ports are ignored. See the [serving guide](../../docs/packages/tool-studio.md#usage).
50
+
42
51
  ## Learn more
43
52
 
44
53
  - [Usage guide](https://github.com/anvia-hq/anvia/blob/main/docs/packages/tool-studio.md)
package/dist/index.d.ts CHANGED
@@ -914,6 +914,13 @@ type StudioOptions = {
914
914
  type StudioServeOptions = {
915
915
  port?: number;
916
916
  hostname?: string;
917
+ /**
918
+ * Extra Host header hostnames (and Origin hosts) trusted by the Studio HTTP
919
+ * API, in addition to the loopback set and an explicitly configured
920
+ * non-wildcard `hostname`. Required when binding to 0.0.0.0 (or ::): list
921
+ * every hostname clients use to reach Studio, for example `["studio.lan"]`.
922
+ */
923
+ allowedHosts?: readonly string[];
917
924
  log?: boolean;
918
925
  handleSignals?: boolean;
919
926
  shutdownTimeoutMs?: number;
package/dist/index.js CHANGED
@@ -1007,6 +1007,61 @@ function optionalStringArray(value) {
1007
1007
  }
1008
1008
 
1009
1009
  // src/runtime/http.ts
1010
+ var STUDIO_LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
1011
+ function normalizeHostname(value) {
1012
+ return value.trim().toLowerCase();
1013
+ }
1014
+ function resolveStudioAllowedHostnames(options) {
1015
+ const allowed = new Set(STUDIO_LOCAL_HOSTNAMES);
1016
+ for (const host of options?.allowedHosts ?? []) {
1017
+ const normalized = normalizeHostname(host);
1018
+ if (normalized !== "") allowed.add(normalized);
1019
+ }
1020
+ const bound = normalizeHostname(options?.hostname ?? "");
1021
+ if (bound !== "" && bound !== "0.0.0.0" && bound !== "::" && !allowed.has(bound)) {
1022
+ allowed.add(bound);
1023
+ if (bound.includes(":")) allowed.add(`[${bound}]`);
1024
+ }
1025
+ return allowed;
1026
+ }
1027
+ function isAllowedHostname(hostname3, allowed) {
1028
+ return hostname3 !== void 0 && allowed.has(hostname3.trim().toLowerCase());
1029
+ }
1030
+ function createStudioHostGuard(options) {
1031
+ const allowed = resolveStudioAllowedHostnames(options);
1032
+ return async function studioHostGuard(c, next) {
1033
+ const host = c.req.header("host");
1034
+ if (host !== void 0 && !isAllowedHostname(studioRequestHostname(host), allowed)) {
1035
+ return errorResponse(c, 403, "forbidden", "Host not allowed");
1036
+ }
1037
+ const origin = c.req.header("origin");
1038
+ if (origin !== void 0 && !isAllowedOrigin(origin, allowed)) {
1039
+ return errorResponse(c, 403, "forbidden", "Origin not allowed");
1040
+ }
1041
+ return next();
1042
+ };
1043
+ }
1044
+ function studioRequestHostname(hostHeader) {
1045
+ if (hostHeader === void 0) return void 0;
1046
+ const host = hostHeader.trim().toLowerCase();
1047
+ if (host.startsWith("[")) {
1048
+ const end = host.indexOf("]");
1049
+ if (end === -1) return void 0;
1050
+ return host.slice(0, end + 1);
1051
+ }
1052
+ const colon = host.lastIndexOf(":");
1053
+ if (colon !== -1 && host.indexOf(":") === colon) return host.slice(0, colon);
1054
+ return host;
1055
+ }
1056
+ function isAllowedOrigin(origin, allowed) {
1057
+ let url2;
1058
+ try {
1059
+ url2 = new URL(origin);
1060
+ } catch {
1061
+ return false;
1062
+ }
1063
+ return isAllowedHostname(url2.hostname, allowed);
1064
+ }
1010
1065
  function errorResponse(c, status, code, message, details) {
1011
1066
  const body = {
1012
1067
  error: {
@@ -23773,6 +23828,7 @@ function registerTeamRoutes(app, teams, lifecycle) {
23773
23828
 
23774
23829
  // src/runtime/knowledge.ts
23775
23830
  import { getAgentToolState } from "@anvia/core/internal/agent";
23831
+ import { parseVectorInspectCursor, VectorInspectCursorError } from "@anvia/core/vector-store";
23776
23832
 
23777
23833
  // src/runtime/query.ts
23778
23834
  function optionalQueryString(value) {
@@ -23841,6 +23897,9 @@ function registerKnowledgeRoutes(app, props) {
23841
23897
  if (page === void 0) {
23842
23898
  return errorResponse(c, 404, "not_found", "Knowledge source not found");
23843
23899
  }
23900
+ if (page === false) {
23901
+ return errorResponse(c, 400, "bad_request", "Invalid cursor");
23902
+ }
23844
23903
  return c.json(page);
23845
23904
  });
23846
23905
  }
@@ -23942,11 +24001,19 @@ async function knowledgeItemsPage(agent, sourceId, request) {
23942
24001
  if (inspect === void 0) {
23943
24002
  return nonInspectablePage(agent.id, sourceId, "dynamic_context");
23944
24003
  }
23945
- const page = await inspect({
23946
- limit: request.limit,
23947
- cursor: request.cursor,
23948
- filter: vectorContext.filter
23949
- });
24004
+ let page;
24005
+ try {
24006
+ page = await inspect({
24007
+ limit: request.limit,
24008
+ cursor: request.cursor,
24009
+ filter: vectorContext.filter
24010
+ });
24011
+ } catch (error62) {
24012
+ if (VectorInspectCursorError.isCursorError(error62)) {
24013
+ return false;
24014
+ }
24015
+ throw error62;
24016
+ }
23950
24017
  const result = {
23951
24018
  agentId: agent.id,
23952
24019
  sourceId,
@@ -23968,11 +24035,19 @@ async function knowledgeItemsPage(agent, sourceId, request) {
23968
24035
  if (inspect === void 0) {
23969
24036
  return nonInspectablePage(agent.id, sourceId, "dynamic_tools");
23970
24037
  }
23971
- const page = await inspect({
23972
- limit: request.limit,
23973
- cursor: request.cursor,
23974
- filter: toolIndex.filter
23975
- });
24038
+ let page;
24039
+ try {
24040
+ page = await inspect({
24041
+ limit: request.limit,
24042
+ cursor: request.cursor,
24043
+ filter: toolIndex.filter
24044
+ });
24045
+ } catch (error62) {
24046
+ if (VectorInspectCursorError.isCursorError(error62)) {
24047
+ return false;
24048
+ }
24049
+ throw error62;
24050
+ }
23976
24051
  const result = {
23977
24052
  agentId: agent.id,
23978
24053
  sourceId,
@@ -23995,7 +24070,12 @@ function inspectFn(store) {
23995
24070
  }
23996
24071
  function staticKnowledgeItemsPage(agent, request) {
23997
24072
  const staticContext = staticContextDocuments(agent.agent);
23998
- const start = Math.max(0, Math.trunc(Number(request.cursor ?? "0")));
24073
+ let start;
24074
+ try {
24075
+ start = parseVectorInspectCursor(request.cursor);
24076
+ } catch {
24077
+ return false;
24078
+ }
23999
24079
  const page = staticContext.slice(start, start + request.limit);
24000
24080
  const nextOffset = start + page.length;
24001
24081
  const result = {
@@ -27174,7 +27254,12 @@ var Studio = class {
27174
27254
  }
27175
27255
  start(serveOptions = {}) {
27176
27256
  this.close();
27177
- this.studio = createStudioApp(this.options);
27257
+ const serveHostOptions = {};
27258
+ if (serveOptions.allowedHosts !== void 0) {
27259
+ serveHostOptions.allowedHosts = serveOptions.allowedHosts;
27260
+ }
27261
+ if (serveOptions.hostname !== void 0) serveHostOptions.hostname = serveOptions.hostname;
27262
+ this.studio = createStudioApp({ ...this.options, ...serveHostOptions });
27178
27263
  this.shutdownPromise = void 0;
27179
27264
  const port = serveOptions.port ?? Number(process.env.RUNNER_PORT ?? 4021);
27180
27265
  const serverOptions = {
@@ -27443,6 +27528,10 @@ function createStudioApp(options) {
27443
27528
  const sandboxRegistry = createStudioSandboxRegistry(agents, options.sandboxes ?? []);
27444
27529
  const memorySources = createStudioMemorySourceRegistry(agents, stores.sessions);
27445
27530
  const app = new HonoApp();
27531
+ const guardOptions = {};
27532
+ if (options.allowedHosts !== void 0) guardOptions.allowedHosts = options.allowedHosts;
27533
+ if (options.hostname !== void 0) guardOptions.hostname = options.hostname;
27534
+ app.use(createStudioHostGuard(guardOptions));
27446
27535
  const uiOptions = isStudioUiEnabled(options.ui) ? resolveStudioUiOptions(options.ui) : void 0;
27447
27536
  if (uiOptions !== void 0 && !uiOptions.protectShell) {
27448
27537
  registerStudioUi(app, uiOptions);