neotoma 0.18.8 → 0.20.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 (237) hide show
  1. package/dist/actions.d.ts +59 -0
  2. package/dist/actions.d.ts.map +1 -1
  3. package/dist/actions.js +325 -15
  4. package/dist/actions.js.map +1 -1
  5. package/dist/cli/index.d.ts +14 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +87 -4
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/docs/developer/cli_agent_instructions.md +37 -0
  10. package/dist/docs/developer/cli_reference.md +1 -1
  11. package/dist/docs/developer/mcp/instructions.md +3 -2
  12. package/dist/docs/developer/mcp/proxy.md +6 -4
  13. package/dist/docs/foundation/entity_resolution.md +34 -7
  14. package/dist/docs/foundation/what_to_store.md +1 -1
  15. package/dist/docs/infrastructure/client_instance_deployment.md +151 -0
  16. package/dist/docs/reference/error_codes.md +75 -23
  17. package/dist/docs/subsystems/errors.md +3 -0
  18. package/dist/docs/testing/automated_test_catalog.md +30 -14
  19. package/dist/inspector/assets/access_policies-6-6DPI39.js +1 -0
  20. package/dist/inspector/assets/{agent_badge-D5c8UAE9.js → agent_badge-CXSO0sXJ.js} +1 -1
  21. package/dist/inspector/assets/{agent_detail-lZm2dWDR.js → agent_detail-Cr7iqdye.js} +1 -1
  22. package/dist/inspector/assets/{agent_filter-_s9ctnZg.js → agent_filter-DQvQyxMp.js} +1 -1
  23. package/dist/inspector/assets/{agent_grant_detail-jPlKCXYO.js → agent_grant_detail-DRRM7KWw.js} +1 -1
  24. package/dist/inspector/assets/{agent_grant_form-BUQdfq4A.js → agent_grant_form-DNfWiVBQ.js} +1 -1
  25. package/dist/inspector/assets/{agent_grants-D7dYlLNX.js → agent_grants-BWgOPiOe.js} +1 -1
  26. package/dist/inspector/assets/{agents-Bl50GiDS.js → agents-CPLAQelS.js} +1 -1
  27. package/dist/inspector/assets/{api_not_configured_state-GASK-mhp.js → api_not_configured_state-DHesochj.js} +1 -1
  28. package/dist/inspector/assets/{attribution_card-Dks_zpQx.js → attribution_card-CCINKjRY.js} +1 -1
  29. package/dist/inspector/assets/{attribution_summary-Cli2qL-J.js → attribution_summary-Dkwz1aNS.js} +1 -1
  30. package/dist/inspector/assets/{bundles-4y41Dsvz.js → bundles-f-bRlQYm.js} +1 -1
  31. package/dist/inspector/assets/{card-DyP1iupa.js → card-18-eZw0x.js} +1 -1
  32. package/dist/inspector/assets/{checkbox-CIUQh9NP.js → checkbox-DUu6AMCH.js} +1 -1
  33. package/dist/inspector/assets/{compliance-CZT3IVp5.js → compliance-C1iqMRYK.js} +1 -1
  34. package/dist/inspector/assets/{confirm-dialog-DwPqhney.js → confirm-dialog-KIPuwTQR.js} +1 -1
  35. package/dist/inspector/assets/{conversation_common-DqXXK2sj.js → conversation_common-aPvd5zmX.js} +1 -1
  36. package/dist/inspector/assets/{conversation_detail-BUqgnrpM.js → conversation_detail-nJDDMCfc.js} +1 -1
  37. package/dist/inspector/assets/{copy_id_button-CFvm8hKT.js → copy_id_button-BwaRGKNA.js} +1 -1
  38. package/dist/inspector/assets/{copyable_code_block-Ds_m6kph.js → copyable_code_block-DDi03yAz.js} +1 -1
  39. package/dist/inspector/assets/{corrections-CYBwDzLB.js → corrections-Dz2v9ju9.js} +1 -1
  40. package/dist/inspector/assets/{dashboard-Bn74vHnA.js → dashboard-CZOdnoJJ.js} +1 -1
  41. package/dist/inspector/assets/{data-table-BdvHAu9o.js → data-table-5i_gqEfj.js} +1 -1
  42. package/dist/inspector/assets/{design-CvkXmrg8.js → design-BdKBUVmX.js} +1 -1
  43. package/dist/inspector/assets/{dialog-ClLKFBmR.js → dialog-CNJoZoQS.js} +1 -1
  44. package/dist/inspector/assets/{docs-DsWM97KO.js → docs-sXzc0LTz.js} +1 -1
  45. package/dist/inspector/assets/{docs_page_layout-B2uZRfVC.js → docs_page_layout-CGg8ubQX.js} +1 -1
  46. package/dist/inspector/assets/{embed_graph-dvewK9nr.js → embed_graph-BpLLD_vX.js} +1 -1
  47. package/dist/inspector/assets/{empty_state-Bc-ujW2X.js → empty_state-DNk61WLW.js} +1 -1
  48. package/dist/inspector/assets/{entities-BOrwMORJ.js → entities-Cexhp3qr.js} +1 -1
  49. package/dist/inspector/assets/{entity_correct-r178klDI.js → entity_correct-CfSggXgT.js} +1 -1
  50. package/dist/inspector/assets/{entity_history-CKLn3lKb.js → entity_history-CGila0eu.js} +1 -1
  51. package/dist/inspector/assets/{entity_history_sections-FVZdr00Y.js → entity_history_sections-CXYX1gph.js} +1 -1
  52. package/dist/inspector/assets/{entity_link-yKDYqbhx.js → entity_link-DUdsaj-A.js} +1 -1
  53. package/dist/inspector/assets/{entity_relationships_by_type-SQLvwlXL.js → entity_relationships_by_type-B_PH1APK.js} +1 -1
  54. package/dist/inspector/assets/{entity_segment_page-BXDQbUUp.js → entity_segment_page-DRYjT1AU.js} +1 -1
  55. package/dist/inspector/assets/{entity_table_column_toggle-D7CwUiAH.js → entity_table_column_toggle-DwwHY7Y8.js} +1 -1
  56. package/dist/inspector/assets/{entity_timeline-Dg0wEuRA.js → entity_timeline-B4XYY56n.js} +1 -1
  57. package/dist/inspector/assets/{entity_types-DLlkd4ex.js → entity_types-ByB2PWZM.js} +1 -1
  58. package/dist/inspector/assets/{faq-KbswV10B.js → faq-B-PEHwkf.js} +1 -1
  59. package/dist/inspector/assets/{graph_auto_fit-SqyYePaZ.js → graph_auto_fit-ClZnIOoR.js} +1 -1
  60. package/dist/inspector/assets/{graph_explorer-DZgjc-hi.js → graph_explorer-C-Xo8VWP.js} +1 -1
  61. package/dist/inspector/assets/{home-CUYUSmke.js → home-D1UVMr6_.js} +1 -1
  62. package/dist/inspector/assets/{index-BGqcUFFW.js → index-C6fxoS_Z.js} +1582 -1582
  63. package/dist/inspector/assets/{index-CBJY_OOI.js → index-Cp0BQ57U.js} +1 -1
  64. package/dist/inspector/assets/{index-DOxiDdC1.js → index-MnCv_KTr.js} +1 -1
  65. package/dist/inspector/assets/interpretations-BCs9qtw0.js +1 -0
  66. package/dist/inspector/assets/{interpretations-CSeyL3hr.js → interpretations-DG5kUN-V.js} +1 -1
  67. package/dist/inspector/assets/{issue_author_attribution-Czyui6JR.js → issue_author_attribution-D7_vwrHJ.js} +1 -1
  68. package/dist/inspector/assets/{issue_detail-DgdqvrIo.js → issue_detail-DKYUwKt_.js} +1 -1
  69. package/dist/inspector/assets/{issues-dw8Q41vn.js → issues-DcSMjt-p.js} +1 -1
  70. package/dist/inspector/assets/{json_viewer-CEMz7ftn.js → json_viewer-BKa7wwrs.js} +1 -1
  71. package/dist/inspector/assets/{label-qbFxe1yC.js → label-BO-aVMjO.js} +1 -1
  72. package/dist/inspector/assets/{live_relative_time-CVpdUZRq.js → live_relative_time-Dx00M9I6.js} +1 -1
  73. package/dist/inspector/assets/mobile_filter_popover-I9fJvTGr.js +1 -0
  74. package/dist/inspector/assets/{not_found-DFUrM9eP.js → not_found-DuCJouBh.js} +1 -1
  75. package/dist/inspector/assets/oauth_callback-w0kyGAb4.js +1 -0
  76. package/dist/inspector/assets/oauth_signin-3LQrxsKz.js +1 -0
  77. package/dist/inspector/assets/{observations-BO6KImWs.js → observations-Bq6mvl5m.js} +1 -1
  78. package/dist/inspector/assets/{page_shell-COxcPFXG.js → page_shell-BoTb5of6.js} +1 -1
  79. package/dist/inspector/assets/{pagination-NLnVe1CU.js → pagination-BJtT1Wgy.js} +1 -1
  80. package/dist/inspector/assets/{peer_detail-DdtQsiwD.js → peer_detail-BM1jhSS2.js} +1 -1
  81. package/dist/inspector/assets/{peers-D0bzQeyz.js → peers-CYSx-jAx.js} +1 -1
  82. package/dist/inspector/assets/{pin_primitive_button-BtuY3Ig7.js → pin_primitive_button-BdAR3e5K.js} +1 -1
  83. package/dist/inspector/assets/{recent_activity-8t3t2hCi.js → recent_activity-Dio9UBCA.js} +1 -1
  84. package/dist/inspector/assets/{recent_conversations-xURWFQ1G.js → recent_conversations-Be7S63h9.js} +1 -1
  85. package/dist/inspector/assets/{recent_conversations-B351X_is.js → recent_conversations-iJTSJf5e.js} +1 -1
  86. package/dist/inspector/assets/{recent_records_feed-C0auJYwP.js → recent_records_feed-ZxHjVjQ6.js} +1 -1
  87. package/dist/inspector/assets/record_activity-BmMC7Q7l.js +1 -0
  88. package/dist/inspector/assets/{relationship_detail-B9y50i1N.js → relationship_detail-i9rsGxA6.js} +1 -1
  89. package/dist/inspector/assets/{relationship_panel_groups-DgC2aJin.js → relationship_panel_groups-CXFlrRHU.js} +1 -1
  90. package/dist/inspector/assets/{relationships-u7QTLzYr.js → relationships-DqAzfnlB.js} +1 -1
  91. package/dist/inspector/assets/{relationships-DvN6JCJC.js → relationships-DvWtOU7W.js} +1 -1
  92. package/dist/inspector/assets/{sandbox-DzEfLQAo.js → sandbox-Cn4vq2Pz.js} +1 -1
  93. package/dist/inspector/assets/{schema_detail-BI3lLXFK.js → schema_detail-BdORXewP.js} +1 -1
  94. package/dist/inspector/assets/{schemas-DpAQVcki.js → schemas-HYsBtoHT.js} +1 -1
  95. package/dist/inspector/assets/{search-CjE75jdm.js → search-LzJnqvTB.js} +1 -1
  96. package/dist/inspector/assets/{segmented_control-D9QbgEtB.js → segmented_control-CHJO5zMt.js} +1 -1
  97. package/dist/inspector/assets/{select-KOFjYHgz.js → select-DFzP9laV.js} +1 -1
  98. package/dist/inspector/assets/settings-Bm0jNa4p.js +1 -0
  99. package/dist/inspector/assets/{source_content-Bi_jkeg2.js → source_content-PwVSPaIU.js} +1 -1
  100. package/dist/inspector/assets/{source_content_open_button-B9mhDPoj.js → source_content_open_button-C7NKRG7q.js} +1 -1
  101. package/dist/inspector/assets/{source_detail-B3jsntel.js → source_detail-C2D885Zy.js} +1 -1
  102. package/dist/inspector/assets/{source_link-DrAAN7IB.js → source_link-6lMlhQnK.js} +1 -1
  103. package/dist/inspector/assets/{sources-B7H4Tgy7.js → sources-BvGo5OZp.js} +1 -1
  104. package/dist/inspector/assets/{subscriptions-B79INOqC.js → subscriptions-BGYmtTLV.js} +1 -1
  105. package/dist/inspector/assets/{switch-DEw5ZD9Y.js → switch-DkusEYsE.js} +1 -1
  106. package/dist/inspector/assets/{tabs-CZDCOqHp.js → tabs-hkbPHxT0.js} +1 -1
  107. package/dist/inspector/assets/{textarea-DM2MsnIS.js → textarea-BhC7xtLk.js} +1 -1
  108. package/dist/inspector/assets/{timeline-DtGp-dM7.js → timeline-CHi-lYcV.js} +1 -1
  109. package/dist/inspector/assets/{timeline-CwKOl2cK.js → timeline-DmjoODDd.js} +1 -1
  110. package/dist/inspector/assets/{timeline_event_detail-D-gU-DZY.js → timeline_event_detail-BfKrsQih.js} +1 -1
  111. package/dist/inspector/assets/{turn_detail-D20KYWIY.js → turn_detail-DGj1lT0k.js} +1 -1
  112. package/dist/inspector/assets/{turns-CCukFhV6.js → turns-CHfOIg9X.js} +1 -1
  113. package/dist/inspector/assets/{use_agents-Duqes0yE.js → use_agents-6sUtEOpZ.js} +1 -1
  114. package/dist/inspector/assets/{use_entities-CAYbeIfx.js → use_entities-DeIlqnzx.js} +1 -1
  115. package/dist/inspector/assets/use_interpretations-DvfzvGu3.js +1 -0
  116. package/dist/inspector/assets/{use_mutations-CaZO8lpD.js → use_mutations-CQ3_5Ppg.js} +1 -1
  117. package/dist/inspector/assets/{use_peers-CGLvEf5V.js → use_peers-CW3OAs41.js} +1 -1
  118. package/dist/inspector/assets/{use_relationships-B8fSaOmY.js → use_relationships-Dbtmx8X8.js} +1 -1
  119. package/dist/inspector/assets/{use_stats-DHAaJFKE.js → use_stats-OyHBzetJ.js} +1 -1
  120. package/dist/inspector/assets/{use_timeline-BMIPVqoi.js → use_timeline-FINsibXm.js} +1 -1
  121. package/dist/inspector/assets/{use_turns-DRAnBf4e.js → use_turns-D4AhGAvM.js} +1 -1
  122. package/dist/inspector/assets/{world_time_event_timeline-CWFM4Q3K.js → world_time_event_timeline-F0elR7Vc.js} +1 -1
  123. package/dist/inspector/index.html +1 -1
  124. package/dist/proxy/mcp_stdio_proxy.d.ts +11 -5
  125. package/dist/proxy/mcp_stdio_proxy.d.ts.map +1 -1
  126. package/dist/proxy/mcp_stdio_proxy.js +12 -6
  127. package/dist/proxy/mcp_stdio_proxy.js.map +1 -1
  128. package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
  129. package/dist/repositories/sqlite/local_db_adapter.js +12 -0
  130. package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
  131. package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
  132. package/dist/repositories/sqlite/sqlite_client.js +7 -0
  133. package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
  134. package/dist/seed_schemas_entry.d.ts +28 -0
  135. package/dist/seed_schemas_entry.d.ts.map +1 -0
  136. package/dist/seed_schemas_entry.js +142 -0
  137. package/dist/seed_schemas_entry.js.map +1 -0
  138. package/dist/server.d.ts +9 -0
  139. package/dist/server.d.ts.map +1 -1
  140. package/dist/server.js +151 -3
  141. package/dist/server.js.map +1 -1
  142. package/dist/services/company_query.d.ts +54 -0
  143. package/dist/services/company_query.d.ts.map +1 -0
  144. package/dist/services/company_query.js +175 -0
  145. package/dist/services/company_query.js.map +1 -0
  146. package/dist/services/company_resolution.d.ts +86 -0
  147. package/dist/services/company_resolution.d.ts.map +1 -0
  148. package/dist/services/company_resolution.js +195 -0
  149. package/dist/services/company_resolution.js.map +1 -0
  150. package/dist/services/entity_cursor.d.ts +57 -0
  151. package/dist/services/entity_cursor.d.ts.map +1 -0
  152. package/dist/services/entity_cursor.js +104 -0
  153. package/dist/services/entity_cursor.js.map +1 -0
  154. package/dist/services/entity_queries.d.ts +32 -0
  155. package/dist/services/entity_queries.d.ts.map +1 -1
  156. package/dist/services/entity_queries.js +64 -2
  157. package/dist/services/entity_queries.js.map +1 -1
  158. package/dist/services/entity_query_limits.d.ts +20 -0
  159. package/dist/services/entity_query_limits.d.ts.map +1 -0
  160. package/dist/services/entity_query_limits.js +25 -0
  161. package/dist/services/entity_query_limits.js.map +1 -0
  162. package/dist/services/entity_resolution.d.ts.map +1 -1
  163. package/dist/services/entity_resolution.js +12 -2
  164. package/dist/services/entity_resolution.js.map +1 -1
  165. package/dist/services/google_oidc.d.ts +110 -0
  166. package/dist/services/google_oidc.d.ts.map +1 -0
  167. package/dist/services/google_oidc.js +249 -0
  168. package/dist/services/google_oidc.js.map +1 -0
  169. package/dist/services/inspector_mount.d.ts.map +1 -1
  170. package/dist/services/inspector_mount.js +9 -0
  171. package/dist/services/inspector_mount.js.map +1 -1
  172. package/dist/services/interpretation.d.ts.map +1 -1
  173. package/dist/services/interpretation.js +17 -0
  174. package/dist/services/interpretation.js.map +1 -1
  175. package/dist/services/issues/issue_operations.d.ts +4 -0
  176. package/dist/services/issues/issue_operations.d.ts.map +1 -1
  177. package/dist/services/issues/issue_operations.js +18 -0
  178. package/dist/services/issues/issue_operations.js.map +1 -1
  179. package/dist/services/issues/neotoma_client.d.ts.map +1 -1
  180. package/dist/services/issues/neotoma_client.js +10 -5
  181. package/dist/services/issues/neotoma_client.js.map +1 -1
  182. package/dist/services/issues/observer_import.d.ts +1 -1
  183. package/dist/services/issues/observer_import.d.ts.map +1 -1
  184. package/dist/services/issues/observer_import.js +3 -3
  185. package/dist/services/issues/observer_import.js.map +1 -1
  186. package/dist/services/mcp_oauth.d.ts +1 -1
  187. package/dist/services/mcp_oauth.d.ts.map +1 -1
  188. package/dist/services/mcp_oauth.js +11 -1
  189. package/dist/services/mcp_oauth.js.map +1 -1
  190. package/dist/services/oauth_key_gate.d.ts +15 -1
  191. package/dist/services/oauth_key_gate.d.ts.map +1 -1
  192. package/dist/services/oauth_key_gate.js +42 -3
  193. package/dist/services/oauth_key_gate.js.map +1 -1
  194. package/dist/services/schema_definitions.d.ts.map +1 -1
  195. package/dist/services/schema_definitions.js +27 -1
  196. package/dist/services/schema_definitions.js.map +1 -1
  197. package/dist/services/schema_reference_linking.d.ts +61 -2
  198. package/dist/services/schema_reference_linking.d.ts.map +1 -1
  199. package/dist/services/schema_reference_linking.js +264 -21
  200. package/dist/services/schema_reference_linking.js.map +1 -1
  201. package/dist/services/schema_registry.d.ts +25 -0
  202. package/dist/services/schema_registry.d.ts.map +1 -1
  203. package/dist/services/schema_registry.js +22 -0
  204. package/dist/services/schema_registry.js.map +1 -1
  205. package/dist/services/snapshot_computation.d.ts +18 -0
  206. package/dist/services/snapshot_computation.d.ts.map +1 -1
  207. package/dist/services/snapshot_computation.js +27 -3
  208. package/dist/services/snapshot_computation.js.map +1 -1
  209. package/dist/shared/action_handlers/entity_handlers.d.ts +9 -0
  210. package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
  211. package/dist/shared/action_handlers/entity_handlers.js +9 -2
  212. package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
  213. package/dist/shared/action_handlers/entity_identifier_handler.d.ts.map +1 -1
  214. package/dist/shared/action_handlers/entity_identifier_handler.js +131 -4
  215. package/dist/shared/action_handlers/entity_identifier_handler.js.map +1 -1
  216. package/dist/shared/action_schemas.d.ts +132 -38
  217. package/dist/shared/action_schemas.d.ts.map +1 -1
  218. package/dist/shared/action_schemas.js +171 -1
  219. package/dist/shared/action_schemas.js.map +1 -1
  220. package/dist/shared/contract_mappings.d.ts.map +1 -1
  221. package/dist/shared/contract_mappings.js +9 -0
  222. package/dist/shared/contract_mappings.js.map +1 -1
  223. package/dist/shared/openapi_types.d.ts +249 -5
  224. package/dist/shared/openapi_types.d.ts.map +1 -1
  225. package/dist/tool_definitions.d.ts +1 -1
  226. package/dist/tool_definitions.d.ts.map +1 -1
  227. package/dist/tool_definitions.js +30 -4
  228. package/dist/tool_definitions.js.map +1 -1
  229. package/docs/developer/mcp/instructions.md +3 -2
  230. package/openapi.yaml +288 -5
  231. package/package.json +2 -1
  232. package/dist/inspector/assets/access_policies-C0QzIgOo.js +0 -1
  233. package/dist/inspector/assets/interpretations-B1shK6Is.js +0 -1
  234. package/dist/inspector/assets/mobile_filter_popover-CI_BhFTn.js +0 -1
  235. package/dist/inspector/assets/record_activity-DUwg1WBC.js +0 -1
  236. package/dist/inspector/assets/settings-CHzeuzGL.js +0 -1
  237. package/dist/inspector/assets/use_interpretations-D_RbNlHg.js +0 -1
package/dist/actions.d.ts CHANGED
@@ -2,6 +2,7 @@ import express from "express";
2
2
  import { type GuestIdentity } from "./services/access_policy.js";
3
3
  import { type AgentIdentity } from "./crypto/agent_identity.js";
4
4
  import { type NeotomaSandboxModeName } from "./services/sandbox_mode.js";
5
+ import { OAuthKeySessionStore } from "./services/oauth_key_gate.js";
5
6
  import { type StoreInterpretationInput } from "./shared/action_schemas.js";
6
7
  /** Exposed for the /me route and any other surface that needs the boot-time mode verdict. */
7
8
  export declare function getResolvedServerMode(): NeotomaSandboxModeName | null;
@@ -76,6 +77,51 @@ export declare function isTrustedProxyIP(ip: string, env?: NodeJS.ProcessEnv): b
76
77
  * entirely and remains valid for single-host deployments without a tunnel.
77
78
  */
78
79
  export declare function isLocalRequest(req: express.Request): boolean;
80
+ /** Exported for tests asserting the cookie maxAge and the server-side session
81
+ * expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
82
+ export declare const oauthKeySessions: OAuthKeySessionStore;
83
+ /**
84
+ * Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
85
+ * short default: a user who signs in expects to stay signed in, whereas the
86
+ * key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
87
+ * sign-in is what logged hosted-instance users out every quarter hour (#2005).
88
+ *
89
+ * Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
90
+ */
91
+ export declare const DEFAULT_SIGN_IN_SESSION_TTL_MIN: number;
92
+ /** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
93
+ * this is a typo (an extra zero), not an intent — without a ceiling,
94
+ * "9999999999999" yields a ~19-million-year session. Mirrors the low-end
95
+ * guard rather than guarding only one side (#2007 qa lens). */
96
+ export declare const MAX_SIGN_IN_SESSION_TTL_MIN: number;
97
+ /**
98
+ * Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
99
+ * so the override's precedence and malformed-input behavior are directly
100
+ * testable — a silently-wrong TTL here reintroduces exactly the desync class
101
+ * this change fixes.
102
+ *
103
+ * Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
104
+ * Everything else — unset, non-numeric, zero, negative, or above the ceiling —
105
+ * falls back to the 7-day default rather than producing a degenerate session.
106
+ *
107
+ * Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
108
+ * and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
109
+ * as-is because they are in-range integers after parsing; the fallback exists
110
+ * for values that are absent, unparseable, or out of range, not to second-guess
111
+ * a caller who wrote a decimal. Asserted in tests so the behavior is documented
112
+ * rather than incidental.
113
+ */
114
+ export declare function resolveSignInSessionTtlMs(raw: string | undefined): number;
115
+ /**
116
+ * Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
117
+ * session and the cookie's own maxAge — they must agree, or the browser keeps
118
+ * presenting a cookie the server has already forgotten (or vice versa).
119
+ * Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
120
+ * sign-in (#2005).
121
+ */
122
+ export declare function setOAuthKeySessionCookie(req: express.Request, res: express.Response, ttlMs?: number): string;
123
+ /** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
124
+ export declare function getGoogleVerifiedUserId(req: express.Request): string | undefined;
79
125
  /**
80
126
  * Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
81
127
  * We trust explicit configuration first, then fall back to the host the
@@ -111,6 +157,18 @@ export declare function routeAllowsLocalIssueWriteFallback(req: Pick<express.Req
111
157
  export declare function routeConsumesGuestWriteBudget(req: Pick<express.Request, "method" | "path">): boolean;
112
158
  /** Exported for unit tests: guest-rate-limit key precedence is thumbprint -> guest token -> IP. */
113
159
  export declare function guestWriteRateLimitKey(req: express.Request): string;
160
+ /**
161
+ * Issue *submission* is the one route a third party may reach with no identity
162
+ * at all: the guest access token is minted BY the submit, so requiring one to
163
+ * perform it is a chicken-and-egg that closes the inbox to first contact.
164
+ *
165
+ * Deliberately submit-only. `add_message` stays authenticated: by the time a
166
+ * reporter has a thread to append to, they hold the token returned by their
167
+ * submit, so anonymity buys nothing there — and letting an unidentified caller
168
+ * write into an existing thread is a strictly wider blast radius than letting
169
+ * them open a new one. The token is what proves "I opened this thread".
170
+ */
171
+ export declare function routeAcceptsAnonymousIssueSubmit(req: Pick<express.Request, "method" | "path">): boolean;
114
172
  /**
115
173
  * Derive a userId for a guest principal from the validated token grant.
116
174
  *
@@ -180,6 +238,7 @@ export declare function storeStructuredForApi(params: {
180
238
  observation_index: number;
181
239
  entity_type: string;
182
240
  entity_id: string;
241
+ details?: Record<string, unknown>;
183
242
  }[] | undefined;
184
243
  warnings?: {
185
244
  code: string;
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
package/dist/actions.js CHANGED
@@ -22,6 +22,7 @@ import { aauthAdmission, getAAuthAdmissionFromRequest } from "./middleware/aauth
22
22
  import { buildSessionInfo, normalizeSessionOrigin } from "./services/session_info.js";
23
23
  import { AttributionPolicyError, enforceAttributionPolicy } from "./services/attribution_policy.js";
24
24
  import { OverridePolicyViolationError } from "./services/override_validation.js";
25
+ import { CursorError } from "./services/entity_cursor.js";
25
26
  import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
26
27
  import { assertGuestWriteAllowed, AccessPolicyError, } from "./services/access_policy.js";
27
28
  import { hashGuestAccessToken } from "./services/guest_access_token.js";
@@ -53,6 +54,7 @@ import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
53
54
  import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
54
55
  import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
55
56
  import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
57
+ import { buildGoogleAuthorizeUrl, createGoogleSigninNonce, consumeGoogleSigninNonce, exchangeGoogleAuthorizationCode, getSharedGraphUserId, GoogleIdTokenError, isGoogleSigninEnabled, verifyGoogleIdToken, } from "./services/google_oidc.js";
56
58
  import { AnalyzeSchemaCandidatesRequestSchema, AuditUndeclaredFragmentsRequestSchema, CorrectEntityRequestSchema, CreateInterpretationRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, IssuesBulkEntityIdsRequestSchema, IssuesAddMessageRequestSchema, IssuesGetStatusRequestSchema, IssuesSubmitRequestSchema, IssuesSyncRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, RELATIONSHIP_ENTITY_ID_FORMAT_HINT, RELATIONSHIP_ENTITY_ID_FORMAT_ISSUE_CODE, StoreRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
57
59
  import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
58
60
  import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
@@ -1095,7 +1097,47 @@ export function isLocalRequest(req) {
1095
1097
  return !isProductionEnvironment();
1096
1098
  }
1097
1099
  const OAUTH_KEY_SESSION_COOKIE = "neotoma_oauth_key_session";
1098
- const oauthKeySessions = new OAuthKeySessionStore();
1100
+ /** Exported for tests asserting the cookie maxAge and the server-side session
1101
+ * expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
1102
+ export const oauthKeySessions = new OAuthKeySessionStore();
1103
+ /**
1104
+ * Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
1105
+ * short default: a user who signs in expects to stay signed in, whereas the
1106
+ * key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
1107
+ * sign-in is what logged hosted-instance users out every quarter hour (#2005).
1108
+ *
1109
+ * Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
1110
+ */
1111
+ export const DEFAULT_SIGN_IN_SESSION_TTL_MIN = 7 * 24 * 60;
1112
+ /** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
1113
+ * this is a typo (an extra zero), not an intent — without a ceiling,
1114
+ * "9999999999999" yields a ~19-million-year session. Mirrors the low-end
1115
+ * guard rather than guarding only one side (#2007 qa lens). */
1116
+ export const MAX_SIGN_IN_SESSION_TTL_MIN = 365 * 24 * 60;
1117
+ /**
1118
+ * Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
1119
+ * so the override's precedence and malformed-input behavior are directly
1120
+ * testable — a silently-wrong TTL here reintroduces exactly the desync class
1121
+ * this change fixes.
1122
+ *
1123
+ * Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
1124
+ * Everything else — unset, non-numeric, zero, negative, or above the ceiling —
1125
+ * falls back to the 7-day default rather than producing a degenerate session.
1126
+ *
1127
+ * Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
1128
+ * and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
1129
+ * as-is because they are in-range integers after parsing; the fallback exists
1130
+ * for values that are absent, unparseable, or out of range, not to second-guess
1131
+ * a caller who wrote a decimal. Asserted in tests so the behavior is documented
1132
+ * rather than incidental.
1133
+ */
1134
+ export function resolveSignInSessionTtlMs(raw) {
1135
+ const parsed = Number.parseInt(raw || "", 10);
1136
+ const inRange = Number.isFinite(parsed) && parsed > 0 && parsed <= MAX_SIGN_IN_SESSION_TTL_MIN;
1137
+ const minutes = inRange ? parsed : DEFAULT_SIGN_IN_SESSION_TTL_MIN;
1138
+ return minutes * 60 * 1000;
1139
+ }
1140
+ const SIGN_IN_SESSION_TTL_MS = resolveSignInSessionTtlMs(process.env.NEOTOMA_SIGN_IN_SESSION_TTL_MIN);
1099
1141
  function readCookie(req, name) {
1100
1142
  const header = (req.headers["cookie"] || req.headers["Cookie"] || "");
1101
1143
  if (!header)
@@ -1321,8 +1363,15 @@ function hasValidOAuthKeySession(req) {
1321
1363
  const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
1322
1364
  return oauthKeySessions.isValid(token);
1323
1365
  }
1324
- function setOAuthKeySessionCookie(req, res) {
1325
- const token = oauthKeySessions.create();
1366
+ /**
1367
+ * Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
1368
+ * session and the cookie's own maxAge — they must agree, or the browser keeps
1369
+ * presenting a cookie the server has already forgotten (or vice versa).
1370
+ * Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
1371
+ * sign-in (#2005).
1372
+ */
1373
+ export function setOAuthKeySessionCookie(req, res, ttlMs) {
1374
+ const token = oauthKeySessions.create(Date.now(), ttlMs);
1326
1375
  const forwardedProto = (req.headers["x-forwarded-proto"] || req.headers["X-Forwarded-Proto"])
1327
1376
  ?.split(",")[0]
1328
1377
  ?.trim()
@@ -1333,8 +1382,16 @@ function setOAuthKeySessionCookie(req, res) {
1333
1382
  sameSite: "lax",
1334
1383
  secure,
1335
1384
  path: "/mcp/oauth",
1336
- maxAge: 15 * 60 * 1000,
1385
+ maxAge: ttlMs != null && ttlMs > 0 ? ttlMs : 15 * 60 * 1000,
1337
1386
  });
1387
+ return token;
1388
+ }
1389
+ /** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
1390
+ export function getGoogleVerifiedUserId(req) {
1391
+ const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
1392
+ // getBoundUser enforces session validity itself, so an expired session can
1393
+ // never resolve a user even if a binding is still in memory.
1394
+ return oauthKeySessions.getBoundUser(token);
1338
1395
  }
1339
1396
  /**
1340
1397
  * Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
@@ -1631,11 +1688,11 @@ app.all("/mcp", async (req, res) => {
1631
1688
  const hadSessionHeader = typeof sessionId === "string" && sessionId.length > 0;
1632
1689
  if (hadSessionHeader) {
1633
1690
  logger.warn(`[MCP HTTP] Unknown or expired Mcp-Session-Id (first 8 chars): ${sessionId.slice(0, 8)}... often wrong replica behind a load balancer, API restart, or stale client state`);
1634
- return res.status(503).json({
1691
+ return res.status(404).json({
1635
1692
  jsonrpc: "2.0",
1636
1693
  error: {
1637
1694
  code: -32001,
1638
- message: "Service Unavailable: MCP session is unknown on this API instance. If you run multiple replicas, enable sticky sessions for POST /mcp (or route /mcp to a single instance). Otherwise restart the MCP client so initialize runs again after a server restart.",
1695
+ message: "Not Found: MCP session is unknown or expired on this API instance. The client should re-initialize by sending a new InitializeRequest without a session ID. If you run multiple replicas, enable sticky sessions for POST /mcp (or route /mcp to a single instance).",
1639
1696
  },
1640
1697
  id: rpcId,
1641
1698
  });
@@ -1886,9 +1943,32 @@ function sendStoreValidationError(res, issues) {
1886
1943
  }
1887
1944
  return sendValidationError(res, issues);
1888
1945
  }
1946
+ /**
1947
+ * Lift a structured `hint` off the first Zod issue that carries one.
1948
+ *
1949
+ * Zod nests custom issue metadata under `params`, so a `ctx.addIssue({ params:
1950
+ * { hint } })` lands at `details.issues[].params.hint`. Clients (and the
1951
+ * legacy-payload replay harness, per docs/subsystems/errors.md § Tightening-change
1952
+ * hint obligation) read the migration hint from the flat `details.hint` slot that
1953
+ * every other standard-envelope error uses. Without this lift, a tightening can
1954
+ * populate `hint` exactly as the guardrail requires and still be invisible to the
1955
+ * gate meant to catch it.
1956
+ */
1957
+ function firstIssueHint(issues) {
1958
+ if (!Array.isArray(issues))
1959
+ return undefined;
1960
+ for (const issue of issues) {
1961
+ const hint = issue?.params?.hint;
1962
+ if (typeof hint === "string" && hint.length > 0)
1963
+ return hint;
1964
+ }
1965
+ return undefined;
1966
+ }
1889
1967
  function sendValidationError(res, issues) {
1968
+ const hint = firstIssueHint(issues);
1890
1969
  return res.status(400).json(buildErrorEnvelope("VALIDATION_INVALID_FORMAT", "Invalid request payload.", {
1891
1970
  issues,
1971
+ ...(hint ? { hint } : {}),
1892
1972
  }));
1893
1973
  }
1894
1974
  // Public health endpoint (no auth)
@@ -2104,10 +2184,19 @@ app.get("/mcp/oauth/key-auth", async (req, res) => {
2104
2184
  const nextPathEscaped = escapeHtml(nextPath);
2105
2185
  const hasBearerFallback = Boolean((process.env.NEOTOMA_BEARER_TOKEN || "").trim());
2106
2186
  const defaultCredentialMode = hasBearerFallback ? "bearer" : "private_key";
2187
+ const googleEnabled = isGoogleSigninEnabled();
2188
+ const googleStartHref = `/mcp/oauth/google/start?next=${encodeURIComponent(nextPath)}`;
2107
2189
  return res.send(renderOauthPage({
2108
2190
  title: "Authenticate OAuth preflight",
2109
2191
  subtitle: "Choose one credential path below, then continue to complete OAuth authorization.",
2110
2192
  contentHtml: `
2193
+ ${googleEnabled
2194
+ ? `<div class="actions" style="margin-bottom: 1rem;">
2195
+ <a class="btn-link" href="${escapeHtml(googleStartHref)}">Sign in with Google</a>
2196
+ </div>
2197
+ <div class="notice">Signs you in as your own Neotoma user if your Google account's verified email is on this server's approved list. Doesn't require a private key.</div>
2198
+ <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid hsl(var(--border));" />`
2199
+ : ""}
2111
2200
  <form method="post" action="/mcp/oauth/key-auth" id="oauth-key-auth-form">
2112
2201
  <input type="hidden" name="next" value="${nextPathEscaped}" />
2113
2202
  <input type="hidden" name="credential_mode" id="credential_mode" value="${defaultCredentialMode}" />
@@ -2213,6 +2302,109 @@ app.post("/mcp/oauth/key-auth", express.urlencoded({ extended: true }), async (r
2213
2302
  setOAuthKeySessionCookie(req, res);
2214
2303
  return res.redirect(nextPath);
2215
2304
  });
2305
+ /**
2306
+ * "Sign in with Google" entry point. Additive to, and independent of, the
2307
+ * key/mnemonic/bearer credential form above — feature-flagged off unless
2308
+ * NEOTOMA_GOOGLE_CLIENT_ID + NEOTOMA_APPROVED_EMAILS are both configured.
2309
+ * AUTH-LAYER ONLY: this establishes who the person is (their own user_id
2310
+ * via the existing per-email primitive); it does not touch data-sharing or
2311
+ * read-scoping.
2312
+ */
2313
+ app.get("/mcp/oauth/google/start", async (req, res) => {
2314
+ if (!isGoogleSigninEnabled()) {
2315
+ return res.status(404).send("Not found");
2316
+ }
2317
+ const nextPath = normalizeOauthNextPath(req.query.next || undefined);
2318
+ const nonce = createGoogleSigninNonce(nextPath);
2319
+ const redirectUri = `${req.protocol}://${req.get("host") || ""}/mcp/oauth/google/callback`;
2320
+ try {
2321
+ const authorizeUrl = buildGoogleAuthorizeUrl({ redirectUri, nonce });
2322
+ return res.redirect(authorizeUrl);
2323
+ }
2324
+ catch (error) {
2325
+ logError("GoogleOAuthStart", req, error);
2326
+ return res.status(500).send(error?.message ?? "Failed to start Google sign-in");
2327
+ }
2328
+ });
2329
+ app.get("/mcp/oauth/google/callback", async (req, res) => {
2330
+ if (!isGoogleSigninEnabled()) {
2331
+ return res.status(404).send("Not found");
2332
+ }
2333
+ const errorParam = req.query.error?.trim();
2334
+ const code = req.query.code?.trim();
2335
+ const nonceParam = req.query.state?.trim();
2336
+ const failWith = (subtitle) => {
2337
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2338
+ return res.status(401).send(renderOauthPage({
2339
+ title: "Google sign-in failed",
2340
+ subtitle,
2341
+ contentHtml: `<div class="actions">
2342
+ <a class="btn-link secondary" href="/mcp/oauth/key-auth">Back to authentication</a>
2343
+ </div>`,
2344
+ }));
2345
+ };
2346
+ if (errorParam) {
2347
+ logger.warn("[Google OAuth] Callback returned an error", { error: errorParam });
2348
+ return failWith("Google sign-in was cancelled or denied.");
2349
+ }
2350
+ if (!code || !nonceParam) {
2351
+ return failWith("Missing code or state from Google callback.");
2352
+ }
2353
+ const nonceEntry = consumeGoogleSigninNonce(nonceParam);
2354
+ if (!nonceEntry) {
2355
+ return failWith("This sign-in link has expired or was already used. Please try again.");
2356
+ }
2357
+ // Same nextPath contract as the key/mnemonic/bearer path above: normalized
2358
+ // back to a /mcp/oauth/* path (typically /mcp/oauth/authorize?...), never
2359
+ // an external URL.
2360
+ const nextPath = normalizeOauthNextPath(nonceEntry.next);
2361
+ const redirectUri = `${req.protocol}://${req.get("host") || ""}/mcp/oauth/google/callback`;
2362
+ try {
2363
+ const { idToken } = await exchangeGoogleAuthorizationCode({ code, redirectUri });
2364
+ const { email } = await verifyGoogleIdToken(idToken);
2365
+ // Resolve the verified email to a stable user_id via the EXISTING per-email
2366
+ // primitive (local_auth.ts) — the same primitive createLocalAuthUser /
2367
+ // hashEmailToUserId already uses elsewhere. By default this person is
2368
+ // admitted as their own isolated user_id, same as any other local-auth user.
2369
+ const { createLocalAuthUser } = await import("./services/local_auth.js");
2370
+ const { randomBytes } = await import("node:crypto");
2371
+ // Password is unused for Google-authenticated users (they never log in
2372
+ // with it) — generate an unguessable throwaway so createLocalAuthUser's
2373
+ // shared code path is satisfied without weakening any other account.
2374
+ const throwawayPassword = randomBytes(32).toString("hex");
2375
+ const perEmailUser = createLocalAuthUser(email, throwawayPassword);
2376
+ // Shared-graph opt-in: when NEOTOMA_SHARED_GRAPH_USER_ID is set, an
2377
+ // approved teammate (the email already passed the allowlist inside
2378
+ // verifyGoogleIdToken) is bound to the shared graph's user_id instead of
2379
+ // their isolated per-email one — so the whole allowlisted team, plus the
2380
+ // bearer/MCP identity that owns the data, share one populated graph. Unset
2381
+ // → isolated per-email behavior (unchanged). See getSharedGraphUserId.
2382
+ const resolvedUserId = getSharedGraphUserId() ?? perEmailUser.id;
2383
+ // Mark this browser session as Google-verified and bound to the resolved
2384
+ // user_id, exactly the same OAuth key-session cookie the private-key/
2385
+ // mnemonic/bearer path sets. `/mcp/oauth/authorize` -> `/mcp/oauth/
2386
+ // local-login` then runs completely unmodified, except local-login now
2387
+ // resolves the user via this session instead of always ensureLocalDevUser().
2388
+ // Sign-in gets the long session TTL, not the key-entry gate's 15 minutes,
2389
+ // and the user binding lives in the same store so it expires with it (#2005).
2390
+ const sessionToken = setOAuthKeySessionCookie(req, res, SIGN_IN_SESSION_TTL_MS);
2391
+ oauthKeySessions.bindUser(sessionToken, resolvedUserId);
2392
+ return res.redirect(nextPath);
2393
+ }
2394
+ catch (error) {
2395
+ logError("GoogleOAuthCallback", req, error);
2396
+ if (error instanceof GoogleIdTokenError) {
2397
+ if (error.code === "EMAIL_NOT_APPROVED") {
2398
+ return failWith("Your Google account is not on the approved list for this server.");
2399
+ }
2400
+ if (error.code === "EMAIL_NOT_VERIFIED") {
2401
+ return failWith("Your Google account's email is not verified.");
2402
+ }
2403
+ return failWith("Google sign-in could not be verified.");
2404
+ }
2405
+ return failWith(error?.message ?? "Google sign-in failed.");
2406
+ }
2407
+ });
2216
2408
  // RFC 8414 authorization endpoint (GET) for Cursor and other OAuth clients
2217
2409
  app.get("/mcp/oauth/authorize", async (req, res) => {
2218
2410
  try {
@@ -2263,7 +2455,10 @@ app.get("/mcp/oauth/authorize", async (req, res) => {
2263
2455
  // When reached via tunnel, only allow redirect_uri to localhost, app schemes, or OpenAI Custom GPT
2264
2456
  if (!isLocalRequest(req)) {
2265
2457
  const { isRedirectUriAllowedForTunnel } = await import("./services/mcp_oauth.js");
2266
- if (!isRedirectUriAllowedForTunnel(redirect_uri)) {
2458
+ // Public host as seen from outside the tunnel (x-forwarded-host set by the
2459
+ // proxy) so an Inspector callback on this instance's own origin is allowed.
2460
+ const selfHost = req.header("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
2461
+ if (!isRedirectUriAllowedForTunnel(redirect_uri, selfHost)) {
2267
2462
  logger.warn("[MCP OAuth] Authorize rejected: redirect_uri not allowed for tunnel", {
2268
2463
  redirect_uri: sanitizeRedirectUriForLog(redirect_uri),
2269
2464
  });
@@ -2378,9 +2573,15 @@ app.get("/mcp/oauth/local-login", async (req, res) => {
2378
2573
  }));
2379
2574
  }
2380
2575
  try {
2381
- const devUser = ensureLocalDevUser();
2576
+ // If this browser session was admitted via Google sign-in (see
2577
+ // /mcp/oauth/google/callback), complete authorization as THAT verified
2578
+ // user's own user_id instead of the shared dev user. Every other path
2579
+ // (no Google session on this cookie — the default, and the only
2580
+ // possibility when the Google feature flag is off) is unchanged.
2581
+ const googleUserId = getGoogleVerifiedUserId(req);
2582
+ const resolvedUserId = googleUserId ?? ensureLocalDevUser().id;
2382
2583
  const { completeLocalAuthorization } = await import("./services/mcp_oauth.js");
2383
- const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(state, devUser.id);
2584
+ const { connectionId, redirectUri, clientState } = await completeLocalAuthorization(state, resolvedUserId);
2384
2585
  const frontendBase = process.env.NEOTOMA_FRONTEND_URL || process.env.FRONTEND_URL || "http://localhost:5195";
2385
2586
  const frontendOauth = `${frontendBase}/oauth`;
2386
2587
  if (redirectUri) {
@@ -2734,6 +2935,23 @@ function guestWriteRateLimit(req, res, next) {
2734
2935
  return next();
2735
2936
  void guestWriteRateLimitMiddleware(req, res, next);
2736
2937
  }
2938
+ /**
2939
+ * Issue *submission* is the one route a third party may reach with no identity
2940
+ * at all: the guest access token is minted BY the submit, so requiring one to
2941
+ * perform it is a chicken-and-egg that closes the inbox to first contact.
2942
+ *
2943
+ * Deliberately submit-only. `add_message` stays authenticated: by the time a
2944
+ * reporter has a thread to append to, they hold the token returned by their
2945
+ * submit, so anonymity buys nothing there — and letting an unidentified caller
2946
+ * write into an existing thread is a strictly wider blast radius than letting
2947
+ * them open a new one. The token is what proves "I opened this thread".
2948
+ */
2949
+ export function routeAcceptsAnonymousIssueSubmit(req) {
2950
+ return (req.method === "POST" && (req.path === "/issues/submit" || req.path === "/api/issues/submit"));
2951
+ }
2952
+ function buildAnonymousGuestPrincipal() {
2953
+ return { kind: "guest", guestId: {}, agentIdentity: null, accessToken: undefined };
2954
+ }
2737
2955
  function buildGuestPrincipalFromRequest(req) {
2738
2956
  const agentIdentity = getCurrentAgentIdentity();
2739
2957
  const accessToken = guestAccessTokenFromRequest(req);
@@ -2757,8 +2975,21 @@ async function maybeStampGuestPrincipal(req) {
2757
2975
  if (!routeAcceptsGuestPrincipal(req))
2758
2976
  return false;
2759
2977
  const guestPrincipal = buildGuestPrincipalFromRequest(req);
2760
- if (!guestPrincipal)
2761
- return false;
2978
+ if (!guestPrincipal) {
2979
+ // No AAuth signature and no guest token: for issue submission this is the
2980
+ // NORMAL first contact from a third party's agent, since the token is an
2981
+ // OUTPUT of the submit rather than a precondition for it (#1953). Remains
2982
+ // the operator's switch — NEOTOMA_ACCESS_POLICY_ISSUE=closed shuts it.
2983
+ if (!routeAcceptsAnonymousIssueSubmit(req))
2984
+ return false;
2985
+ const { resolveAccessPolicy } = await import("./services/access_policy.js");
2986
+ const mode = await resolveAccessPolicy("issue");
2987
+ if (mode !== "submit_only" && mode !== "submitter_scoped" && mode !== "open") {
2988
+ return false;
2989
+ }
2990
+ stampGuestPrincipal(req, buildAnonymousGuestPrincipal());
2991
+ return true;
2992
+ }
2762
2993
  const headerAuth = (req.headers.authorization || req.headers.Authorization || "");
2763
2994
  if (headerAuth.startsWith("Bearer ") && guestPrincipal.accessToken) {
2764
2995
  const { validateGuestAccessToken } = await import("./services/guest_access_token.js");
@@ -3321,6 +3552,12 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
3321
3552
  .status(403)
3322
3553
  .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
3323
3554
  }
3555
+ if (error instanceof CursorError) {
3556
+ logWarn(logContext || "CursorRejection", req, error.toErrorEnvelope());
3557
+ return res
3558
+ .status(error.statusCode)
3559
+ .json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
3560
+ }
3324
3561
  if (error instanceof IssueValidationError) {
3325
3562
  logWarn(logContext || "IssueValidationError", req, {
3326
3563
  code: error.code,
@@ -3371,8 +3608,8 @@ async function runEntitiesQuery(req, res, rawInput) {
3371
3608
  // scoping only via getAuthenticatedUserId; it is never read directly off
3372
3609
  // the request for access control.
3373
3610
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
3374
- const { entity_type, entity_types, search, limit, offset, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
3375
- const { entities, total, applied_search_strategies, search_mode } = await queryEntitiesWithCount({
3611
+ const { entity_type, entity_types, search, limit, offset, cursor, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
3612
+ const { entities, total, applied_search_strategies, search_mode, next_cursor } = await queryEntitiesWithCount({
3376
3613
  userId,
3377
3614
  entityType: entity_type,
3378
3615
  entityTypes: entity_types,
@@ -3386,6 +3623,7 @@ async function runEntitiesQuery(req, res, rawInput) {
3386
3623
  search,
3387
3624
  limit,
3388
3625
  offset,
3626
+ cursor,
3389
3627
  updatedSince: updated_since,
3390
3628
  createdSince: created_since,
3391
3629
  identityBasis: identity_basis,
@@ -3399,6 +3637,7 @@ async function runEntitiesQuery(req, res, rawInput) {
3399
3637
  offset,
3400
3638
  ...(applied_search_strategies ? { applied_search_strategies } : {}),
3401
3639
  search_mode,
3640
+ ...(next_cursor ? { next_cursor } : {}),
3402
3641
  });
3403
3642
  }
3404
3643
  catch (error) {
@@ -3439,6 +3678,7 @@ function coerceEntitiesQueryParams(query) {
3439
3678
  "updated_since",
3440
3679
  "created_since",
3441
3680
  "identity_basis",
3681
+ "cursor",
3442
3682
  ]) {
3443
3683
  const value = readString(key);
3444
3684
  if (value !== undefined)
@@ -5834,6 +6074,12 @@ export async function storeStructuredForApi(params) {
5834
6074
  // entity_id.
5835
6075
  const priorSnapshotByEntityId = new Map();
5836
6076
  const newSnapshotByEntityId = new Map();
6077
+ // #1963: auto-link may retract a stale reference-field edge (e.g. a contact's
6078
+ // organization moved companies). A retraction *failure* must not be silent —
6079
+ // that would reproduce the very silent-drift bug this fix exists to close, one
6080
+ // layer up. Accumulate per-observation retraction outcomes here so the
6081
+ // schemaStoreWarnings pass below can surface them on the store response.
6082
+ const autoLinkRetractionByObsIndex = new Map();
5837
6083
  for (const r of resolved) {
5838
6084
  let observation_id = null;
5839
6085
  let snapshotAfter = null;
@@ -5917,14 +6163,25 @@ export async function storeStructuredForApi(params) {
5917
6163
  const schemaEntry = await schemaRegistry.loadActiveSchema(r.entity_type, userId);
5918
6164
  if (schemaEntry?.schema_definition?.reference_fields?.length) {
5919
6165
  const { autoLinkReferenceFields } = await import("./services/schema_reference_linking.js");
5920
- await autoLinkReferenceFields({
6166
+ const autoLinkResult = await autoLinkReferenceFields({
5921
6167
  entityId: r.entity_id,
5922
6168
  entityType: r.entity_type,
5923
6169
  fields: r.fields,
5924
6170
  schema: schemaEntry.schema_definition,
5925
6171
  userId,
5926
6172
  sourceId: observationSourceId,
6173
+ commit,
5927
6174
  });
6175
+ if (autoLinkResult.retracted > 0 || autoLinkResult.retraction_failures > 0) {
6176
+ autoLinkRetractionByObsIndex.set(r.observation_index, {
6177
+ entity_type: r.entity_type,
6178
+ entity_id: r.entity_id,
6179
+ retracted: autoLinkResult.retracted,
6180
+ retraction_failures: autoLinkResult.retraction_failures,
6181
+ failed_target_entity_ids: autoLinkResult.failed_retraction_target_entity_ids,
6182
+ failed_retractions: autoLinkResult.failed_retractions,
6183
+ });
6184
+ }
5928
6185
  }
5929
6186
  }
5930
6187
  catch (linkErr) {
@@ -6254,6 +6511,46 @@ export async function storeStructuredForApi(params) {
6254
6511
  }
6255
6512
  }
6256
6513
  }
6514
+ // #1963: surface auto-link edge retractions on the store response so a
6515
+ // retraction — and especially a retraction *failure* — is never silent. A
6516
+ // failure means a stale reference-field edge may still be live despite the
6517
+ // store reporting success, so it is emitted as its own actionable warning.
6518
+ for (const [obsIndex, rec] of autoLinkRetractionByObsIndex) {
6519
+ if (rec.retraction_failures > 0) {
6520
+ schemaStoreWarnings.push({
6521
+ code: "AUTO_LINK_RETRACTION_FAILED",
6522
+ message: `Auto-link retraction failed for ${rec.retraction_failures} stale ` +
6523
+ `reference-field edge(s) on ${rec.entity_type}/${rec.entity_id}. ` +
6524
+ `A superseded edge (e.g. a prior works_at) may still be live even though ` +
6525
+ `the store succeeded. To clean up, call delete_relationship for each ` +
6526
+ `stale edge in details.stale_edges, or re-run the store.`,
6527
+ observation_index: obsIndex,
6528
+ entity_type: rec.entity_type,
6529
+ entity_id: rec.entity_id,
6530
+ // Structured remediation: each entry is a ready-to-use delete_relationship
6531
+ // argument set, so the caller needs no knowledge of its own schema.
6532
+ details: {
6533
+ stale_edges: rec.failed_retractions.map((f) => ({
6534
+ source_entity_id: rec.entity_id,
6535
+ target_entity_id: f.target_entity_id,
6536
+ relationship_type: f.relationship_type,
6537
+ field_name: f.field,
6538
+ })),
6539
+ },
6540
+ });
6541
+ }
6542
+ else if (rec.retracted > 0) {
6543
+ schemaStoreWarnings.push({
6544
+ code: "AUTO_LINK_EDGE_RETRACTED",
6545
+ message: `Retracted ${rec.retracted} stale auto-linked reference-field edge(s) on ` +
6546
+ `${rec.entity_type}/${rec.entity_id} because the field's resolved target ` +
6547
+ `changed (#1963).`,
6548
+ observation_index: obsIndex,
6549
+ entity_type: rec.entity_type,
6550
+ entity_id: rec.entity_id,
6551
+ });
6552
+ }
6553
+ }
6257
6554
  const unknownFieldNames = Array.from(unknownFieldNamesSet).sort();
6258
6555
  // Conditional hint (#1549): update_schema_incremental requires the schema to
6259
6556
  // declare canonical_name_fields, else it dead-ends on
@@ -7879,6 +8176,10 @@ const handleIssuesSubmitHttp = async (req, res) => {
7879
8176
  author: parsed.data.author,
7880
8177
  local_issue_id: parsed.data.local_issue_id,
7881
8178
  submission_timestamp: parsed.data.submission_timestamp,
8179
+ reporter_git_sha: parsed.data.reporter_git_sha,
8180
+ reporter_git_ref: parsed.data.reporter_git_ref,
8181
+ reporter_channel: parsed.data.reporter_channel,
8182
+ reporter_app_version: parsed.data.reporter_app_version,
7882
8183
  });
7883
8184
  return {
7884
8185
  issue_number: 0,
@@ -8261,7 +8562,7 @@ app.post("/update_schema_incremental", async (req, res) => {
8261
8562
  }
8262
8563
  try {
8263
8564
  const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
8264
- const { entity_type, fields_to_add, fields_to_remove, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
8565
+ const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
8265
8566
  const { schemaRegistry } = await import("./services/schema_registry.js");
8266
8567
  let newSchema;
8267
8568
  try {
@@ -8269,6 +8570,11 @@ app.post("/update_schema_incremental", async (req, res) => {
8269
8570
  entity_type,
8270
8571
  fields_to_add: (fields_to_add || []),
8271
8572
  fields_to_remove: fields_to_remove || [],
8573
+ // #2018: thread the identity-rule change through the HTTP/offline path.
8574
+ // Omitted here originally, so CLI/HTTP callers silently no-op'd the
8575
+ // re-key (the MCP server handler had it, but this Express route — which
8576
+ // the CLI offline transport uses — did not).
8577
+ canonical_name_fields,
8272
8578
  schema_version,
8273
8579
  user_specific,
8274
8580
  user_id: userId,
@@ -8295,6 +8601,10 @@ app.post("/update_schema_incremental", async (req, res) => {
8295
8601
  schema: newSchema,
8296
8602
  schema_version: newSchema.schema_version,
8297
8603
  fields_removed: fields_to_remove || [],
8604
+ // Echo the resolved identity rule so CLI/HTTP callers can confirm the
8605
+ // re-key without a second describe_entity_type round trip (#2020 ux).
8606
+ canonical_name_fields: newSchema.schema_definition
8607
+ .canonical_name_fields ?? null,
8298
8608
  });
8299
8609
  }
8300
8610
  catch (error) {