@agent-inspect/studio 6.7.1 → 6.7.3

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
@@ -1,16 +1,20 @@
1
1
  # @agent-inspect/studio
2
2
 
3
- Self-hosted, read-only Studio analyzer for multi-project AgentInspect workspaces. Customer-owned — no maintainer cloud.
3
+ Customer-owned, read-only Studio analyzer for registered AgentInspect workspaces. **No maintainer-hosted cloud.**
4
+
5
+ **Support level:** Beta — see [SUPPORT-LEVELS.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/SUPPORT-LEVELS.md).
4
6
 
5
7
  ## When to use
6
8
 
7
- - Team-internal browser UI over multiple AgentInspect workspaces
8
- - Localhost or explicitly configured self-hosted binding
9
+ - Team-internal browser UI over one or more local workspaces
10
+ - Localhost review of projects, runs, search, and imports
11
+ - Explicit, opt-in ingest of files / GitHub artifacts / HTTP (all **disabled by default**)
9
12
 
10
13
  ## When not to use
11
14
 
12
15
  - Maintainer-hosted dashboards or SaaS
13
- - Trace mutation or ingest (use CLI writers; ingest is v6.1+)
16
+ - Production HA / Postgres / enterprise SSO (not supported)
17
+ - Trace mutation (Studio is read-only over registered workspaces)
14
18
 
15
19
  ## Install
16
20
 
@@ -18,17 +22,30 @@ Self-hosted, read-only Studio analyzer for multi-project AgentInspect workspaces
18
22
  npm install agent-inspect @agent-inspect/studio
19
23
  ```
20
24
 
21
- ## Example
25
+ Optional index: `@agent-inspect/index-sqlite` (Beta).
26
+
27
+ ## Two-minute start
22
28
 
23
29
  ```bash
24
30
  npx agent-inspect studio
31
+ # default: http://127.0.0.1:7340
25
32
  npx agent-inspect studio --port 7340 --host 127.0.0.1
26
33
  ```
27
34
 
28
- ## Privacy
35
+ Register workspaces via the Studio registry / CLI docs. SQLite is the current supported store.
36
+
37
+ ## Privacy and network
38
+
39
+ - Binds to **localhost** by default
40
+ - **No default upload** or hidden telemetry
41
+ - File-drop ingest: local only
42
+ - GitHub artifact import: **explicit**; uses your credentials → GitHub
43
+ - HTTP ingest: **disabled by default**; requires token + binding when enabled
44
+ - See [NETWORK-BEHAVIOR.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/NETWORK-BEHAVIOR.md) · [SELF-HOSTING.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/SELF-HOSTING.md)
45
+
46
+ ## Current pages (honest)
29
47
 
30
- - Binds to localhost by default; read-only GET routes
31
- - No default upload or telemetry
48
+ Useful HTML for projects, runs, and search. Sessions / suites / safety views may still be thinner than the underlying APIs (JSON-oriented). Treat Studio as Beta.
32
49
 
33
50
  ## API
34
51
 
@@ -38,9 +55,22 @@ npx agent-inspect studio --port 7340 --host 127.0.0.1
38
55
 
39
56
  `npx agent-inspect studio` (requires this package installed)
40
57
 
58
+ ## Limitations
59
+
60
+ - No Postgres mode
61
+ - Not a production APM
62
+ - External design-partner evidence still pending for broad adoption claims
63
+ - Docker Compose examples are customer-owned templates — verify before production use
64
+
41
65
  ## Docs
42
66
 
43
- - [SELF-HOSTED-STUDIO-V6.0.md](../../docs/proposals/SELF-HOSTED-STUDIO-V6.0.md)
67
+ - [SELF-HOSTING.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/SELF-HOSTING.md)
68
+ - [WORKSPACE.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/WORKSPACE.md)
69
+ - [PRE-V7-PILOT-KIT.md](https://github.com/rajudandigam/agent-inspect/blob/main/docs/PRE-V7-PILOT-KIT.md)
70
+
71
+ ## Version
72
+
73
+ Part of the fixed AgentInspect release line. See the npm badge / package manifest for the current version.
44
74
 
45
75
  ## License
46
76
 
package/dist/index.cjs CHANGED
@@ -862,9 +862,9 @@ var DEFAULT_HTTP_INGEST_BASE_PATH = "/api/ingest";
862
862
  var HTTP_INGEST_BUNDLE_PATH = "/api/ingest/bundle";
863
863
  var HTTP_INGEST_ARTIFACT_PATH = "/api/ingest/artifact";
864
864
  var DEFAULT_MAX_INGEST_BYTES = 52428800;
865
- function resolveHttpIngestConfig(options, registryEnabled) {
866
- const http = options.context?.registry.ingest?.http;
867
- const enabled = options.ingestHttp === true || registryEnabled === true || http?.enabled === true;
865
+ function resolveHttpIngestConfig(options, registryHttp) {
866
+ const http = registryHttp ?? options.context?.registry.ingest?.http;
867
+ const enabled = options.ingestHttp === true || http?.enabled === true;
868
868
  const basePath = (http?.path ?? DEFAULT_HTTP_INGEST_BASE_PATH).trim() || DEFAULT_HTTP_INGEST_BASE_PATH;
869
869
  const tokenEnv = resolveIngestTokenEnv({
870
870
  ...options.ingestTokenEnv !== void 0 ? { tokenEnv: options.ingestTokenEnv } : {},
@@ -897,7 +897,7 @@ function isHttpIngestRoute(pathname, config) {
897
897
  return pathname === HTTP_INGEST_BUNDLE_PATH || pathname === HTTP_INGEST_ARTIFACT_PATH || pathname === `${config.basePath}/bundle` || pathname === `${config.basePath}/artifact`;
898
898
  }
899
899
  async function handleHttpIngestRequest(req, res, ctx, options, pathname) {
900
- const config = resolveHttpIngestConfig(options, ctx.registry.ingest?.http?.enabled);
900
+ const config = resolveHttpIngestConfig(options, ctx.registry.ingest?.http);
901
901
  if (!isHttpIngestRoute(pathname, config)) return false;
902
902
  if (!config.enabled) {
903
903
  sendJson(res, 404, { error: "HTTP ingest is disabled" });
@@ -1344,8 +1344,6 @@ async function loadBundleExportView(ctx, params) {
1344
1344
  note: "Studio does not mutate traces or upload bundles. Run the CLI locally to assemble a share-safe bundle."
1345
1345
  };
1346
1346
  }
1347
-
1348
- // packages/studio/src/auth.ts
1349
1347
  function resolveStudioAuthMode(options) {
1350
1348
  return options.auth === "basic" ? "basic" : "none";
1351
1349
  }
@@ -1365,13 +1363,19 @@ function parseBasicAuth(header) {
1365
1363
  return void 0;
1366
1364
  }
1367
1365
  }
1366
+ function timingSafePasswordEqual(provided, expected) {
1367
+ const providedDigest = crypto.createHash("sha256").update(provided, "utf8").digest();
1368
+ const expectedDigest = crypto.createHash("sha256").update(expected, "utf8").digest();
1369
+ return crypto.timingSafeEqual(providedDigest, expectedDigest);
1370
+ }
1368
1371
  function isStudioRequestAuthorized(req, options) {
1369
1372
  const mode = resolveStudioAuthMode(options);
1370
1373
  if (mode === "none") return true;
1371
1374
  const expected = resolveStudioPassword(options);
1372
1375
  if (!expected) return false;
1373
1376
  const provided = parseBasicAuth(req.headers.authorization);
1374
- return provided === expected;
1377
+ if (provided === void 0) return false;
1378
+ return timingSafePasswordEqual(provided, expected);
1375
1379
  }
1376
1380
  function studioAuthRequiredResponse() {
1377
1381
  return {
@@ -1616,7 +1620,7 @@ function createStudioServer(options = {}) {
1616
1620
  contextPromise = createStudioContext(options);
1617
1621
  }
1618
1622
  const ctx = await contextPromise;
1619
- const httpConfig = resolveHttpIngestConfig(options, ctx.registry.ingest?.http?.enabled);
1623
+ const httpConfig = resolveHttpIngestConfig(options, ctx.registry.ingest?.http);
1620
1624
  if (isHttpIngestRoute(pathname, httpConfig)) {
1621
1625
  const handled2 = await handleHttpIngestRequest(req, res, ctx, options, pathname);
1622
1626
  if (handled2) return;
@@ -1895,6 +1899,28 @@ async function validateBundleDirectory(bundleDir) {
1895
1899
  }
1896
1900
  return errors;
1897
1901
  }
1902
+ async function hashBundleContents(bundleDir) {
1903
+ const hash = crypto.createHash("sha256");
1904
+ const walk = async (dir, relPrefix) => {
1905
+ const entries = (await promises.readdir(dir, { withFileTypes: true })).sort(
1906
+ (a, b) => a.name.localeCompare(b.name)
1907
+ );
1908
+ for (const entry of entries) {
1909
+ const abs = path8__default.default.join(dir, entry.name);
1910
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
1911
+ if (entry.isDirectory()) {
1912
+ await walk(abs, rel);
1913
+ } else if (entry.isFile()) {
1914
+ hash.update(rel);
1915
+ hash.update("\0");
1916
+ hash.update(await promises.readFile(abs));
1917
+ hash.update("\0");
1918
+ }
1919
+ }
1920
+ };
1921
+ await walk(bundleDir, "");
1922
+ return hash.digest("hex");
1923
+ }
1898
1924
  async function copyDirectoryRecursive(sourceDir, destDir) {
1899
1925
  await promises.mkdir(destDir, { recursive: true });
1900
1926
  const entries = await promises.readdir(sourceDir, { withFileTypes: true });
@@ -1949,8 +1975,7 @@ async function importBundleUpload(options) {
1949
1975
  registryImportWarnings
1950
1976
  };
1951
1977
  }
1952
- const metadataRaw = await promises.readFile(path8__default.default.join(bundlePath, "metadata.json"), "utf8");
1953
- const contentHash = crypto.createHash("sha256").update(metadataRaw).digest("hex");
1978
+ const contentHash = await hashBundleContents(bundlePath);
1954
1979
  const sourceKey = `bundle:${bundlePath}`;
1955
1980
  const existing = findIngestFileBySourceKey(options.db, sourceKey);
1956
1981
  if (existing && existing.contentHash === contentHash) {