neotoma 0.8.0 → 0.9.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 (163) hide show
  1. package/README.md +2 -0
  2. package/dist/actions.d.ts.map +1 -1
  3. package/dist/actions.js +181 -30
  4. package/dist/actions.js.map +1 -1
  5. package/dist/cli/bootstrap.js +0 -0
  6. package/dist/cli/index.js +4 -4
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/triage.d.ts.map +1 -1
  9. package/dist/cli/triage.js +9 -0
  10. package/dist/cli/triage.js.map +1 -1
  11. package/dist/inspector/assets/Combination-CujEssa-.js +41 -0
  12. package/dist/inspector/assets/agent_badge-Cy2xdM19.js +1 -0
  13. package/dist/inspector/assets/agent_detail-34_3u913.js +1 -0
  14. package/dist/inspector/assets/agent_filter-9Y5V9TSY.js +1 -0
  15. package/dist/inspector/assets/agent_grant_detail-fs0DKTd5.js +1 -0
  16. package/dist/inspector/assets/agent_grant_form-DjoTByJj.js +1 -0
  17. package/dist/inspector/assets/agent_grants-DUtqBZkK.js +1 -0
  18. package/dist/inspector/assets/agents-CIUBwgRT.js +1 -0
  19. package/dist/inspector/assets/arrow-left-Bxer0ezV.js +6 -0
  20. package/dist/inspector/assets/attribution_card-C48p_bBB.js +1 -0
  21. package/dist/inspector/assets/attribution_summary-B7KAy0mn.js +1 -0
  22. package/dist/inspector/assets/card-Bz-JK2L1.js +1 -0
  23. package/dist/inspector/assets/check-DN6vjWJ7.js +6 -0
  24. package/dist/inspector/assets/checkbox-DuNHytV3.js +1 -0
  25. package/dist/inspector/assets/chevron-down-BGEwSYyI.js +6 -0
  26. package/dist/inspector/assets/chevron-right-D5OasTAw.js +6 -0
  27. package/dist/inspector/assets/compliance-B7gRLVKA.js +1 -0
  28. package/dist/inspector/assets/confirm-dialog-2InCKZky.js +6 -0
  29. package/dist/inspector/assets/copy_id_button-CoFHQieu.js +6 -0
  30. package/dist/inspector/assets/corrections-C-GqSum_.js +1 -0
  31. package/dist/inspector/assets/dashboard-CB0ynQ0H.js +73 -0
  32. package/dist/inspector/assets/data-table-Nyryp-JO.js +22 -0
  33. package/dist/inspector/assets/dialog-DS3fUAm1.js +10 -0
  34. package/dist/inspector/assets/dropdown-menu-Cypcul5-.js +6 -0
  35. package/dist/inspector/assets/entities-B1ZBrker.js +1 -0
  36. package/dist/inspector/assets/entity_detail-DlsbmdeN.js +17 -0
  37. package/dist/inspector/assets/entity_link-DujmD1Yr.js +1 -0
  38. package/dist/inspector/assets/external-link-C4V3VgZf.js +6 -0
  39. package/dist/inspector/assets/feedback-BQFBT-KP.js +35 -0
  40. package/dist/inspector/assets/graph_explorer-BZV40eAE.css +1 -0
  41. package/dist/inspector/assets/graph_explorer-Ckir-IQl.js +23 -0
  42. package/dist/inspector/assets/index-Bx2zn3Hd.js +1 -0
  43. package/dist/inspector/assets/index-ClljmJBP.js +199 -0
  44. package/dist/inspector/assets/index-Dqo226iA.js +1 -0
  45. package/dist/inspector/assets/index-NU89qCAK.css +1 -0
  46. package/dist/inspector/assets/index-ZmrnhI6W.js +1 -0
  47. package/dist/inspector/assets/interpretations-B7wb65mL.js +1 -0
  48. package/dist/inspector/assets/interpretations-CUaxEt1j.js +1 -0
  49. package/dist/inspector/assets/json_viewer-BkvSv8xS.js +1 -0
  50. package/dist/inspector/assets/label-CbDLvpxl.js +1 -0
  51. package/dist/inspector/assets/observations-lXPbAxMw.js +1 -0
  52. package/dist/inspector/assets/page_shell-_vozYBwF.js +1 -0
  53. package/dist/inspector/assets/pagination-aHI1LrUp.js +6 -0
  54. package/dist/inspector/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  55. package/dist/inspector/assets/plus-CPWsVXx9.js +6 -0
  56. package/dist/inspector/assets/recent_activity-DOx6FPz3.js +11 -0
  57. package/dist/inspector/assets/recent_conversations-CgG1tewt.js +1 -0
  58. package/dist/inspector/assets/recent_records_feed-CQN5zaI8.js +1 -0
  59. package/dist/inspector/assets/relationship_detail-LsEDdRi1.js +1 -0
  60. package/dist/inspector/assets/relationships-DnfqeglP.js +1 -0
  61. package/dist/inspector/assets/relationships-bLyNDRTv.js +1 -0
  62. package/dist/inspector/assets/sandbox-CHH6I9Ww.js +1 -0
  63. package/dist/inspector/assets/schema_detail-DnkpfNNX.js +11 -0
  64. package/dist/inspector/assets/schemas-CvrYW_Oj.js +5 -0
  65. package/dist/inspector/assets/search-Bj5zl6Sn.js +1 -0
  66. package/dist/inspector/assets/select-MlFuilHt.js +6 -0
  67. package/dist/inspector/assets/settings-D5tFC9Ho.js +1 -0
  68. package/dist/inspector/assets/source_detail-C2OkmsHz.js +17 -0
  69. package/dist/inspector/assets/source_link-B0ZU_1AG.js +1 -0
  70. package/dist/inspector/assets/sources-BDnp2oAz.js +9 -0
  71. package/dist/inspector/assets/switch-DXGlOHG4.js +1 -0
  72. package/dist/inspector/assets/tabs-C0mWq9S1.js +1 -0
  73. package/dist/inspector/assets/textarea-wLUG8kv6.js +1 -0
  74. package/dist/inspector/assets/timeline-DXEE4O4U.js +1 -0
  75. package/dist/inspector/assets/timeline-LjwviNGU.js +1 -0
  76. package/dist/inspector/assets/timeline_event_detail-C0M5GhTm.js +1 -0
  77. package/dist/inspector/assets/trash-2-DWBXKl-O.js +6 -0
  78. package/dist/inspector/assets/turn_detail-DXCnGJQ4.js +1 -0
  79. package/dist/inspector/assets/turns-C6blPs3y.js +1 -0
  80. package/dist/inspector/assets/use_agents-Cl-ZvBxZ.js +1 -0
  81. package/dist/inspector/assets/use_entities-DtfxOiM5.js +1 -0
  82. package/dist/inspector/assets/use_interpretations-usFKH4p9.js +1 -0
  83. package/dist/inspector/assets/use_mutations-C-WJL341.js +1 -0
  84. package/dist/inspector/assets/use_recent_conversations-CcGV91zn.js +1 -0
  85. package/dist/inspector/assets/use_relationships-Ci7lTjAZ.js +1 -0
  86. package/dist/inspector/assets/use_schemas-D_uXgmEv.js +1 -0
  87. package/dist/inspector/assets/use_sources-CFY0i0dw.js +1 -0
  88. package/dist/inspector/assets/use_stats-DBERXcdx.js +1 -0
  89. package/dist/inspector/assets/use_timeline-BqlPwIlB.js +1 -0
  90. package/dist/inspector/assets/use_turns-vQjPAKeW.js +1 -0
  91. package/dist/inspector/assets/value-BTdN53H7.js +1 -0
  92. package/dist/inspector/favicon.svg +10 -0
  93. package/dist/inspector/index.html +14 -0
  94. package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
  95. package/dist/repositories/sqlite/sqlite_client.js +12 -0
  96. package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
  97. package/dist/scripts/seed_sandbox.js +11 -1
  98. package/dist/scripts/seed_sandbox.js.map +1 -1
  99. package/dist/server.d.ts +10 -0
  100. package/dist/server.d.ts.map +1 -1
  101. package/dist/server.js +33 -23
  102. package/dist/server.js.map +1 -1
  103. package/dist/services/conversation_turn.d.ts +20 -51
  104. package/dist/services/conversation_turn.d.ts.map +1 -1
  105. package/dist/services/conversation_turn.js +136 -373
  106. package/dist/services/conversation_turn.js.map +1 -1
  107. package/dist/services/feedback/admin_proxy.d.ts +21 -7
  108. package/dist/services/feedback/admin_proxy.d.ts.map +1 -1
  109. package/dist/services/feedback/admin_proxy.js +142 -16
  110. package/dist/services/feedback/admin_proxy.js.map +1 -1
  111. package/dist/services/feedback/mirror_local_to_entity.d.ts +20 -53
  112. package/dist/services/feedback/mirror_local_to_entity.d.ts.map +1 -1
  113. package/dist/services/feedback/mirror_local_to_entity.js +46 -76
  114. package/dist/services/feedback/mirror_local_to_entity.js.map +1 -1
  115. package/dist/services/feedback/neotoma_payload.d.ts +72 -33
  116. package/dist/services/feedback/neotoma_payload.d.ts.map +1 -1
  117. package/dist/services/feedback/neotoma_payload.js +12 -24
  118. package/dist/services/feedback/neotoma_payload.js.map +1 -1
  119. package/dist/services/feedback_transport_local.d.ts.map +1 -1
  120. package/dist/services/feedback_transport_local.js +4 -0
  121. package/dist/services/feedback_transport_local.js.map +1 -1
  122. package/dist/services/inspector_mount.d.ts +32 -70
  123. package/dist/services/inspector_mount.d.ts.map +1 -1
  124. package/dist/services/inspector_mount.js +219 -183
  125. package/dist/services/inspector_mount.js.map +1 -1
  126. package/dist/services/recent_conversations.d.ts +2 -0
  127. package/dist/services/recent_conversations.d.ts.map +1 -1
  128. package/dist/services/recent_conversations.js +17 -0
  129. package/dist/services/recent_conversations.js.map +1 -1
  130. package/dist/services/root_landing/harness_snippets.d.ts +2 -0
  131. package/dist/services/root_landing/harness_snippets.d.ts.map +1 -1
  132. package/dist/services/root_landing/harness_snippets.js +15 -8
  133. package/dist/services/root_landing/harness_snippets.js.map +1 -1
  134. package/dist/services/root_landing/html_template.d.ts +9 -0
  135. package/dist/services/root_landing/html_template.d.ts.map +1 -1
  136. package/dist/services/root_landing/html_template.js +66 -1
  137. package/dist/services/root_landing/html_template.js.map +1 -1
  138. package/dist/services/root_landing/index.d.ts +11 -0
  139. package/dist/services/root_landing/index.d.ts.map +1 -1
  140. package/dist/services/root_landing/index.js +26 -8
  141. package/dist/services/root_landing/index.js.map +1 -1
  142. package/dist/services/root_landing/md_template.d.ts.map +1 -1
  143. package/dist/services/root_landing/md_template.js +22 -2
  144. package/dist/services/root_landing/md_template.js.map +1 -1
  145. package/dist/services/sandbox/pack_registry.d.ts +6 -50
  146. package/dist/services/sandbox/pack_registry.d.ts.map +1 -1
  147. package/dist/services/sandbox/pack_registry.js +74 -86
  148. package/dist/services/sandbox/pack_registry.js.map +1 -1
  149. package/dist/services/sandbox/seeder.d.ts +8 -47
  150. package/dist/services/sandbox/seeder.d.ts.map +1 -1
  151. package/dist/services/sandbox/seeder.js +51 -89
  152. package/dist/services/sandbox/seeder.js.map +1 -1
  153. package/dist/services/sandbox/sessions.d.ts +23 -114
  154. package/dist/services/sandbox/sessions.d.ts.map +1 -1
  155. package/dist/services/sandbox/sessions.js +99 -288
  156. package/dist/services/sandbox/sessions.js.map +1 -1
  157. package/dist/services/schema_definitions.d.ts.map +1 -1
  158. package/dist/services/schema_definitions.js +152 -0
  159. package/dist/services/schema_definitions.js.map +1 -1
  160. package/dist/shared/openapi_types.d.ts +3 -0
  161. package/dist/shared/openapi_types.d.ts.map +1 -1
  162. package/openapi.yaml +4 -0
  163. package/package.json +23 -9
package/README.md CHANGED
@@ -23,6 +23,8 @@ Neotoma is a deterministic state layer for AI agents. It stores structured recor
23
23
 
24
24
  Not retrieval memory (RAG, vector search, semantic lookup). Neotoma enforces deterministic state evolution: same observations always produce the same entity state, regardless of when or in what order they are processed.
25
25
 
26
+ The **Inspector** — Neotoma's visual control plane for browsing the entity graph, timeline, schema editor, and agent attribution — is bundled and served at `/inspector` by default when the server starts. No separate build or configuration required. Override with `NEOTOMA_INSPECTOR_DISABLE`, `NEOTOMA_PUBLIC_INSPECTOR_URL`, `NEOTOMA_INSPECTOR_STATIC_DIR`, or `NEOTOMA_INSPECTOR_BASE_PATH` (see `.env.example`).
27
+
26
28
  ## Architecture
27
29
 
28
30
  ```mermaid
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAiJ9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAwZ7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AA4PD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAmBhD;AA2kHD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,4BAA4B,EAAE,iBAAiB,CAAC;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;cAkfS,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;;;;;mBA9IV,MAAM;qBACJ,MAAM;wBACH,MAAM,GAAG,IAAI;2BACV,MAAM;;wBAET,MAAM;uBACP,MAAM,EAAE;wBACP,MAAM;uBACP,MAAM;;kBA9NX,MAAM;oBACJ,MAAM;yBACD,MAAM;4BACH,MAAM;2BACP,MAAM;;+BA4NF,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;;;2BAkFlC,MAAM;0BACP,MAAM;0BACN,MAAM;;GA8E3B;AA29DD,wBAAsB,eAAe;;;eA6EpC"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAiK9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAif7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AA4PD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAmBhD;AA6nHD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,4BAA4B,EAAE,iBAAiB,CAAC;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;cAkfS,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;;;;;mBA9IV,MAAM;qBACJ,MAAM;wBACH,MAAM,GAAG,IAAI;2BACV,MAAM;;wBAET,MAAM;uBACP,MAAM,EAAE;wBACP,MAAM;uBACP,MAAM;;kBA9NX,MAAM;oBACJ,MAAM;yBACD,MAAM;4BACH,MAAM;2BACP,MAAM;;+BA4NF,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;;;2BAkFlC,MAAM;0BACP,MAAM;0BACN,MAAM;;GA8E3B;AA29DD,wBAAsB,eAAe;;;eA2FpC"}
package/dist/actions.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import express from "express";
2
+ import cookieParser from "cookie-parser";
2
3
  import cors from "cors";
3
4
  import helmet from "helmet";
4
5
  import morgan from "morgan";
@@ -34,7 +35,9 @@ import { logger } from "./utils/logger.js";
34
35
  import { OAuthError } from "./services/mcp_oauth_errors.js";
35
36
  import { ensureLocalDevUser, ensureSandboxAauthUser, ensureSandboxPublicUser, LOCAL_DEV_USER_ID, SANDBOX_PUBLIC_USER_ID, } from "./services/local_auth.js";
36
37
  import { isSandboxMode, sandboxDestructiveGuard, sandboxHeaderMiddleware, } from "./services/sandbox_mode.js";
37
- import { buildLandingContext, buildRootLandingHtml, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
38
+ import { createSandboxSession, redeemOneTimeCode, resolveSessionFromRequest, revokeSession, purgeSessionUserData, sweepExpiredSessions, SESSION_COOKIE_NAME, } from "./services/sandbox/sessions.js";
39
+ import { buildLandingContext, buildRootLandingHtml, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, readNeotomaConfigEnvironment, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
40
+ import { installInspectorMount } from "./services/inspector_mount.js";
38
41
  import { getSandboxTermsResponse } from "./services/sandbox/terms.js";
39
42
  import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
40
43
  import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
@@ -49,6 +52,7 @@ import { readOpenApiActionsFile, readOpenApiFile } from "./shared/openapi_file.j
49
52
  import { buildSmitheryServerCard } from "./mcp_server_card.js";
50
53
  import { listRecentRecordActivity, parseRecordActivityTypesQuery, } from "./services/recent_record_activity.js";
51
54
  import { listRecentConversations } from "./services/recent_conversations.js";
55
+ import { listConversationTurns, getConversationTurn, } from "./services/conversation_turn.js";
52
56
  import { getAgent, listAgentRecords, listAgents } from "./services/agents_directory.js";
53
57
  export const app = express();
54
58
  // Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
@@ -102,6 +106,7 @@ app.use(express.json({
102
106
  },
103
107
  }));
104
108
  app.use(morgan("dev"));
109
+ app.use(cookieParser());
105
110
  app.use(unknownFieldsGuard);
106
111
  // Sandbox-mode response header. Stamped on every response so clients can
107
112
  // detect public-sandbox deployments (sandbox.neotoma.io) without an extra
@@ -110,37 +115,125 @@ if (isSandboxMode()) {
110
115
  app.use(sandboxHeaderMiddleware);
111
116
  logger.info("[Sandbox] NEOTOMA_SANDBOX_MODE=1 — bearer bypass to SANDBOX_PUBLIC_USER_ID, destructive routes gated, weekly reset expected");
112
117
  }
113
- // Inspector SPA mount. When NEOTOMA_INSPECTOR_STATIC_DIR is set, serve the
114
- // pre-built Inspector bundle at NEOTOMA_INSPECTOR_BASE_PATH (default /app).
115
- // Deliberately registered before all auth / rate-limit middleware so the SPA
116
- // shell + assets are reachable without a bearer — the API calls it makes still
117
- // flow through the normal auth stack below.
118
- const inspectorStaticDir = (process.env.NEOTOMA_INSPECTOR_STATIC_DIR || "").trim();
119
- const inspectorBasePath = ((process.env.NEOTOMA_INSPECTOR_BASE_PATH || "/app").trim() || "/app").replace(/\/$/, "");
120
- if (inspectorStaticDir) {
121
- try {
122
- const indexHtmlPath = path.resolve(inspectorStaticDir, "index.html");
123
- // express.static with fallthrough so 404s on unknown files fall into the
124
- // SPA history handler below rather than short-circuiting.
125
- app.use(inspectorBasePath, express.static(inspectorStaticDir, {
126
- index: false,
127
- fallthrough: true,
128
- maxAge: "1h",
129
- }));
130
- app.get([`${inspectorBasePath}`, `${inspectorBasePath}/*`], (req, res, next) => {
131
- // Only respond if the request was headed for the SPA (accepts html).
132
- if (req.method !== "GET")
133
- return next();
134
- res.sendFile(indexHtmlPath, (err) => {
135
- if (err)
136
- next(err);
118
+ // Inspector SPA mount. Deliberately registered before all auth / rate-limit
119
+ // middleware so the SPA shell + assets are reachable without a bearer — the
120
+ // API calls the Inspector makes still flow through the normal auth stack below.
121
+ installInspectorMount(app, process.env, logger);
122
+ // ── Sandbox session endpoints ───────────────────────────────────────────
123
+ // Registered before general auth so the session handshake works for
124
+ // unauthenticated visitors. Routes exist only when NEOTOMA_SANDBOX_MODE=1.
125
+ if (isSandboxMode()) {
126
+ const sessionRateLimit = rateLimit({
127
+ windowMs: 60 * 60 * 1000,
128
+ limit: 10,
129
+ standardHeaders: true,
130
+ legacyHeaders: false,
131
+ keyGenerator: (req) => `ip:${ipKeyGenerator(req.ip || "")}`,
132
+ validate: { trustProxy: false },
133
+ });
134
+ app.post("/sandbox/session/new", sessionRateLimit, (req, res) => {
135
+ try {
136
+ const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : "generic";
137
+ const session = createSandboxSession(packId);
138
+ res.cookie(SESSION_COOKIE_NAME, session.bearerToken, {
139
+ httpOnly: true,
140
+ sameSite: "lax",
141
+ path: "/",
142
+ expires: new Date(session.expiresAt),
143
+ });
144
+ res.json({
145
+ one_time_code: session.oneTimeCode,
146
+ expires_at: session.expiresAt,
147
+ pack_id: session.packId,
148
+ });
149
+ }
150
+ catch (err) {
151
+ logger.error(`[Sandbox] session/new failed: ${err.message}`);
152
+ res.status(500).json({ error_code: "SESSION_CREATE_FAILED", message: err.message });
153
+ }
154
+ });
155
+ app.post("/sandbox/session/redeem", (req, res) => {
156
+ try {
157
+ const code = typeof req.body?.code === "string" ? req.body.code : "";
158
+ if (!code) {
159
+ res.status(400).json({ error_code: "MISSING_CODE", message: "code is required" });
160
+ return;
161
+ }
162
+ const result = redeemOneTimeCode(code);
163
+ if (!result) {
164
+ res.status(404).json({ error_code: "INVALID_CODE", message: "Code expired or already redeemed" });
165
+ return;
166
+ }
167
+ res.cookie(SESSION_COOKIE_NAME, result.bearerToken, {
168
+ httpOnly: true,
169
+ sameSite: "lax",
170
+ path: "/",
171
+ expires: new Date(result.expiresAt),
137
172
  });
173
+ res.json({
174
+ bearer_token: result.bearerToken,
175
+ user_id: result.userId,
176
+ expires_at: result.expiresAt,
177
+ pack_id: result.packId,
178
+ });
179
+ }
180
+ catch (err) {
181
+ logger.error(`[Sandbox] session/redeem failed: ${err.message}`);
182
+ res.status(500).json({ error_code: "SESSION_REDEEM_FAILED", message: err.message });
183
+ }
184
+ });
185
+ app.get("/sandbox/session", (req, res) => {
186
+ const session = resolveSessionFromRequest(req);
187
+ if (!session) {
188
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
189
+ return;
190
+ }
191
+ res.json({
192
+ user_id: session.userId,
193
+ pack_id: session.packId,
194
+ created_at: session.createdAt,
195
+ expires_at: session.expiresAt,
138
196
  });
139
- logger.info(`[Inspector] Serving SPA from ${inspectorStaticDir} at ${inspectorBasePath}`);
140
- }
141
- catch (err) {
142
- logger.warn(`[Inspector] Failed to mount SPA: ${err.message}`);
143
- }
197
+ });
198
+ app.post("/sandbox/session/reset", (req, res) => {
199
+ const session = resolveSessionFromRequest(req);
200
+ if (!session) {
201
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
202
+ return;
203
+ }
204
+ const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : undefined;
205
+ purgeSessionUserData(session.userId);
206
+ const newSession = createSandboxSession(packId ?? session.packId);
207
+ res.cookie(SESSION_COOKIE_NAME, newSession.bearerToken, {
208
+ httpOnly: true,
209
+ sameSite: "lax",
210
+ path: "/",
211
+ expires: new Date(newSession.expiresAt),
212
+ });
213
+ res.json({
214
+ user_id: newSession.userId,
215
+ pack_id: newSession.packId,
216
+ expires_at: newSession.expiresAt,
217
+ });
218
+ });
219
+ app.delete("/sandbox/session", (req, res) => {
220
+ const session = resolveSessionFromRequest(req);
221
+ if (!session) {
222
+ res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
223
+ return;
224
+ }
225
+ revokeSession(session.userId);
226
+ purgeSessionUserData(session.userId);
227
+ res.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
228
+ res.json({ ok: true });
229
+ });
230
+ setInterval(() => {
231
+ const purged = sweepExpiredSessions();
232
+ if (purged > 0) {
233
+ logger.info(`[Sandbox] Swept ${purged} expired session(s)`);
234
+ }
235
+ }, 15 * 60 * 1000);
236
+ logger.info("[Sandbox] Session endpoints registered");
144
237
  }
145
238
  // Rate limiters for OAuth endpoints
146
239
  // validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
@@ -399,6 +492,7 @@ app.get("/server-info", (_req, res) => {
399
492
  httpPort,
400
493
  apiBase: config.apiBase,
401
494
  mcpUrl,
495
+ neotoma_env: readNeotomaConfigEnvironment(),
402
496
  });
403
497
  });
404
498
  // ============================================================================
@@ -1803,6 +1897,16 @@ app.use(async (req, res, next) => {
1803
1897
  logger.info(`[Auth] ${req.method} ${req.path} auth_method=local_no_bearer user_id=${devUser.id}`);
1804
1898
  return next();
1805
1899
  }
1900
+ // Sandbox ephemeral session: resolve from cookie or Bearer token before
1901
+ // falling back to the shared public user. Expired/revoked sessions 401.
1902
+ if (isSandboxMode()) {
1903
+ const sessionInfo = resolveSessionFromRequest(req);
1904
+ if (sessionInfo) {
1905
+ req.authenticatedUserId = sessionInfo.userId;
1906
+ logger.info(`[Auth] ${req.method} ${req.path} auth_method=sandbox_session user_id=${sessionInfo.userId}`);
1907
+ return next();
1908
+ }
1909
+ }
1806
1910
  // Sandbox mode: public deployment at sandbox.neotoma.io where anonymous
1807
1911
  // callers are attributed to SANDBOX_PUBLIC_USER_ID without a Bearer. AAuth
1808
1912
  // still runs (earlier in the chain via aauthVerify) so agents exercising the
@@ -3187,6 +3291,43 @@ app.get("/recent_conversations", async (req, res) => {
3187
3291
  return handleApiError(req, res, error, "Failed to list recent conversations", "DB_QUERY_FAILED", "APIError:recent_conversations");
3188
3292
  }
3189
3293
  });
3294
+ // GET /turns — Inspector: paginated conversation_turn index
3295
+ app.get("/turns", async (req, res) => {
3296
+ try {
3297
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3298
+ const limit = parseInt(String(req.query.limit ?? "25"), 10) || 25;
3299
+ const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
3300
+ const harness = typeof req.query.harness === "string" ? req.query.harness.trim() || null : null;
3301
+ const status = typeof req.query.status === "string" ? req.query.status.trim() || null : null;
3302
+ const activity_after = typeof req.query.activity_after === "string" ? req.query.activity_after.trim() || null : null;
3303
+ const activity_before = typeof req.query.activity_before === "string" ? req.query.activity_before.trim() || null : null;
3304
+ const result = listConversationTurns(userId, limit, offset, {
3305
+ harness,
3306
+ status,
3307
+ activity_after,
3308
+ activity_before,
3309
+ });
3310
+ return res.json(result);
3311
+ }
3312
+ catch (error) {
3313
+ return handleApiError(req, res, error, "Failed to list turns", "DB_QUERY_FAILED", "APIError:turns");
3314
+ }
3315
+ });
3316
+ // GET /turns/:turn_key — Inspector: conversation_turn detail
3317
+ app.get("/turns/:turn_key", async (req, res) => {
3318
+ try {
3319
+ const userId = await getAuthenticatedUserId(req, req.query.user_id);
3320
+ const turnKey = decodeURIComponent(req.params.turn_key);
3321
+ const result = getConversationTurn(userId, turnKey);
3322
+ if (!result) {
3323
+ return res.status(404).json({ error: "Turn not found", code: "NOT_FOUND" });
3324
+ }
3325
+ return res.json(result);
3326
+ }
3327
+ catch (error) {
3328
+ return handleApiError(req, res, error, "Failed to get turn", "DB_QUERY_FAILED", "APIError:turn_detail");
3329
+ }
3330
+ });
3190
3331
  // GET /api/sources - Get source list (FU-301)
3191
3332
  app.get("/sources", async (req, res) => {
3192
3333
  try {
@@ -5665,6 +5806,16 @@ export async function startHTTPServer() {
5665
5806
  }
5666
5807
  // Initialize encryption service
5667
5808
  await initServerKeys();
5809
+ // Seed `neotoma_feedback` schema unconditionally so local-only installs
5810
+ // (no AGENT_SITE_BASE_URL) can mirror feedback into the entity graph.
5811
+ try {
5812
+ const { seedNeotomaFeedbackSchema } = await import("./services/feedback/seed_schema.js");
5813
+ await seedNeotomaFeedbackSchema();
5814
+ logger.info("[Feedback] neotoma_feedback schema seeded");
5815
+ }
5816
+ catch (err) {
5817
+ logger.warn(`[Feedback] failed to seed neotoma_feedback schema: ${err.message}`);
5818
+ }
5668
5819
  // Sandbox mode: ensure the `sandbox_abuse_report` entity type is registered
5669
5820
  // before any report comes in so forwarded records can attach cleanly to the
5670
5821
  // entity graph. Non-sandbox deployments still benefit from having the schema