neotoma 0.8.0 → 0.9.1
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 +6 -1
- package/dist/actions.d.ts +10 -5
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +306 -43
- package/dist/actions.js.map +1 -1
- package/dist/cli/bootstrap.js +0 -0
- package/dist/cli/hooks.d.ts +4 -4
- package/dist/cli/hooks.js +4 -4
- package/dist/cli/hooks_detect.d.ts.map +1 -1
- package/dist/cli/hooks_detect.js +16 -9
- package/dist/cli/hooks_detect.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +91 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp_config_scan.d.ts +10 -0
- package/dist/cli/mcp_config_scan.d.ts.map +1 -1
- package/dist/cli/mcp_config_scan.js +85 -5
- package/dist/cli/mcp_config_scan.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +71 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/triage.d.ts.map +1 -1
- package/dist/cli/triage.js +9 -0
- package/dist/cli/triage.js.map +1 -1
- package/dist/core/operations.d.ts +9 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +3 -0
- package/dist/core/operations.js.map +1 -1
- package/dist/inspector/assets/Combination-BP0-kPZX.js +41 -0
- package/dist/inspector/assets/agent_badge-BZT-JO2h.js +1 -0
- package/dist/inspector/assets/agent_detail-BGMLF8-n.js +1 -0
- package/dist/inspector/assets/agent_filter-DMC4CzhM.js +1 -0
- package/dist/inspector/assets/agent_grant_detail-Brqy5K7M.js +1 -0
- package/dist/inspector/assets/agent_grant_form-BLDkUh1Y.js +1 -0
- package/dist/inspector/assets/agent_grants-B2StunRb.js +1 -0
- package/dist/inspector/assets/agents-lYmKs-fG.js +1 -0
- package/dist/inspector/assets/arrow-left-D1s7DOns.js +6 -0
- package/dist/inspector/assets/attribution_card-D-bp008l.js +1 -0
- package/dist/inspector/assets/attribution_summary-Cs9Ccvt3.js +1 -0
- package/dist/inspector/assets/card-Btqgzh6p.js +1 -0
- package/dist/inspector/assets/check-DAyRtq63.js +6 -0
- package/dist/inspector/assets/checkbox-BrpwHaRo.js +1 -0
- package/dist/inspector/assets/chevron-down-CCk_jMPN.js +6 -0
- package/dist/inspector/assets/chevron-right-C9NbdZtC.js +6 -0
- package/dist/inspector/assets/compliance-BHiHtgfg.js +1 -0
- package/dist/inspector/assets/confirm-dialog-BcxtVONz.js +6 -0
- package/dist/inspector/assets/conversation_common-Dw5j3QuN.js +1 -0
- package/dist/inspector/assets/conversation_detail-BR_rBMFV.js +1 -0
- package/dist/inspector/assets/copy_id_button-CyjfY7dx.js +6 -0
- package/dist/inspector/assets/corrections-DFExbcsm.js +1 -0
- package/dist/inspector/assets/dashboard-CxnNRphy.js +73 -0
- package/dist/inspector/assets/data-table-CazqdSem.js +22 -0
- package/dist/inspector/assets/dialog-X5X7rLah.js +10 -0
- package/dist/inspector/assets/dropdown-menu-CmZjxUWM.js +6 -0
- package/dist/inspector/assets/entities-l6icu6fc.js +1 -0
- package/dist/inspector/assets/entity_detail-daoxB-h1.js +17 -0
- package/dist/inspector/assets/entity_link-DtMv__WC.js +1 -0
- package/dist/inspector/assets/external-link-BBoTnT-P.js +6 -0
- package/dist/inspector/assets/feedback-DeFhdWId.js +35 -0
- package/dist/inspector/assets/graph_explorer-BZV40eAE.css +1 -0
- package/dist/inspector/assets/graph_explorer-BvicLJEW.js +23 -0
- package/dist/inspector/assets/index-B2zHigxN.js +199 -0
- package/dist/inspector/assets/index-Czej0Y93.js +1 -0
- package/dist/inspector/assets/index-D5i6AEXI.js +1 -0
- package/dist/inspector/assets/index-DJuPlRtP.js +1 -0
- package/dist/inspector/assets/index-Df569_c9.css +1 -0
- package/dist/inspector/assets/interpretations-D9gWqVhy.js +1 -0
- package/dist/inspector/assets/interpretations-E0sIBf-l.js +1 -0
- package/dist/inspector/assets/json_viewer-ojLDPDtf.js +1 -0
- package/dist/inspector/assets/label-DWyQNl4E.js +1 -0
- package/dist/inspector/assets/live_relative_time-DzLnsA9y.js +1 -0
- package/dist/inspector/assets/observations-Ds0kFmaM.js +1 -0
- package/dist/inspector/assets/page_shell-C-4AKr0Y.js +1 -0
- package/dist/inspector/assets/pagination-lSg-a95h.js +6 -0
- package/dist/inspector/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/dist/inspector/assets/plus-CQMoR71F.js +6 -0
- package/dist/inspector/assets/query_loading-BFETHugg.js +1 -0
- package/dist/inspector/assets/query_refresh_indicator-Bewf0Dj1.js +1 -0
- package/dist/inspector/assets/recent_activity-Csh_YraY.js +11 -0
- package/dist/inspector/assets/recent_conversations-CBengQjb.js +1 -0
- package/dist/inspector/assets/recent_conversations-MKmxYevd.js +1 -0
- package/dist/inspector/assets/recent_records_feed-CCrBRfkG.js +1 -0
- package/dist/inspector/assets/relationship_detail-CUz_GhPI.js +1 -0
- package/dist/inspector/assets/relationships-Ulajo16_.js +1 -0
- package/dist/inspector/assets/relationships-Z9ALu9Oa.js +1 -0
- package/dist/inspector/assets/sandbox-CfD5QvC5.js +1 -0
- package/dist/inspector/assets/schema_detail-B1W8rbzV.js +11 -0
- package/dist/inspector/assets/schemas-DVlEFPuf.js +5 -0
- package/dist/inspector/assets/search-BccQXyTN.js +1 -0
- package/dist/inspector/assets/select-Dv1QM6oO.js +6 -0
- package/dist/inspector/assets/settings-B4U8tFYI.js +1 -0
- package/dist/inspector/assets/source_detail-B1JlZBBx.js +17 -0
- package/dist/inspector/assets/source_link-p8KzI1os.js +1 -0
- package/dist/inspector/assets/sources-B5ssCN-s.js +9 -0
- package/dist/inspector/assets/switch-BPI_y_Z3.js +1 -0
- package/dist/inspector/assets/tabs-HUG-sxc2.js +1 -0
- package/dist/inspector/assets/textarea-DNz92WE1.js +1 -0
- package/dist/inspector/assets/timeline-DRqxyQUF.js +1 -0
- package/dist/inspector/assets/timeline-HN2EoMGt.js +1 -0
- package/dist/inspector/assets/timeline_event_detail-DbA5EpiN.js +1 -0
- package/dist/inspector/assets/trash-2-BAfHKatZ.js +6 -0
- package/dist/inspector/assets/turn_detail-BUHzIhWX.js +1 -0
- package/dist/inspector/assets/turns-ADRkuqKL.js +1 -0
- package/dist/inspector/assets/use_agents-CTZrpsvS.js +1 -0
- package/dist/inspector/assets/use_entities-BEhy6HWn.js +1 -0
- package/dist/inspector/assets/use_interpretations-CKN63UxX.js +1 -0
- package/dist/inspector/assets/use_mutations-B4y1qmV5.js +1 -0
- package/dist/inspector/assets/use_recent_conversations-CIBgmz9B.js +1 -0
- package/dist/inspector/assets/use_relationships-Cl-o_7u6.js +1 -0
- package/dist/inspector/assets/use_schemas-Bl11WNgP.js +1 -0
- package/dist/inspector/assets/use_sources-DkJZZBDp.js +1 -0
- package/dist/inspector/assets/use_stats-Cn1a3yt-.js +1 -0
- package/dist/inspector/assets/use_timeline-DZkbwA-7.js +1 -0
- package/dist/inspector/assets/use_turns-BHfaal9v.js +1 -0
- package/dist/inspector/assets/value-BTdN53H7.js +1 -0
- package/dist/inspector/favicon.svg +10 -0
- package/dist/inspector/index.html +14 -0
- package/dist/mcp_dev_shim.d.ts +17 -0
- package/dist/mcp_dev_shim.d.ts.map +1 -0
- package/dist/mcp_dev_shim.js +324 -0
- package/dist/mcp_dev_shim.js.map +1 -0
- package/dist/mcp_server_card.js +1 -1
- package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.js +12 -0
- package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
- package/dist/scripts/seed_sandbox.js +11 -1
- package/dist/scripts/seed_sandbox.js.map +1 -1
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +118 -37
- package/dist/server.js.map +1 -1
- package/dist/services/compliance/scorecard.d.ts +35 -73
- package/dist/services/compliance/scorecard.d.ts.map +1 -1
- package/dist/services/compliance/scorecard.js +234 -270
- package/dist/services/compliance/scorecard.js.map +1 -1
- package/dist/services/conversation_turn.d.ts +44 -37
- package/dist/services/conversation_turn.d.ts.map +1 -1
- package/dist/services/conversation_turn.js +248 -362
- package/dist/services/conversation_turn.js.map +1 -1
- package/dist/services/feedback/admin_proxy.d.ts +21 -7
- package/dist/services/feedback/admin_proxy.d.ts.map +1 -1
- package/dist/services/feedback/admin_proxy.js +142 -16
- package/dist/services/feedback/admin_proxy.js.map +1 -1
- package/dist/services/feedback/mirror_local_to_entity.d.ts +20 -53
- package/dist/services/feedback/mirror_local_to_entity.d.ts.map +1 -1
- package/dist/services/feedback/mirror_local_to_entity.js +46 -76
- package/dist/services/feedback/mirror_local_to_entity.js.map +1 -1
- package/dist/services/feedback/neotoma_payload.d.ts +72 -33
- package/dist/services/feedback/neotoma_payload.d.ts.map +1 -1
- package/dist/services/feedback/neotoma_payload.js +12 -24
- package/dist/services/feedback/neotoma_payload.js.map +1 -1
- package/dist/services/feedback_transport_local.d.ts.map +1 -1
- package/dist/services/feedback_transport_local.js +4 -0
- package/dist/services/feedback_transport_local.js.map +1 -1
- package/dist/services/inspector_mount.d.ts +32 -70
- package/dist/services/inspector_mount.d.ts.map +1 -1
- package/dist/services/inspector_mount.js +219 -183
- package/dist/services/inspector_mount.js.map +1 -1
- package/dist/services/recent_conversations.d.ts +9 -0
- package/dist/services/recent_conversations.d.ts.map +1 -1
- package/dist/services/recent_conversations.js +106 -14
- package/dist/services/recent_conversations.js.map +1 -1
- package/dist/services/root_landing/harness_snippets.d.ts +2 -0
- package/dist/services/root_landing/harness_snippets.d.ts.map +1 -1
- package/dist/services/root_landing/harness_snippets.js +15 -8
- package/dist/services/root_landing/harness_snippets.js.map +1 -1
- package/dist/services/root_landing/html_template.d.ts +9 -0
- package/dist/services/root_landing/html_template.d.ts.map +1 -1
- package/dist/services/root_landing/html_template.js +66 -1
- package/dist/services/root_landing/html_template.js.map +1 -1
- package/dist/services/root_landing/index.d.ts +11 -0
- package/dist/services/root_landing/index.d.ts.map +1 -1
- package/dist/services/root_landing/index.js +26 -8
- package/dist/services/root_landing/index.js.map +1 -1
- package/dist/services/root_landing/md_template.d.ts.map +1 -1
- package/dist/services/root_landing/md_template.js +22 -2
- package/dist/services/root_landing/md_template.js.map +1 -1
- package/dist/services/sandbox/pack_registry.d.ts +6 -50
- package/dist/services/sandbox/pack_registry.d.ts.map +1 -1
- package/dist/services/sandbox/pack_registry.js +74 -86
- package/dist/services/sandbox/pack_registry.js.map +1 -1
- package/dist/services/sandbox/seeder.d.ts +8 -47
- package/dist/services/sandbox/seeder.d.ts.map +1 -1
- package/dist/services/sandbox/seeder.js +51 -89
- package/dist/services/sandbox/seeder.js.map +1 -1
- package/dist/services/sandbox/sessions.d.ts +23 -114
- package/dist/services/sandbox/sessions.d.ts.map +1 -1
- package/dist/services/sandbox/sessions.js +99 -288
- package/dist/services/sandbox/sessions.js.map +1 -1
- package/dist/services/schema_definitions.d.ts.map +1 -1
- package/dist/services/schema_definitions.js +180 -3
- package/dist/services/schema_definitions.js.map +1 -1
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +28 -2
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +163 -16
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +28 -10
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/contract_mappings.d.ts.map +1 -1
- package/dist/shared/contract_mappings.js +23 -0
- package/dist/shared/contract_mappings.js.map +1 -1
- package/dist/shared/openapi_types.d.ts +245 -16
- package/dist/shared/openapi_types.d.ts.map +1 -1
- package/dist/tool_definitions.d.ts +1 -1
- package/dist/tool_definitions.d.ts.map +1 -1
- package/dist/tool_definitions.js +6 -0
- package/dist/tool_definitions.js.map +1 -1
- package/openapi.yaml +291 -28
- package/package.json +26 -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
|
|
@@ -186,6 +188,7 @@ Neotoma stores user data and requires secure configuration.
|
|
|
186
188
|
|
|
187
189
|
```bash
|
|
188
190
|
npm run dev # MCP server (stdio)
|
|
191
|
+
npm run dev:mcp:dev-shim # stable stdio shim for MCP source iteration
|
|
189
192
|
npm run dev:ui # Frontend
|
|
190
193
|
npm run dev:server # API only (MCP at /mcp)
|
|
191
194
|
npm run dev:full # API + UI + build watch
|
|
@@ -216,7 +219,9 @@ Neotoma exposes state via MCP. Local storage only in preview. Local built-in aut
|
|
|
216
219
|
|
|
217
220
|
**Setup guides:** [Cursor](https://neotoma.io/neotoma-with-cursor) · [Claude Code](https://neotoma.io/neotoma-with-claude-code) · [Claude](https://neotoma.io/neotoma-with-claude) · [ChatGPT](https://neotoma.io/neotoma-with-chatgpt) · [Codex](https://neotoma.io/neotoma-with-codex) · [OpenClaw](https://neotoma.io/neotoma-with-openclaw)
|
|
218
221
|
|
|
219
|
-
|
|
222
|
+
For local source iteration, use the stable dev shim (`scripts/run_neotoma_mcp_stdio_dev_shim.sh` or `npm run dev:mcp:dev-shim`) instead of pointing installed MCP clients at a `tsx watch` stdio process. The shim keeps the client-facing JSON-RPC stream stable and asks clients to refresh or reconnect when the tool interface changes.
|
|
223
|
+
|
|
224
|
+
**Agent behavior contract:** Store first, retrieve before storing, extract entities from user input, create tasks for commitments, and attach bounded host context such as repository name/root scope when available. Full instructions: [MCP instructions](docs/developer/mcp/instructions.md) and [CLI agent instructions](docs/developer/cli_agent_instructions.md).
|
|
220
225
|
|
|
221
226
|
**Representative actions:** `store`, `retrieve_entities`, `retrieve_entity_snapshot`, `merge_entities`, `list_observations`, `create_relationship`, `list_relationships`, `list_timeline_events`, `retrieve_graph_neighborhood`. Full list: [MCP spec](docs/specs/MCP_SPEC.md).
|
|
222
227
|
|
package/dist/actions.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export declare function isLocalRequest(req: express.Request): boolean;
|
|
|
22
22
|
* a different authority. See src/middleware/aauth_verify.ts.
|
|
23
23
|
*/
|
|
24
24
|
export declare function canonicalAauthAuthority(): string;
|
|
25
|
+
type StoreRelationshipRef = {
|
|
26
|
+
relationship_type: string;
|
|
27
|
+
source_index?: number;
|
|
28
|
+
target_index?: number;
|
|
29
|
+
source_entity_id?: string;
|
|
30
|
+
target_entity_id?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
};
|
|
25
33
|
export declare function storeStructuredForApi(params: {
|
|
26
34
|
userId: string;
|
|
27
35
|
entities: Record<string, unknown>[];
|
|
@@ -29,11 +37,7 @@ export declare function storeStructuredForApi(params: {
|
|
|
29
37
|
observationSource?: import("./shared/action_schemas.js").ObservationSource;
|
|
30
38
|
idempotencyKey: string;
|
|
31
39
|
originalFilename?: string;
|
|
32
|
-
relationships?:
|
|
33
|
-
relationship_type: string;
|
|
34
|
-
source_index: number;
|
|
35
|
-
target_index: number;
|
|
36
|
-
}>;
|
|
40
|
+
relationships?: StoreRelationshipRef[];
|
|
37
41
|
commit?: boolean;
|
|
38
42
|
strict?: boolean;
|
|
39
43
|
}): Promise<{
|
|
@@ -92,4 +96,5 @@ export declare function startHTTPServer(): Promise<{
|
|
|
92
96
|
server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
93
97
|
port: number;
|
|
94
98
|
} | undefined>;
|
|
99
|
+
export {};
|
|
95
100
|
//# sourceMappingURL=actions.d.ts.map
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAmK9B,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;AAyrHD,KAAK,oBAAoB,GAAG;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,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,oBAAoB,EAAE,CAAC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;cA+fS,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;;;;;mBA3JV,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;;GA2F3B;AAuhED,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,13 +35,15 @@ 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 {
|
|
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";
|
|
41
44
|
import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
|
|
42
45
|
import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
|
|
43
|
-
import { AnalyzeSchemaCandidatesRequestSchema, CorrectEntityRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, StoreRequestSchema, StoreUnstructuredRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
|
|
46
|
+
import { AnalyzeSchemaCandidatesRequestSchema, CorrectEntityRequestSchema, CreateRelationshipsRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, SplitEntityRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, StoreRequestSchema, StoreUnstructuredRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
|
|
44
47
|
import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
|
|
45
48
|
import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
|
|
46
49
|
import { retrieveEntityByIdentifierWithFallback } from "./shared/action_handlers/entity_identifier_handler.js";
|
|
@@ -48,7 +51,9 @@ import { prepareEntitySnapshotWithEmbedding, upsertEntitySnapshotWithEmbedding,
|
|
|
48
51
|
import { readOpenApiActionsFile, readOpenApiFile } from "./shared/openapi_file.js";
|
|
49
52
|
import { buildSmitheryServerCard } from "./mcp_server_card.js";
|
|
50
53
|
import { listRecentRecordActivity, parseRecordActivityTypesQuery, } from "./services/recent_record_activity.js";
|
|
51
|
-
import { listRecentConversations } from "./services/recent_conversations.js";
|
|
54
|
+
import { getRecentConversationById, listRecentConversations } from "./services/recent_conversations.js";
|
|
55
|
+
import { listConversationTurns, getConversationTurn, } from "./services/conversation_turn.js";
|
|
56
|
+
import { buildComplianceScorecard } from "./services/compliance/scorecard.js";
|
|
52
57
|
import { getAgent, listAgentRecords, listAgents } from "./services/agents_directory.js";
|
|
53
58
|
export const app = express();
|
|
54
59
|
// Trust proxy headers (required for express-rate-limit when X-Forwarded-For is present)
|
|
@@ -102,6 +107,7 @@ app.use(express.json({
|
|
|
102
107
|
},
|
|
103
108
|
}));
|
|
104
109
|
app.use(morgan("dev"));
|
|
110
|
+
app.use(cookieParser());
|
|
105
111
|
app.use(unknownFieldsGuard);
|
|
106
112
|
// Sandbox-mode response header. Stamped on every response so clients can
|
|
107
113
|
// detect public-sandbox deployments (sandbox.neotoma.io) without an extra
|
|
@@ -110,37 +116,125 @@ if (isSandboxMode()) {
|
|
|
110
116
|
app.use(sandboxHeaderMiddleware);
|
|
111
117
|
logger.info("[Sandbox] NEOTOMA_SANDBOX_MODE=1 — bearer bypass to SANDBOX_PUBLIC_USER_ID, destructive routes gated, weekly reset expected");
|
|
112
118
|
}
|
|
113
|
-
// Inspector SPA mount.
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
// Inspector SPA mount. Deliberately registered before all auth / rate-limit
|
|
120
|
+
// middleware so the SPA shell + assets are reachable without a bearer — the
|
|
121
|
+
// API calls the Inspector makes still flow through the normal auth stack below.
|
|
122
|
+
installInspectorMount(app, process.env, logger);
|
|
123
|
+
// ── Sandbox session endpoints ───────────────────────────────────────────
|
|
124
|
+
// Registered before general auth so the session handshake works for
|
|
125
|
+
// unauthenticated visitors. Routes exist only when NEOTOMA_SANDBOX_MODE=1.
|
|
126
|
+
if (isSandboxMode()) {
|
|
127
|
+
const sessionRateLimit = rateLimit({
|
|
128
|
+
windowMs: 60 * 60 * 1000,
|
|
129
|
+
limit: 10,
|
|
130
|
+
standardHeaders: true,
|
|
131
|
+
legacyHeaders: false,
|
|
132
|
+
keyGenerator: (req) => `ip:${ipKeyGenerator(req.ip || "")}`,
|
|
133
|
+
validate: { trustProxy: false },
|
|
134
|
+
});
|
|
135
|
+
app.post("/sandbox/session/new", sessionRateLimit, (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : "generic";
|
|
138
|
+
const session = createSandboxSession(packId);
|
|
139
|
+
res.cookie(SESSION_COOKIE_NAME, session.bearerToken, {
|
|
140
|
+
httpOnly: true,
|
|
141
|
+
sameSite: "lax",
|
|
142
|
+
path: "/",
|
|
143
|
+
expires: new Date(session.expiresAt),
|
|
144
|
+
});
|
|
145
|
+
res.json({
|
|
146
|
+
one_time_code: session.oneTimeCode,
|
|
147
|
+
expires_at: session.expiresAt,
|
|
148
|
+
pack_id: session.packId,
|
|
137
149
|
});
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
logger.error(`[Sandbox] session/new failed: ${err.message}`);
|
|
153
|
+
res.status(500).json({ error_code: "SESSION_CREATE_FAILED", message: err.message });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
app.post("/sandbox/session/redeem", (req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
const code = typeof req.body?.code === "string" ? req.body.code : "";
|
|
159
|
+
if (!code) {
|
|
160
|
+
res.status(400).json({ error_code: "MISSING_CODE", message: "code is required" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const result = redeemOneTimeCode(code);
|
|
164
|
+
if (!result) {
|
|
165
|
+
res.status(404).json({ error_code: "INVALID_CODE", message: "Code expired or already redeemed" });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
res.cookie(SESSION_COOKIE_NAME, result.bearerToken, {
|
|
169
|
+
httpOnly: true,
|
|
170
|
+
sameSite: "lax",
|
|
171
|
+
path: "/",
|
|
172
|
+
expires: new Date(result.expiresAt),
|
|
173
|
+
});
|
|
174
|
+
res.json({
|
|
175
|
+
bearer_token: result.bearerToken,
|
|
176
|
+
user_id: result.userId,
|
|
177
|
+
expires_at: result.expiresAt,
|
|
178
|
+
pack_id: result.packId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
logger.error(`[Sandbox] session/redeem failed: ${err.message}`);
|
|
183
|
+
res.status(500).json({ error_code: "SESSION_REDEEM_FAILED", message: err.message });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
app.get("/sandbox/session", (req, res) => {
|
|
187
|
+
const session = resolveSessionFromRequest(req);
|
|
188
|
+
if (!session) {
|
|
189
|
+
res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
res.json({
|
|
193
|
+
user_id: session.userId,
|
|
194
|
+
pack_id: session.packId,
|
|
195
|
+
created_at: session.createdAt,
|
|
196
|
+
expires_at: session.expiresAt,
|
|
138
197
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
198
|
+
});
|
|
199
|
+
app.post("/sandbox/session/reset", (req, res) => {
|
|
200
|
+
const session = resolveSessionFromRequest(req);
|
|
201
|
+
if (!session) {
|
|
202
|
+
res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const packId = typeof req.body?.pack_id === "string" ? req.body.pack_id : undefined;
|
|
206
|
+
purgeSessionUserData(session.userId);
|
|
207
|
+
const newSession = createSandboxSession(packId ?? session.packId);
|
|
208
|
+
res.cookie(SESSION_COOKIE_NAME, newSession.bearerToken, {
|
|
209
|
+
httpOnly: true,
|
|
210
|
+
sameSite: "lax",
|
|
211
|
+
path: "/",
|
|
212
|
+
expires: new Date(newSession.expiresAt),
|
|
213
|
+
});
|
|
214
|
+
res.json({
|
|
215
|
+
user_id: newSession.userId,
|
|
216
|
+
pack_id: newSession.packId,
|
|
217
|
+
expires_at: newSession.expiresAt,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
app.delete("/sandbox/session", (req, res) => {
|
|
221
|
+
const session = resolveSessionFromRequest(req);
|
|
222
|
+
if (!session) {
|
|
223
|
+
res.status(401).json({ error_code: "NO_SESSION", message: "No active sandbox session" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
revokeSession(session.userId);
|
|
227
|
+
purgeSessionUserData(session.userId);
|
|
228
|
+
res.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
|
|
229
|
+
res.json({ ok: true });
|
|
230
|
+
});
|
|
231
|
+
setInterval(() => {
|
|
232
|
+
const purged = sweepExpiredSessions();
|
|
233
|
+
if (purged > 0) {
|
|
234
|
+
logger.info(`[Sandbox] Swept ${purged} expired session(s)`);
|
|
235
|
+
}
|
|
236
|
+
}, 15 * 60 * 1000);
|
|
237
|
+
logger.info("[Sandbox] Session endpoints registered");
|
|
144
238
|
}
|
|
145
239
|
// Rate limiters for OAuth endpoints
|
|
146
240
|
// validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
|
|
@@ -399,6 +493,7 @@ app.get("/server-info", (_req, res) => {
|
|
|
399
493
|
httpPort,
|
|
400
494
|
apiBase: config.apiBase,
|
|
401
495
|
mcpUrl,
|
|
496
|
+
neotoma_env: readNeotomaConfigEnvironment(),
|
|
402
497
|
});
|
|
403
498
|
});
|
|
404
499
|
// ============================================================================
|
|
@@ -1803,6 +1898,16 @@ app.use(async (req, res, next) => {
|
|
|
1803
1898
|
logger.info(`[Auth] ${req.method} ${req.path} auth_method=local_no_bearer user_id=${devUser.id}`);
|
|
1804
1899
|
return next();
|
|
1805
1900
|
}
|
|
1901
|
+
// Sandbox ephemeral session: resolve from cookie or Bearer token before
|
|
1902
|
+
// falling back to the shared public user. Expired/revoked sessions 401.
|
|
1903
|
+
if (isSandboxMode()) {
|
|
1904
|
+
const sessionInfo = resolveSessionFromRequest(req);
|
|
1905
|
+
if (sessionInfo) {
|
|
1906
|
+
req.authenticatedUserId = sessionInfo.userId;
|
|
1907
|
+
logger.info(`[Auth] ${req.method} ${req.path} auth_method=sandbox_session user_id=${sessionInfo.userId}`);
|
|
1908
|
+
return next();
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1806
1911
|
// Sandbox mode: public deployment at sandbox.neotoma.io where anonymous
|
|
1807
1912
|
// callers are attributed to SANDBOX_PUBLIC_USER_ID without a Bearer. AAuth
|
|
1808
1913
|
// still runs (earlier in the chain via aauthVerify) so agents exercising the
|
|
@@ -3167,6 +3272,21 @@ app.get("/agents/:key/records", async (req, res) => {
|
|
|
3167
3272
|
return handleApiError(req, res, error, "Failed to list agent records", "DB_QUERY_FAILED", "APIError:agents_records");
|
|
3168
3273
|
}
|
|
3169
3274
|
});
|
|
3275
|
+
// GET /recent_conversations/:conversation_id — Inspector: one conversation with nested messages
|
|
3276
|
+
app.get("/recent_conversations/:conversation_id", async (req, res) => {
|
|
3277
|
+
try {
|
|
3278
|
+
const userId = await getAuthenticatedUserId(req, req.query.user_id);
|
|
3279
|
+
const conversationId = String(req.params.conversation_id ?? "").trim();
|
|
3280
|
+
const item = getRecentConversationById(userId, conversationId);
|
|
3281
|
+
if (!item) {
|
|
3282
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Conversation not found");
|
|
3283
|
+
}
|
|
3284
|
+
return res.json(item);
|
|
3285
|
+
}
|
|
3286
|
+
catch (error) {
|
|
3287
|
+
return handleApiError(req, res, error, "Failed to load conversation", "DB_QUERY_FAILED", "APIError:recent_conversation_detail");
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3170
3290
|
// GET /recent_conversations — Inspector: conversations with nested messages (SQLite)
|
|
3171
3291
|
app.get("/recent_conversations", async (req, res) => {
|
|
3172
3292
|
try {
|
|
@@ -3175,7 +3295,9 @@ app.get("/recent_conversations", async (req, res) => {
|
|
|
3175
3295
|
const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
|
|
3176
3296
|
const activity_after = typeof req.query.activity_after === "string" ? req.query.activity_after.trim() || null : null;
|
|
3177
3297
|
const activity_before = typeof req.query.activity_before === "string" ? req.query.activity_before.trim() || null : null;
|
|
3178
|
-
const
|
|
3298
|
+
const agentKeyRaw = typeof req.query.agent_key === "string" ? req.query.agent_key.trim() : "";
|
|
3299
|
+
// UI and bookmarks may send agent_key=all; never treat that as a real deriveAgentKey value.
|
|
3300
|
+
const agent_key = agentKeyRaw.length > 0 && agentKeyRaw.toLowerCase() !== "all" ? agentKeyRaw : null;
|
|
3179
3301
|
const result = listRecentConversations(userId, limit, offset, {
|
|
3180
3302
|
activity_after,
|
|
3181
3303
|
activity_before,
|
|
@@ -3187,6 +3309,69 @@ app.get("/recent_conversations", async (req, res) => {
|
|
|
3187
3309
|
return handleApiError(req, res, error, "Failed to list recent conversations", "DB_QUERY_FAILED", "APIError:recent_conversations");
|
|
3188
3310
|
}
|
|
3189
3311
|
});
|
|
3312
|
+
// GET /turns — Inspector: paginated conversation_turn index
|
|
3313
|
+
app.get("/turns", async (req, res) => {
|
|
3314
|
+
try {
|
|
3315
|
+
const userId = await getAuthenticatedUserId(req, req.query.user_id);
|
|
3316
|
+
const limit = parseInt(String(req.query.limit ?? "25"), 10) || 25;
|
|
3317
|
+
const offset = parseInt(String(req.query.offset ?? "0"), 10) || 0;
|
|
3318
|
+
const harness = typeof req.query.harness === "string" ? req.query.harness.trim() || null : null;
|
|
3319
|
+
const status = typeof req.query.status === "string" ? req.query.status.trim() || null : null;
|
|
3320
|
+
const activity_after = typeof req.query.activity_after === "string" ? req.query.activity_after.trim() || null : null;
|
|
3321
|
+
const activity_before = typeof req.query.activity_before === "string" ? req.query.activity_before.trim() || null : null;
|
|
3322
|
+
const result = listConversationTurns(userId, limit, offset, {
|
|
3323
|
+
harness,
|
|
3324
|
+
status,
|
|
3325
|
+
activity_after,
|
|
3326
|
+
activity_before,
|
|
3327
|
+
});
|
|
3328
|
+
return res.json(result);
|
|
3329
|
+
}
|
|
3330
|
+
catch (error) {
|
|
3331
|
+
return handleApiError(req, res, error, "Failed to list turns", "DB_QUERY_FAILED", "APIError:turns");
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
// GET /turns/:turn_key — Inspector: conversation_turn detail
|
|
3335
|
+
app.get("/turns/:turn_key", async (req, res) => {
|
|
3336
|
+
try {
|
|
3337
|
+
const userId = await getAuthenticatedUserId(req, req.query.user_id);
|
|
3338
|
+
const turnKey = decodeURIComponent(req.params.turn_key);
|
|
3339
|
+
const result = getConversationTurn(userId, turnKey);
|
|
3340
|
+
if (!result) {
|
|
3341
|
+
return res.status(404).json({ error: "Turn not found", code: "NOT_FOUND" });
|
|
3342
|
+
}
|
|
3343
|
+
return res.json(result);
|
|
3344
|
+
}
|
|
3345
|
+
catch (error) {
|
|
3346
|
+
return handleApiError(req, res, error, "Failed to get turn", "DB_QUERY_FAILED", "APIError:turn_detail");
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
// GET /admin/compliance/scorecard — Inspector: aggregated hook compliance (SQLite)
|
|
3350
|
+
app.get("/admin/compliance/scorecard", async (req, res) => {
|
|
3351
|
+
try {
|
|
3352
|
+
const userId = await getAuthenticatedUserId(req, req.query.user_id);
|
|
3353
|
+
const since = typeof req.query.since === "string" ? req.query.since : undefined;
|
|
3354
|
+
const until = typeof req.query.until === "string" ? req.query.until : undefined;
|
|
3355
|
+
const group_by = typeof req.query.group_by === "string" ? req.query.group_by : undefined;
|
|
3356
|
+
const min_turns = parseInt(String(req.query.min_turns ?? ""), 10);
|
|
3357
|
+
const min_backfill_rate = parseFloat(String(req.query.min_backfill_rate ?? ""));
|
|
3358
|
+
const top_missed_steps = parseInt(String(req.query.top_missed_steps ?? ""), 10);
|
|
3359
|
+
const include_synthetic = req.query.include_synthetic === "1" || String(req.query.include_synthetic).toLowerCase() === "true";
|
|
3360
|
+
const result = buildComplianceScorecard(userId, {
|
|
3361
|
+
since,
|
|
3362
|
+
until,
|
|
3363
|
+
group_by,
|
|
3364
|
+
min_turns: Number.isFinite(min_turns) ? min_turns : undefined,
|
|
3365
|
+
min_backfill_rate: Number.isFinite(min_backfill_rate) ? min_backfill_rate : undefined,
|
|
3366
|
+
top_missed_steps: Number.isFinite(top_missed_steps) ? top_missed_steps : undefined,
|
|
3367
|
+
include_synthetic,
|
|
3368
|
+
});
|
|
3369
|
+
return res.json(result);
|
|
3370
|
+
}
|
|
3371
|
+
catch (error) {
|
|
3372
|
+
return handleApiError(req, res, error, "Failed to build compliance scorecard", "DB_QUERY_FAILED", "APIError:compliance_scorecard");
|
|
3373
|
+
}
|
|
3374
|
+
});
|
|
3190
3375
|
// GET /api/sources - Get source list (FU-301)
|
|
3191
3376
|
app.get("/sources", async (req, res) => {
|
|
3192
3377
|
try {
|
|
@@ -4044,30 +4229,41 @@ export async function storeStructuredForApi(params) {
|
|
|
4044
4229
|
if (commit && relationships && relationships.length > 0) {
|
|
4045
4230
|
const { relationshipsService } = await import("./services/relationships.js");
|
|
4046
4231
|
for (const rel of relationships) {
|
|
4047
|
-
const
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4232
|
+
const sourceEntityId = typeof rel.source_entity_id === "string"
|
|
4233
|
+
? rel.source_entity_id
|
|
4234
|
+
: typeof rel.source_index === "number"
|
|
4235
|
+
? resolved[rel.source_index]?.entity_id
|
|
4236
|
+
: undefined;
|
|
4237
|
+
const targetEntityId = typeof rel.target_entity_id === "string"
|
|
4238
|
+
? rel.target_entity_id
|
|
4239
|
+
: typeof rel.target_index === "number"
|
|
4240
|
+
? resolved[rel.target_index]?.entity_id
|
|
4241
|
+
: undefined;
|
|
4242
|
+
if (!sourceEntityId || !targetEntityId) {
|
|
4243
|
+
logger.warn(`[STORE] Skipping relationship: invalid source reference ` +
|
|
4244
|
+
`(source_index=${rel.source_index}, source_entity_id=${rel.source_entity_id}) ` +
|
|
4245
|
+
`or target reference (target_index=${rel.target_index}, ` +
|
|
4246
|
+
`target_entity_id=${rel.target_entity_id}); have ${resolved.length} entities.`);
|
|
4052
4247
|
continue;
|
|
4053
4248
|
}
|
|
4054
4249
|
try {
|
|
4055
4250
|
await relationshipsService.createRelationship({
|
|
4056
|
-
source_entity_id:
|
|
4057
|
-
target_entity_id:
|
|
4251
|
+
source_entity_id: sourceEntityId,
|
|
4252
|
+
target_entity_id: targetEntityId,
|
|
4058
4253
|
relationship_type: rel.relationship_type,
|
|
4059
4254
|
source_id: storageResult.sourceId,
|
|
4255
|
+
metadata: rel.metadata ?? {},
|
|
4060
4256
|
user_id: userId,
|
|
4061
4257
|
});
|
|
4062
4258
|
relationshipsCreated.push({
|
|
4063
4259
|
relationship_type: rel.relationship_type,
|
|
4064
|
-
source_entity_id:
|
|
4065
|
-
target_entity_id:
|
|
4260
|
+
source_entity_id: sourceEntityId,
|
|
4261
|
+
target_entity_id: targetEntityId,
|
|
4066
4262
|
});
|
|
4067
4263
|
}
|
|
4068
4264
|
catch (relErr) {
|
|
4069
4265
|
logger.warn(`Failed to create relationship ${rel.relationship_type} ` +
|
|
4070
|
-
`${
|
|
4266
|
+
`${sourceEntityId} -> ${targetEntityId}: ${relErr instanceof Error ? relErr.message : String(relErr)}`);
|
|
4071
4267
|
}
|
|
4072
4268
|
}
|
|
4073
4269
|
}
|
|
@@ -4635,6 +4831,63 @@ app.post("/create_relationship", async (req, res) => {
|
|
|
4635
4831
|
return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationship");
|
|
4636
4832
|
}
|
|
4637
4833
|
});
|
|
4834
|
+
// Create relationships in batch
|
|
4835
|
+
app.post("/create_relationships", async (req, res) => {
|
|
4836
|
+
const parsed = CreateRelationshipsRequestSchema.safeParse(req.body);
|
|
4837
|
+
if (!parsed.success) {
|
|
4838
|
+
logWarn("ValidationError:create_relationships", req, {
|
|
4839
|
+
issues: parsed.error.issues,
|
|
4840
|
+
});
|
|
4841
|
+
return sendValidationError(res, parsed.error.issues);
|
|
4842
|
+
}
|
|
4843
|
+
const { relationships, source_id, user_id } = parsed.data;
|
|
4844
|
+
const { relationshipsService } = await import("./services/relationships.js");
|
|
4845
|
+
try {
|
|
4846
|
+
const userId = await getAuthenticatedUserId(req, user_id);
|
|
4847
|
+
const created = [];
|
|
4848
|
+
const errors = [];
|
|
4849
|
+
for (const [index, relationship] of relationships.entries()) {
|
|
4850
|
+
try {
|
|
4851
|
+
const snapshot = await relationshipsService.createRelationship({
|
|
4852
|
+
relationship_type: relationship.relationship_type,
|
|
4853
|
+
source_entity_id: relationship.source_entity_id,
|
|
4854
|
+
target_entity_id: relationship.target_entity_id,
|
|
4855
|
+
source_id: relationship.source_id || source_id || null,
|
|
4856
|
+
metadata: relationship.metadata || {},
|
|
4857
|
+
user_id: userId,
|
|
4858
|
+
});
|
|
4859
|
+
created.push({
|
|
4860
|
+
index,
|
|
4861
|
+
...snapshot,
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
catch (error) {
|
|
4865
|
+
errors.push({
|
|
4866
|
+
index,
|
|
4867
|
+
relationship,
|
|
4868
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4869
|
+
});
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
logDebug("Success:create_relationships", req, {
|
|
4873
|
+
requested: relationships.length,
|
|
4874
|
+
created: created.length,
|
|
4875
|
+
errors: errors.length,
|
|
4876
|
+
});
|
|
4877
|
+
return res.json({
|
|
4878
|
+
success: errors.length === 0,
|
|
4879
|
+
requested: relationships.length,
|
|
4880
|
+
created_count: created.length,
|
|
4881
|
+
error_count: errors.length,
|
|
4882
|
+
relationships: created,
|
|
4883
|
+
errors,
|
|
4884
|
+
});
|
|
4885
|
+
}
|
|
4886
|
+
catch (error) {
|
|
4887
|
+
logError("RelationshipCreationError:create_relationships", req, error);
|
|
4888
|
+
return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationships");
|
|
4889
|
+
}
|
|
4890
|
+
});
|
|
4638
4891
|
// List relationships
|
|
4639
4892
|
app.post("/list_relationships", async (req, res) => {
|
|
4640
4893
|
const parsed = ListRelationshipsRequestSchema.safeParse(req.body);
|
|
@@ -5665,6 +5918,16 @@ export async function startHTTPServer() {
|
|
|
5665
5918
|
}
|
|
5666
5919
|
// Initialize encryption service
|
|
5667
5920
|
await initServerKeys();
|
|
5921
|
+
// Seed `neotoma_feedback` schema unconditionally so local-only installs
|
|
5922
|
+
// (no AGENT_SITE_BASE_URL) can mirror feedback into the entity graph.
|
|
5923
|
+
try {
|
|
5924
|
+
const { seedNeotomaFeedbackSchema } = await import("./services/feedback/seed_schema.js");
|
|
5925
|
+
await seedNeotomaFeedbackSchema();
|
|
5926
|
+
logger.info("[Feedback] neotoma_feedback schema seeded");
|
|
5927
|
+
}
|
|
5928
|
+
catch (err) {
|
|
5929
|
+
logger.warn(`[Feedback] failed to seed neotoma_feedback schema: ${err.message}`);
|
|
5930
|
+
}
|
|
5668
5931
|
// Sandbox mode: ensure the `sandbox_abuse_report` entity type is registered
|
|
5669
5932
|
// before any report comes in so forwarded records can attach cleanly to the
|
|
5670
5933
|
// entity graph. Non-sandbox deployments still benefit from having the schema
|