neotoma 0.25.0 → 0.26.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.
- package/README.md +77 -47
- package/dist/actions.d.ts +116 -27
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +560 -191
- package/dist/actions.js.map +1 -1
- package/dist/cli/agents_grants_import.d.ts.map +1 -1
- package/dist/cli/agents_grants_import.js +22 -2
- package/dist/cli/agents_grants_import.js.map +1 -1
- package/dist/cli/index.d.ts +29 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +993 -220
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/parse_schema_fields.d.ts +2 -1
- package/dist/cli/parse_schema_fields.d.ts.map +1 -1
- package/dist/cli/parse_schema_fields.js +4 -1
- package/dist/cli/parse_schema_fields.js.map +1 -1
- package/dist/docs/api/rest_api.md +169 -365
- package/dist/docs/architecture/architecture.md +250 -830
- package/dist/docs/architecture/idempotence_pattern.md +101 -0
- package/dist/docs/conventions/documentation_standards.md +184 -75
- package/dist/docs/developer/agentic_eval_fixture_format.md +2 -0
- package/dist/docs/developer/atomic_corrections.md +17 -0
- package/dist/docs/developer/cli_agent_instructions.md +30 -4
- package/dist/docs/developer/cli_reference.md +217 -170
- package/dist/docs/developer/fleet_onboarding.md +7 -6
- package/dist/docs/developer/mcp/instructions.md +29 -18
- package/dist/docs/developer/mcp_overview.md +4 -4
- package/dist/docs/integrations/matrix.md +4 -4
- package/dist/docs/operations/configuration.md +2 -1
- package/dist/docs/operations/running_the_server.md +1 -1
- package/dist/docs/reference/error_codes.md +1 -0
- package/dist/docs/specs/MCP_SPEC.md +5 -4
- package/dist/docs/subsystems/agent_attribution_integration.md +159 -0
- package/dist/docs/subsystems/agent_capabilities.md +57 -1
- package/dist/docs/subsystems/agentic_eval.md +13 -2
- package/dist/docs/subsystems/deletion.md +24 -9
- package/dist/docs/subsystems/errors.md +80 -45
- package/dist/docs/subsystems/ingestion/ingestion.md +1 -1
- package/dist/docs/subsystems/reducer.md +208 -52
- package/dist/docs/subsystems/schema_registry.md +4 -1
- package/dist/docs/subsystems/sources.md +25 -0
- package/dist/docs/subsystems/substrate_events.md +1 -1
- package/dist/docs/testing/automated_test_catalog.md +89 -20
- package/dist/docs/testing/testing_standard.md +33 -0
- package/dist/docs/vocabulary/canonical_terms.md +212 -180
- package/dist/embeddings.d.ts +4 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +7 -10
- package/dist/embeddings.js.map +1 -1
- package/dist/inspector/assets/{access_policies-B1HM9aJQ.js → access_policies-CJOlV60d.js} +1 -1
- package/dist/inspector/assets/{agent_badge-XmciduFJ.js → agent_badge-BnBqn8kt.js} +1 -1
- package/dist/inspector/assets/{agent_detail-DOEe2mhV.js → agent_detail-wP0ccjhX.js} +1 -1
- package/dist/inspector/assets/{agent_filter-Dio0XybG.js → agent_filter-Bp2xA131.js} +1 -1
- package/dist/inspector/assets/{agent_grant_detail-BfUFnclY.js → agent_grant_detail-HIGr5dq_.js} +1 -1
- package/dist/inspector/assets/agent_grant_form-BoNBvzEf.js +1 -0
- package/dist/inspector/assets/{agent_grants-DZaHIPk8.js → agent_grants-CTKkadE5.js} +1 -1
- package/dist/inspector/assets/{agents-B-f-NRE2.js → agents-CPwJLNJN.js} +1 -1
- package/dist/inspector/assets/{api_not_configured_state-Ou-G3uKz.js → api_not_configured_state-ivqIcs2c.js} +1 -1
- package/dist/inspector/assets/{attribution_card-CIkjiijF.js → attribution_card-5ZAqRz6K.js} +1 -1
- package/dist/inspector/assets/{attribution_summary-0ckzYad9.js → attribution_summary-D_MHFv7e.js} +1 -1
- package/dist/inspector/assets/{bundles-Burq1ci7.js → bundles-QfMciBj9.js} +1 -1
- package/dist/inspector/assets/{card-CTv_O7eJ.js → card-1rkzdi9W.js} +1 -1
- package/dist/inspector/assets/{checkbox--jk4IAjc.js → checkbox-CO5dXSZf.js} +1 -1
- package/dist/inspector/assets/{compliance-B3vrfBCU.js → compliance-OTGEtY7Y.js} +1 -1
- package/dist/inspector/assets/{confirm-dialog-DgDSsLkR.js → confirm-dialog-D0cZ5vck.js} +1 -1
- package/dist/inspector/assets/{conversation_common-BAdG50H9.js → conversation_common-cSILaHXq.js} +1 -1
- package/dist/inspector/assets/{conversation_detail-DIUc4HKE.js → conversation_detail-DT-lGapN.js} +1 -1
- package/dist/inspector/assets/{copy_id_button-D1VkPBFT.js → copy_id_button-Dz0JDn4l.js} +1 -1
- package/dist/inspector/assets/{copyable_code_block-ChPCJ02V.js → copyable_code_block-B2f99g8Q.js} +1 -1
- package/dist/inspector/assets/{corrections-ChJ2p7_i.js → corrections-KvxGvvLV.js} +1 -1
- package/dist/inspector/assets/{dashboard-DIYNcYg6.js → dashboard-BudvW21X.js} +1 -1
- package/dist/inspector/assets/{data-table-C4VXfuGY.js → data-table-d9EMxowf.js} +1 -1
- package/dist/inspector/assets/{design-DXOwL75T.js → design-DyiQbEeP.js} +1 -1
- package/dist/inspector/assets/{dialog-CXVzcRia.js → dialog-BmW1Iw8o.js} +1 -1
- package/dist/inspector/assets/{docs-B26tYp-6.js → docs-CT3IYX0E.js} +1 -1
- package/dist/inspector/assets/{docs_page_layout-CrM6Un_f.js → docs_page_layout-CosNjUZa.js} +1 -1
- package/dist/inspector/assets/{embed_graph-C2Jc2iwk.js → embed_graph-Dk1-zVnz.js} +1 -1
- package/dist/inspector/assets/{empty_state-d5guw2_7.js → empty_state-B8Ot2oGB.js} +1 -1
- package/dist/inspector/assets/{entities-BrnaqwF9.js → entities-Dt5Ts5UN.js} +1 -1
- package/dist/inspector/assets/{entity_correct-DnD1oYUl.js → entity_correct-D2PHEpGY.js} +1 -1
- package/dist/inspector/assets/{entity_history-PFlm7n3S.js → entity_history-DHgNythA.js} +1 -1
- package/dist/inspector/assets/{entity_history_sections-C5gKEqH8.js → entity_history_sections-Cnxblw-Z.js} +1 -1
- package/dist/inspector/assets/{entity_link-BlAO6pqg.js → entity_link-DWf1VD1n.js} +1 -1
- package/dist/inspector/assets/{entity_relationships_by_type-DzLwN95P.js → entity_relationships_by_type-B0-TLC5o.js} +1 -1
- package/dist/inspector/assets/{entity_segment_page-CGm6UKGn.js → entity_segment_page-CzZ0VhKm.js} +1 -1
- package/dist/inspector/assets/{entity_table_column_toggle-CotVPBog.js → entity_table_column_toggle-C73aErGE.js} +1 -1
- package/dist/inspector/assets/{entity_timeline-DYqizEmo.js → entity_timeline-SAq5qvB5.js} +1 -1
- package/dist/inspector/assets/{entity_types-CntHwuK5.js → entity_types-DEcDvyO0.js} +1 -1
- package/dist/inspector/assets/{faq-_NXFNTgM.js → faq-DazWIFdv.js} +1 -1
- package/dist/inspector/assets/{graph_auto_fit-BeH9MOss.js → graph_auto_fit-CSzD7yNh.js} +1 -1
- package/dist/inspector/assets/{graph_explorer-CxWZohQv.js → graph_explorer-BEXXiMQ1.js} +1 -1
- package/dist/inspector/assets/{home-DudMw6_Z.js → home-3AZtmjKp.js} +1 -1
- package/dist/inspector/assets/{index-DDgXel2r.js → index-Cii0RALL.js} +1 -1
- package/dist/inspector/assets/{index-VT5Dxs36.js → index-Dt_q_A1j.js} +1 -1
- package/dist/inspector/assets/{index-BMURChxZ.js → index-fc4sMprn.js} +2 -2
- package/dist/inspector/assets/{index-WhUlQzmL.css → index-yoY5L3eB.css} +1 -1
- package/dist/inspector/assets/interpretations-CjR2csNL.js +1 -0
- package/dist/inspector/assets/{interpretations-xJnQ-uYX.js → interpretations-DtzoiGuK.js} +1 -1
- package/dist/inspector/assets/{issue_author_attribution-DOn6PLKU.js → issue_author_attribution-DtmFaluD.js} +1 -1
- package/dist/inspector/assets/{issue_detail-DztHpggK.js → issue_detail-ChHiz9I_.js} +1 -1
- package/dist/inspector/assets/{issues-BkgvGReD.js → issues-BoxR2nB2.js} +1 -1
- package/dist/inspector/assets/{json_viewer-xKCfoCA1.js → json_viewer-KAFp_j-8.js} +1 -1
- package/dist/inspector/assets/{label-DDkYZSlU.js → label-Dn0ARjYl.js} +1 -1
- package/dist/inspector/assets/{live_relative_time-D-U5_U5-.js → live_relative_time-gOIFOOLR.js} +1 -1
- package/dist/inspector/assets/{mobile_filter_popover-DQNcOXkp.js → mobile_filter_popover-2ezU-mlZ.js} +1 -1
- package/dist/inspector/assets/{not_found-D6QKUWRM.js → not_found-DwX7ikQC.js} +1 -1
- package/dist/inspector/assets/{oauth_callback-BVxx0KXw.js → oauth_callback-BDGLOyTN.js} +1 -1
- package/dist/inspector/assets/{oauth_signin-DI4E46_1.js → oauth_signin-CGcCtq-V.js} +1 -1
- package/dist/inspector/assets/{observations-C1wQYMZV.js → observations-DvcUGpHm.js} +1 -1
- package/dist/inspector/assets/{page_shell-EP5xmaGY.js → page_shell-DiwchdUC.js} +1 -1
- package/dist/inspector/assets/{pagination-B_G7Mbyi.js → pagination-BGIUHQkm.js} +1 -1
- package/dist/inspector/assets/{peer_detail-BC-DSDif.js → peer_detail-DeD1sTeJ.js} +1 -1
- package/dist/inspector/assets/{peers-fVyfV57y.js → peers-CXe47m55.js} +1 -1
- package/dist/inspector/assets/{pin_primitive_button-_TIZX9AI.js → pin_primitive_button-C37Ha0Bt.js} +1 -1
- package/dist/inspector/assets/{recent_activity-CFv4LsTR.js → recent_activity-CzQjt4ym.js} +1 -1
- package/dist/inspector/assets/{recent_conversations-DU1XrX7M.js → recent_conversations-BFYZTxUJ.js} +1 -1
- package/dist/inspector/assets/{recent_conversations-Cac7CkDg.js → recent_conversations-BnUl4h4t.js} +1 -1
- package/dist/inspector/assets/{recent_records_feed-BFJzHH4J.js → recent_records_feed-y1n0RuMa.js} +1 -1
- package/dist/inspector/assets/record_activity-kXm-jtQ6.js +1 -0
- package/dist/inspector/assets/{relationship_detail-Cc9dVYco.js → relationship_detail-ic83ZCwG.js} +1 -1
- package/dist/inspector/assets/{relationship_panel_groups-Bknw8oKA.js → relationship_panel_groups-CI1fZIU_.js} +1 -1
- package/dist/inspector/assets/{relationships-DoCpO8F8.js → relationships-C1JrEl85.js} +1 -1
- package/dist/inspector/assets/{relationships-CJ7GneMs.js → relationships-C7iSdgXG.js} +1 -1
- package/dist/inspector/assets/{sandbox-BrYkvSj5.js → sandbox-Dst-WJhB.js} +1 -1
- package/dist/inspector/assets/{schema_detail-CdX2n82a.js → schema_detail-C7_JSWAw.js} +1 -1
- package/dist/inspector/assets/{schemas-CR6vDZ-d.js → schemas-BgrIpRVJ.js} +1 -1
- package/dist/inspector/assets/{search-uJmTOqpx.js → search-BUmT32dy.js} +1 -1
- package/dist/inspector/assets/{segmented_control-Dgdqohml.js → segmented_control-t1pBImFE.js} +1 -1
- package/dist/inspector/assets/{select-BCmhrYiM.js → select-DU7Jwzuy.js} +1 -1
- package/dist/inspector/assets/{settings-DRbR3maP.js → settings-BAPHY6RY.js} +1 -1
- package/dist/inspector/assets/{source_content-BuBSyT72.js → source_content-CIhKrGsl.js} +1 -1
- package/dist/inspector/assets/{source_content_open_button-DAYwX9Dh.js → source_content_open_button-Erw2ErgG.js} +1 -1
- package/dist/inspector/assets/{source_detail-DhEV5aJv.js → source_detail-C0XOo0Uu.js} +1 -1
- package/dist/inspector/assets/{source_link-CE1kUYaL.js → source_link-CkcRMEO3.js} +1 -1
- package/dist/inspector/assets/{sources-LZmXkGnn.js → sources-C0JBEINb.js} +1 -1
- package/dist/inspector/assets/{subscriptions-b_DO87jI.js → subscriptions-DxU4kHGt.js} +1 -1
- package/dist/inspector/assets/{switch-B6x__4FK.js → switch-BhKadp29.js} +1 -1
- package/dist/inspector/assets/{tabs-Bi6BUJqc.js → tabs-d81XObYb.js} +1 -1
- package/dist/inspector/assets/{textarea-3QlsHqkf.js → textarea-DTiKLH24.js} +1 -1
- package/dist/inspector/assets/{timeline-BoE9ZRrA.js → timeline-98QXcGln.js} +1 -1
- package/dist/inspector/assets/{timeline-a2L8UjV_.js → timeline-BEyKQpER.js} +1 -1
- package/dist/inspector/assets/{timeline_event_detail-D8eTp4i6.js → timeline_event_detail-DzGzqtrx.js} +1 -1
- package/dist/inspector/assets/{turn_detail-B2uAxNHK.js → turn_detail-CDP_FEPT.js} +1 -1
- package/dist/inspector/assets/{turns-BpYk8UFz.js → turns-DLmr1Iue.js} +1 -1
- package/dist/inspector/assets/{use_agents-Bt3UqR6w.js → use_agents-D3rvlr5v.js} +1 -1
- package/dist/inspector/assets/{use_entities-wuMfztjo.js → use_entities-CVikeUOv.js} +1 -1
- package/dist/inspector/assets/use_interpretations-CCoQ6ihs.js +1 -0
- package/dist/inspector/assets/{use_mutations-BhRvMBFs.js → use_mutations-BhwSLXcJ.js} +1 -1
- package/dist/inspector/assets/{use_peers-CD_9h7zQ.js → use_peers-QTm1Z_BV.js} +1 -1
- package/dist/inspector/assets/{use_relationships-CjsGAwR8.js → use_relationships-DtymxAI9.js} +1 -1
- package/dist/inspector/assets/{use_stats-IyFfTD4L.js → use_stats-BW1Z-dI2.js} +1 -1
- package/dist/inspector/assets/{use_timeline-BueAlCUW.js → use_timeline-DND87Sof.js} +1 -1
- package/dist/inspector/assets/{use_turns-CZtZ839A.js → use_turns-Dfb6Q8Ru.js} +1 -1
- package/dist/inspector/assets/{world_time_event_timeline-D8bL35LA.js → world_time_event_timeline-DxzejNrJ.js} +1 -1
- package/dist/inspector/index.html +2 -2
- package/dist/maintenance/entity_lifecycle_inventory.d.ts +31 -0
- package/dist/maintenance/entity_lifecycle_inventory.d.ts.map +1 -0
- package/dist/maintenance/entity_lifecycle_inventory.js +107 -0
- package/dist/maintenance/entity_lifecycle_inventory.js.map +1 -0
- package/dist/maintenance/entity_lifecycle_manifest.d.ts +84 -0
- package/dist/maintenance/entity_lifecycle_manifest.d.ts.map +1 -0
- package/dist/maintenance/entity_lifecycle_manifest.js +225 -0
- package/dist/maintenance/entity_lifecycle_manifest.js.map +1 -0
- package/dist/maintenance/entity_lifecycle_migration.d.ts +11 -0
- package/dist/maintenance/entity_lifecycle_migration.d.ts.map +1 -0
- package/dist/maintenance/entity_lifecycle_migration.js +576 -0
- package/dist/maintenance/entity_lifecycle_migration.js.map +1 -0
- package/dist/maintenance/entity_lifecycle_records.d.ts +12 -0
- package/dist/maintenance/entity_lifecycle_records.d.ts.map +1 -0
- package/dist/maintenance/entity_lifecycle_records.js +95 -0
- package/dist/maintenance/entity_lifecycle_records.js.map +1 -0
- package/dist/mcp_ws_bridge.js +26 -3
- package/dist/mcp_ws_bridge.js.map +1 -1
- package/dist/reducers/observation_reducer.d.ts +67 -3
- package/dist/reducers/observation_reducer.d.ts.map +1 -1
- package/dist/reducers/observation_reducer.js +202 -70
- package/dist/reducers/observation_reducer.js.map +1 -1
- package/dist/repositories/db/connection.d.ts +2 -0
- package/dist/repositories/db/connection.d.ts.map +1 -1
- package/dist/repositories/db/connection.js +27 -0
- package/dist/repositories/db/connection.js.map +1 -1
- package/dist/repositories/db/store_condition_schema.d.ts +3 -0
- package/dist/repositories/db/store_condition_schema.d.ts.map +1 -0
- package/dist/repositories/db/store_condition_schema.js +14 -0
- package/dist/repositories/db/store_condition_schema.js.map +1 -0
- package/dist/repositories/sqlite/local_db_adapter.d.ts +2 -0
- package/dist/repositories/sqlite/local_db_adapter.d.ts.map +1 -1
- package/dist/repositories/sqlite/local_db_adapter.js +21 -5
- package/dist/repositories/sqlite/local_db_adapter.js.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.js +2 -0
- package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
- package/dist/repositories/sqlite/sqlite_driver.d.ts +10 -2
- package/dist/repositories/sqlite/sqlite_driver.d.ts.map +1 -1
- package/dist/repositories/sqlite/sqlite_driver.js +32 -5
- package/dist/repositories/sqlite/sqlite_driver.js.map +1 -1
- package/dist/repositories/worker/worker_file_database.d.ts +19 -0
- package/dist/repositories/worker/worker_file_database.d.ts.map +1 -1
- package/dist/repositories/worker/worker_file_database.js +23 -0
- package/dist/repositories/worker/worker_file_database.js.map +1 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +550 -112
- package/dist/server.js.map +1 -1
- package/dist/services/agent_capabilities.d.ts +19 -4
- package/dist/services/agent_capabilities.d.ts.map +1 -1
- package/dist/services/agent_capabilities.js +60 -0
- package/dist/services/agent_capabilities.js.map +1 -1
- package/dist/services/agent_grants.d.ts.map +1 -1
- package/dist/services/agent_grants.js +19 -0
- package/dist/services/agent_grants.js.map +1 -1
- package/dist/services/array_item_patch.d.ts +158 -0
- package/dist/services/array_item_patch.d.ts.map +1 -0
- package/dist/services/array_item_patch.js +375 -0
- package/dist/services/array_item_patch.js.map +1 -0
- package/dist/services/attachment_resolution.d.ts.map +1 -1
- package/dist/services/attachment_resolution.js +2 -0
- package/dist/services/attachment_resolution.js.map +1 -1
- package/dist/services/attribution_redaction.d.ts.map +1 -1
- package/dist/services/attribution_redaction.js +16 -2
- package/dist/services/attribution_redaction.js.map +1 -1
- package/dist/services/batch_correction.d.ts +2 -0
- package/dist/services/batch_correction.d.ts.map +1 -1
- package/dist/services/batch_correction.js +19 -15
- package/dist/services/batch_correction.js.map +1 -1
- package/dist/services/canonical_markdown.d.ts +7 -23
- package/dist/services/canonical_markdown.d.ts.map +1 -1
- package/dist/services/canonical_markdown.js +10 -0
- package/dist/services/canonical_markdown.js.map +1 -1
- package/dist/services/correction.d.ts +118 -0
- package/dist/services/correction.d.ts.map +1 -1
- package/dist/services/correction.js +298 -36
- package/dist/services/correction.js.map +1 -1
- package/dist/services/correction_transaction.d.ts +27 -0
- package/dist/services/correction_transaction.d.ts.map +1 -0
- package/dist/services/correction_transaction.js +189 -0
- package/dist/services/correction_transaction.js.map +1 -0
- package/dist/services/dashboard_stats.d.ts.map +1 -1
- package/dist/services/dashboard_stats.js +8 -3
- package/dist/services/dashboard_stats.js.map +1 -1
- package/dist/services/deletion.d.ts.map +1 -1
- package/dist/services/deletion.js +235 -101
- package/dist/services/deletion.js.map +1 -1
- package/dist/services/entity_lifecycle_authority.d.ts +62 -0
- package/dist/services/entity_lifecycle_authority.d.ts.map +1 -0
- package/dist/services/entity_lifecycle_authority.js +210 -0
- package/dist/services/entity_lifecycle_authority.js.map +1 -0
- package/dist/services/entity_lifecycle_ingress.d.ts +15 -0
- package/dist/services/entity_lifecycle_ingress.d.ts.map +1 -0
- package/dist/services/entity_lifecycle_ingress.js +72 -0
- package/dist/services/entity_lifecycle_ingress.js.map +1 -0
- package/dist/services/entity_lifecycle_storage.d.ts +40 -0
- package/dist/services/entity_lifecycle_storage.d.ts.map +1 -0
- package/dist/services/entity_lifecycle_storage.js +415 -0
- package/dist/services/entity_lifecycle_storage.js.map +1 -0
- package/dist/services/entity_queries.d.ts +14 -1
- package/dist/services/entity_queries.d.ts.map +1 -1
- package/dist/services/entity_queries.js +117 -70
- package/dist/services/entity_queries.js.map +1 -1
- package/dist/services/entity_resolution.d.ts +24 -0
- package/dist/services/entity_resolution.d.ts.map +1 -1
- package/dist/services/entity_resolution.js +60 -4
- package/dist/services/entity_resolution.js.map +1 -1
- package/dist/services/entity_semantic_search.d.ts +3 -0
- package/dist/services/entity_semantic_search.d.ts.map +1 -1
- package/dist/services/entity_semantic_search.js +12 -4
- package/dist/services/entity_semantic_search.js.map +1 -1
- package/dist/services/entity_snapshot_at_time.d.ts +3 -1
- package/dist/services/entity_snapshot_at_time.d.ts.map +1 -1
- package/dist/services/entity_snapshot_at_time.js +71 -16
- package/dist/services/entity_snapshot_at_time.js.map +1 -1
- package/dist/services/entity_snapshot_embedding.d.ts +8 -1
- package/dist/services/entity_snapshot_embedding.d.ts.map +1 -1
- package/dist/services/entity_snapshot_embedding.js +60 -3
- package/dist/services/entity_snapshot_embedding.js.map +1 -1
- package/dist/services/entity_split.d.ts.map +1 -1
- package/dist/services/entity_split.js +4 -2
- package/dist/services/entity_split.js.map +1 -1
- package/dist/services/entity_version.d.ts +12 -0
- package/dist/services/entity_version.d.ts.map +1 -0
- package/dist/services/entity_version.js +17 -0
- package/dist/services/entity_version.js.map +1 -0
- package/dist/services/instance_policy.d.ts +3 -1
- package/dist/services/instance_policy.d.ts.map +1 -1
- package/dist/services/instance_policy.js +16 -2
- package/dist/services/instance_policy.js.map +1 -1
- package/dist/services/interpretation.d.ts +5 -0
- package/dist/services/interpretation.d.ts.map +1 -1
- package/dist/services/interpretation.js +3 -1
- package/dist/services/interpretation.js.map +1 -1
- package/dist/services/local_entity_embedding.d.ts +3 -0
- package/dist/services/local_entity_embedding.d.ts.map +1 -1
- package/dist/services/local_entity_embedding.js +14 -2
- package/dist/services/local_entity_embedding.js.map +1 -1
- package/dist/services/memory_export.d.ts.map +1 -1
- package/dist/services/memory_export.js +2 -1
- package/dist/services/memory_export.js.map +1 -1
- package/dist/services/observation_insert.d.ts +137 -0
- package/dist/services/observation_insert.d.ts.map +1 -0
- package/dist/services/observation_insert.js +124 -0
- package/dist/services/observation_insert.js.map +1 -0
- package/dist/services/observation_source_rank.d.ts +29 -0
- package/dist/services/observation_source_rank.d.ts.map +1 -0
- package/dist/services/observation_source_rank.js +37 -0
- package/dist/services/observation_source_rank.js.map +1 -0
- package/dist/services/observation_source_warning.d.ts +66 -0
- package/dist/services/observation_source_warning.d.ts.map +1 -0
- package/dist/services/observation_source_warning.js +71 -0
- package/dist/services/observation_source_warning.js.map +1 -0
- package/dist/services/observation_storage.d.ts +16 -11
- package/dist/services/observation_storage.d.ts.map +1 -1
- package/dist/services/observation_storage.js +25 -38
- package/dist/services/observation_storage.js.map +1 -1
- package/dist/services/provenance_data_origin.d.ts +81 -0
- package/dist/services/provenance_data_origin.d.ts.map +1 -0
- package/dist/services/provenance_data_origin.js +171 -0
- package/dist/services/provenance_data_origin.js.map +1 -0
- package/dist/services/raw_fragments.d.ts +2 -0
- package/dist/services/raw_fragments.d.ts.map +1 -1
- package/dist/services/raw_fragments.js +20 -4
- package/dist/services/raw_fragments.js.map +1 -1
- package/dist/services/raw_storage.d.ts.map +1 -1
- package/dist/services/raw_storage.js +3 -0
- package/dist/services/raw_storage.js.map +1 -1
- package/dist/services/relationship_write_capability.d.ts +85 -0
- package/dist/services/relationship_write_capability.d.ts.map +1 -0
- package/dist/services/relationship_write_capability.js +139 -0
- package/dist/services/relationship_write_capability.js.map +1 -0
- package/dist/services/relationships.d.ts.map +1 -1
- package/dist/services/relationships.js +20 -0
- package/dist/services/relationships.js.map +1 -1
- package/dist/services/root_landing/site_nav.d.ts.map +1 -1
- package/dist/services/root_landing/site_nav.js +1 -16
- package/dist/services/root_landing/site_nav.js.map +1 -1
- package/dist/services/schema_reference_linking.d.ts.map +1 -1
- package/dist/services/schema_reference_linking.js +39 -0
- package/dist/services/schema_reference_linking.js.map +1 -1
- package/dist/services/schema_registry.d.ts +22 -3
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +60 -3
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/snapshot_computation.d.ts +6 -3
- package/dist/services/snapshot_computation.d.ts.map +1 -1
- package/dist/services/snapshot_computation.js +40 -11
- package/dist/services/snapshot_computation.js.map +1 -1
- package/dist/services/snapshot_export.d.ts.map +1 -1
- package/dist/services/snapshot_export.js +2 -1
- package/dist/services/snapshot_export.js.map +1 -1
- package/dist/services/stable_serialize.d.ts +19 -0
- package/dist/services/stable_serialize.d.ts.map +1 -0
- package/dist/services/stable_serialize.js +40 -0
- package/dist/services/stable_serialize.js.map +1 -0
- package/dist/services/store_admission.d.ts +34 -0
- package/dist/services/store_admission.d.ts.map +1 -0
- package/dist/services/store_admission.js +155 -0
- package/dist/services/store_admission.js.map +1 -0
- package/dist/services/store_combined_admission.d.ts +7 -0
- package/dist/services/store_combined_admission.d.ts.map +1 -0
- package/dist/services/store_combined_admission.js +59 -0
- package/dist/services/store_combined_admission.js.map +1 -0
- package/dist/services/store_condition_identity.d.ts +11 -0
- package/dist/services/store_condition_identity.d.ts.map +1 -0
- package/dist/services/store_condition_identity.js +44 -0
- package/dist/services/store_condition_identity.js.map +1 -0
- package/dist/services/store_condition_keys.d.ts +72 -0
- package/dist/services/store_condition_keys.d.ts.map +1 -0
- package/dist/services/store_condition_keys.js +249 -0
- package/dist/services/store_condition_keys.js.map +1 -0
- package/dist/services/store_condition_request.d.ts +2 -0
- package/dist/services/store_condition_request.d.ts.map +1 -0
- package/dist/services/store_condition_request.js +39 -0
- package/dist/services/store_condition_request.js.map +1 -0
- package/dist/services/store_conditional.d.ts +50 -0
- package/dist/services/store_conditional.d.ts.map +1 -0
- package/dist/services/store_conditional.js +392 -0
- package/dist/services/store_conditional.js.map +1 -0
- package/dist/services/store_mutation_authority.d.ts +4 -0
- package/dist/services/store_mutation_authority.d.ts.map +1 -0
- package/dist/services/store_mutation_authority.js +42 -0
- package/dist/services/store_mutation_authority.js.map +1 -0
- package/dist/services/store_ownership_admission.d.ts +3 -0
- package/dist/services/store_ownership_admission.d.ts.map +1 -0
- package/dist/services/store_ownership_admission.js +60 -0
- package/dist/services/store_ownership_admission.js.map +1 -0
- package/dist/services/store_write_state.d.ts +112 -0
- package/dist/services/store_write_state.d.ts.map +1 -0
- package/dist/services/store_write_state.js +254 -0
- package/dist/services/store_write_state.js.map +1 -0
- package/dist/services/sync/peer_sync_batch.d.ts.map +1 -1
- package/dist/services/sync/peer_sync_batch.js +2 -1
- package/dist/services/sync/peer_sync_batch.js.map +1 -1
- package/dist/services/timeline_events.d.ts +2 -0
- package/dist/services/timeline_events.d.ts.map +1 -1
- package/dist/services/timeline_events.js +2 -0
- package/dist/services/timeline_events.js.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.d.ts +3 -0
- package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.js +280 -15
- package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +683 -227
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +243 -8
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/bind_host.d.ts +28 -0
- package/dist/shared/bind_host.d.ts.map +1 -0
- package/dist/shared/bind_host.js +34 -0
- package/dist/shared/bind_host.js.map +1 -0
- package/dist/shared/contract_mappings.d.ts.map +1 -1
- package/dist/shared/contract_mappings.js +27 -0
- package/dist/shared/contract_mappings.js.map +1 -1
- package/dist/shared/entity_read_contract.d.ts +55 -0
- package/dist/shared/entity_read_contract.d.ts.map +1 -0
- package/dist/shared/entity_read_contract.js +201 -0
- package/dist/shared/entity_read_contract.js.map +1 -0
- package/dist/shared/openapi_types.d.ts +986 -31
- 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 +25 -4
- package/dist/tool_definitions.js.map +1 -1
- package/dist/utils/lucide_icons.d.ts +1 -1
- package/dist/utils/safe_request_log_format.d.ts +2 -0
- package/dist/utils/safe_request_log_format.d.ts.map +1 -1
- package/dist/utils/safe_request_log_format.js +53 -0
- package/dist/utils/safe_request_log_format.js.map +1 -1
- package/docs/README.md +49 -0
- package/docs/developer/mcp/instructions.md +29 -18
- package/openapi.yaml +1240 -22
- package/package.json +10 -2
- package/dist/inspector/assets/agent_grant_form-DoLjngLK.js +0 -1
- package/dist/inspector/assets/interpretations-CJF4giZE.js +0 -1
- package/dist/inspector/assets/record_activity-Bcv9OocC.js +0 -1
- package/dist/inspector/assets/use_interpretations-L8TsWYxH.js +0 -1
package/dist/actions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { preflightStructuredStoreAdmission, } from "./services/store_admission.js";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import cookieParser from "cookie-parser";
|
|
3
4
|
import cors from "cors";
|
|
@@ -35,14 +36,14 @@ import { OwnedEntityNotFoundError, SOURCES_STORAGE_BUCKET } from "./services/sco
|
|
|
35
36
|
import { invalidEntityIdRelationshipRefusal, relationshipRefusalFromError, unresolvedRelationshipRefusal, } from "./services/store_relationships.js";
|
|
36
37
|
import { CursorError } from "./services/entity_cursor.js";
|
|
37
38
|
import { assertNoShadowedRoutes } from "./services/route_shadowing.js";
|
|
38
|
-
import { StorePolicyUnavailableError } from "./services/instance_policy.js";
|
|
39
|
+
import { StorePolicyDeniedError, StorePolicyUnavailableError } from "./services/instance_policy.js";
|
|
39
40
|
import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
|
|
40
|
-
import {
|
|
41
|
+
import { AccessPolicyError } from "./services/access_policy.js";
|
|
41
42
|
import { hashGuestAccessToken } from "./services/guest_access_token.js";
|
|
42
43
|
import { subscriptionWithinGuestScope } from "./services/subscriptions/guest_scope.js";
|
|
43
44
|
import { IssueTransportError, IssueValidationError } from "./services/issues/errors.js";
|
|
44
45
|
import { externalActorFromCallerInput } from "./services/issues/external_actor_builder.js";
|
|
45
|
-
import { getCurrentAAuthAdmission, getCurrentAgentIdentity, getCurrentAttribution, getCurrentExternalActor,
|
|
46
|
+
import { getCurrentAAuthAdmission, getCurrentAgentIdentity, getCurrentAttribution, getCurrentExternalActor, runWithAuthenticatedPrincipal, runWithExternalActor, runWithRequestContext, } from "./services/request_context.js";
|
|
46
47
|
import { assertCanWriteProtectedBatch } from "./services/protected_entity_types.js";
|
|
47
48
|
import { redactMemberAttribution } from "./services/attribution_redaction.js";
|
|
48
49
|
import { createAgentIdentity as buildAgentIdentity, getAgentIdentityFromRequest, normaliseClientName, } from "./crypto/agent_identity.js";
|
|
@@ -74,7 +75,8 @@ import { getDb } from "./repositories/db/connection.js";
|
|
|
74
75
|
import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
|
|
75
76
|
import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
|
|
76
77
|
import { buildGoogleAuthorizeUrl, createGoogleSigninNonce, consumeGoogleSigninNonce, exchangeGoogleAuthorizationCode, getSharedGraphUserId, GoogleIdTokenError, isGoogleSigninEnabled, verifyGoogleIdToken, } from "./services/google_oidc.js";
|
|
77
|
-
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";
|
|
78
|
+
import { AnalyzeSchemaCandidatesRequestSchema, AuditUndeclaredFragmentsRequestSchema, CorrectEntityRequestSchema, PatchArrayItemRequestSchema, 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, ObservationSourceSchema, StoreRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
|
|
79
|
+
import { DEFAULT_OBSERVATION_SOURCE } from "./services/observation_insert.js";
|
|
78
80
|
import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
|
|
79
81
|
import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
|
|
80
82
|
import { retrieveEntityByIdentifierWithFallback } from "./shared/action_handlers/entity_identifier_handler.js";
|
|
@@ -92,6 +94,7 @@ import { buildComplianceScorecard } from "./services/compliance/scorecard.js";
|
|
|
92
94
|
import { getAgent, listAgentRecords, listAgents } from "./services/agents_directory.js";
|
|
93
95
|
import { computeEntitySnapshotAtTime } from "./services/entity_snapshot_at_time.js";
|
|
94
96
|
import { isProductionEnvironment } from "./shared/environment.js";
|
|
97
|
+
import { isLoopbackHost, resolveBindHostFromEnv } from "./shared/bind_host.js";
|
|
95
98
|
// Set to true during startHTTPServer() when resolveSandboxMode() returns
|
|
96
99
|
// "local_sandbox". Read by getAuthenticatedUserId() to substitute the
|
|
97
100
|
// per-install fingerprinted sandbox principal for the shared nil UUID fallback.
|
|
@@ -455,21 +458,25 @@ function isInternalSeedRequest(req) {
|
|
|
455
458
|
// available so a single abusive client does not starve others. Operators
|
|
456
459
|
// can tune with NEOTOMA_WRITE_RATE_LIMIT_PER_MIN.
|
|
457
460
|
const WRITE_RATE_LIMIT_PER_MIN = Math.max(1, Number.parseInt(process.env.NEOTOMA_WRITE_RATE_LIMIT_PER_MIN || "", 10) || 120);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
461
|
+
/** Build the shared write limiter; exported so its limiting effect is testable without a listener. */
|
|
462
|
+
export function createWriteRateLimit(max = WRITE_RATE_LIMIT_PER_MIN) {
|
|
463
|
+
return rateLimit({
|
|
464
|
+
windowMs: 60 * 1000,
|
|
465
|
+
max,
|
|
466
|
+
keyGenerator: (req) => {
|
|
467
|
+
const userId = req.authenticatedUserId || "";
|
|
468
|
+
if (userId)
|
|
469
|
+
return `u:${userId}`;
|
|
470
|
+
// Use the library-provided IPv6-safe key helper; keying directly on
|
|
471
|
+
// req.ip lets IPv6 callers rotate the low-64 bits to bypass limits.
|
|
472
|
+
return `ip:${ipKeyGenerator(req.ip || "")}`;
|
|
473
|
+
},
|
|
474
|
+
skip: isInternalSeedRequest,
|
|
475
|
+
message: "Write rate limit exceeded, please slow down",
|
|
476
|
+
...rateLimitOptions,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const writeRateLimit = createWriteRateLimit();
|
|
473
480
|
// SECURITY: guest-capable write routes (issue submission / thread append,
|
|
474
481
|
// subscribe / unsubscribe) should not share the broader `/store` bucket.
|
|
475
482
|
// Key by cryptographic guest identity first, then scoped guest token, then IP.
|
|
@@ -4163,6 +4170,13 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
|
|
|
4163
4170
|
.status(error.statusCode)
|
|
4164
4171
|
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
4165
4172
|
}
|
|
4173
|
+
if (error instanceof StorePolicyDeniedError) {
|
|
4174
|
+
logWarn(logContext || "StorePolicyDenied", req, {
|
|
4175
|
+
denied_count: error.denied.length,
|
|
4176
|
+
reason_codes: [...new Set(error.denied.map((denial) => denial.reason_code))].sort(),
|
|
4177
|
+
});
|
|
4178
|
+
return res.status(400).json({ error: error.toErrorEnvelope() });
|
|
4179
|
+
}
|
|
4166
4180
|
if (error instanceof StorePolicyUnavailableError) {
|
|
4167
4181
|
// 503, not 400: the write was refused because the instance could not read
|
|
4168
4182
|
// its own policy, which is a service-side fault the caller can retry — not
|
|
@@ -4266,7 +4280,12 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
4266
4280
|
// the request for access control.
|
|
4267
4281
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
4268
4282
|
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;
|
|
4269
|
-
const { entities, total, applied_search_strategies, search_mode, next_cursor } = await queryEntitiesWithCount({
|
|
4283
|
+
const { entities, total, applied_search_strategies, search_mode, next_cursor, read_contract } = await queryEntitiesWithCount({
|
|
4284
|
+
readRequest: {
|
|
4285
|
+
raw: (req.method === "GET" ? req.query : rawInput),
|
|
4286
|
+
surface: req.method === "GET" ? "http_get" : "http_post",
|
|
4287
|
+
consumed: [...Object.keys(parsed.data), "query", "search_query"],
|
|
4288
|
+
},
|
|
4270
4289
|
userId,
|
|
4271
4290
|
entityType: entity_type,
|
|
4272
4291
|
entityTypes: entity_types,
|
|
@@ -4292,6 +4311,7 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
4292
4311
|
total,
|
|
4293
4312
|
limit,
|
|
4294
4313
|
offset,
|
|
4314
|
+
read_contract,
|
|
4295
4315
|
...(applied_search_strategies ? { applied_search_strategies } : {}),
|
|
4296
4316
|
search_mode,
|
|
4297
4317
|
...(next_cursor ? { next_cursor } : {}),
|
|
@@ -6259,9 +6279,9 @@ app.post("/observations/create", async (req, res) => {
|
|
|
6259
6279
|
entity_identifier: z.string(),
|
|
6260
6280
|
fields: z.record(z.unknown()),
|
|
6261
6281
|
source_priority: z.number().optional().default(100),
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6282
|
+
// Shared enum (#2241): an inline copy here had drifted from the store
|
|
6283
|
+
// schema's once already; reuse the single source instead.
|
|
6284
|
+
observation_source: ObservationSourceSchema.optional(),
|
|
6265
6285
|
source_peer_id: z.string().min(1).optional(),
|
|
6266
6286
|
user_id: z.string().uuid(),
|
|
6267
6287
|
});
|
|
@@ -6357,143 +6377,53 @@ async function completeInterpretationRun(params) {
|
|
|
6357
6377
|
})
|
|
6358
6378
|
.eq("id", params.interpretationId);
|
|
6359
6379
|
}
|
|
6380
|
+
/**
|
|
6381
|
+
* Structured store entry point.
|
|
6382
|
+
*
|
|
6383
|
+
* A committing store is a write: once it starts it runs to completion and is
|
|
6384
|
+
* not cancelled by the caller's abort signal, and a second call for the same
|
|
6385
|
+
* idempotency key waits for the first (#2144). See `runKeyedStoreWrite`.
|
|
6386
|
+
* Plan mode (`commit: false`) writes nothing and the conditional path is its
|
|
6387
|
+
* own atomic transaction, so both run as before.
|
|
6388
|
+
*/
|
|
6360
6389
|
export async function storeStructuredForApi(params) {
|
|
6361
|
-
|
|
6390
|
+
if (params.commit === false || params.expectedEntityAbsent === true) {
|
|
6391
|
+
return storeStructuredForApiUnguarded(params);
|
|
6392
|
+
}
|
|
6393
|
+
const { runKeyedStoreWrite } = await import("./services/store_write_state.js");
|
|
6394
|
+
return runKeyedStoreWrite(params.userId, params.idempotencyKey, () => storeStructuredForApiUnguarded(params));
|
|
6395
|
+
}
|
|
6396
|
+
async function storeStructuredForApiUnguarded(params) {
|
|
6397
|
+
const { userId, entities, sourcePriority, observationSource, sourcePeerId, idempotencyKey, originalFilename, relationships, interpretation, interpretationSourceId, commit: commitInput, strict: strictInput, dataOrigins, } = params;
|
|
6362
6398
|
const commit = commitInput !== false;
|
|
6363
6399
|
const strict = strictInput === true;
|
|
6364
|
-
|
|
6365
|
-
// via a grant), check per-entity-type access policies. If the policy allows
|
|
6366
|
-
// guest writes, the request proceeds without requiring an agent_grant.
|
|
6367
|
-
const requestContext = getRequestContext();
|
|
6368
|
-
const agentIdentity = getCurrentAgentIdentity();
|
|
6369
|
-
const admission = getCurrentAAuthAdmission();
|
|
6370
|
-
const isAAuthVerified = agentIdentity?.thumbprint != null;
|
|
6371
|
-
const isGuest = isAAuthVerified && (!admission || !admission.admitted);
|
|
6372
|
-
if (isGuest && !requestContext?.bypassGuestStoreAccessPolicy) {
|
|
6373
|
-
const entityTypes = entities
|
|
6374
|
-
.map((entity) => entity?.entity_type)
|
|
6375
|
-
.filter((t) => typeof t === "string" && t.length > 0);
|
|
6376
|
-
const guestId = {
|
|
6377
|
-
thumbprint: agentIdentity.thumbprint,
|
|
6378
|
-
sub: agentIdentity.sub,
|
|
6379
|
-
iss: agentIdentity.iss,
|
|
6380
|
-
};
|
|
6381
|
-
await assertGuestWriteAllowed(entityTypes, guestId);
|
|
6382
|
-
}
|
|
6383
|
-
// usage_digest store-seam redaction guard (server-side backstop).
|
|
6384
|
-
// Scope: strictly usage_digest entities only — no impact on any other entity type.
|
|
6385
|
-
// For each incoming usage_digest entity, scan free-text fields `notes` and
|
|
6386
|
-
// `friction_notes` via the pure redactUsageDigestEntity helper, and write the
|
|
6387
|
-
// redacted copies back onto the entity object so the raw input is never persisted.
|
|
6388
|
-
// This is a redact-and-store (scan) approach, not hard-reject, so a digest is
|
|
6389
|
-
// never silently dropped on a client-side redaction miss.
|
|
6390
|
-
//
|
|
6391
|
-
// NOTE: Single telemetry sink today — entity_type === "usage_digest" is the only
|
|
6392
|
-
// branch. If a second redacted-free-text entity_type appears, lift these field
|
|
6393
|
-
// names into schema metadata (redact_free_text_fields) rather than adding another
|
|
6394
|
-
// branch here.
|
|
6395
|
-
{
|
|
6396
|
-
const hasUsageDigest = entities.some((e) => e?.entity_type === "usage_digest");
|
|
6397
|
-
if (hasUsageDigest) {
|
|
6398
|
-
const { redactUsageDigestEntity } = await import("./services/feedback/usage_digest_redaction.js");
|
|
6399
|
-
for (const entityData of entities) {
|
|
6400
|
-
const ed = entityData;
|
|
6401
|
-
if (ed.entity_type !== "usage_digest")
|
|
6402
|
-
continue;
|
|
6403
|
-
const result = redactUsageDigestEntity(ed);
|
|
6404
|
-
if (result.applied) {
|
|
6405
|
-
logger.warn(`[STORE] usage_digest server-side redaction applied: ${result.hits} hit(s) in free-text fields.`);
|
|
6406
|
-
}
|
|
6407
|
-
}
|
|
6408
|
-
}
|
|
6409
|
-
}
|
|
6410
|
-
// Capability scoping: when the caller is an AAuth-verified agent covered
|
|
6411
|
-
// by the capability registry, gate store_structured by entity_type here
|
|
6412
|
-
// before any writes touch the DB. Guests who passed access policy above
|
|
6413
|
-
// skip grant-based capability enforcement. Unknown / anonymous callers
|
|
6414
|
-
// fall through (attribution_policy still gates their writes downstream).
|
|
6415
|
-
const capabilityCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
|
|
6416
|
-
if (capabilityCtx && !isGuest) {
|
|
6417
|
-
const entityTypes = entities
|
|
6418
|
-
.map((entity) => entity?.entity_type)
|
|
6419
|
-
.filter((t) => typeof t === "string" && t.length > 0);
|
|
6420
|
-
enforceAgentCapability("store", entityTypes, capabilityCtx);
|
|
6421
|
-
const relationshipOp = Array.isArray(relationships) && relationships.length > 0;
|
|
6422
|
-
if (relationshipOp) {
|
|
6423
|
-
enforceAgentCapability("create_relationship", entityTypes, capabilityCtx);
|
|
6424
|
-
}
|
|
6425
|
-
}
|
|
6426
|
-
// Relationship-type validation, UP FRONT and BEFORE any entity is persisted
|
|
6427
|
-
// (#1972 / G25).
|
|
6428
|
-
//
|
|
6429
|
-
// This closes the more damaging half of the closed-vocabulary defect. The
|
|
6430
|
-
// relationships leg further down catches per edge and downgrades to
|
|
6431
|
-
// logger.warn, so a store carrying an edge the substrate would not accept
|
|
6432
|
-
// returned SUCCESS with the edge silently absent — a caller believed it had
|
|
6433
|
-
// written a graph it had not written, and nothing in the response said
|
|
6434
|
-
// otherwise. Validating here means an unacceptable type is a refusal the
|
|
6435
|
-
// caller sees, and no entities are written that would have been orphaned by
|
|
6436
|
-
// the edge that was going to fail anyway.
|
|
6437
|
-
if (commit && Array.isArray(relationships) && relationships.length > 0) {
|
|
6438
|
-
const { relationshipsService } = await import("./services/relationships.js");
|
|
6439
|
-
const seen = new Set();
|
|
6440
|
-
for (const rel of relationships) {
|
|
6441
|
-
const type = rel?.relationship_type;
|
|
6442
|
-
if (typeof type !== "string" || seen.has(type))
|
|
6443
|
-
continue;
|
|
6444
|
-
seen.add(type);
|
|
6445
|
-
await relationshipsService.assertRegisteredType(type, userId);
|
|
6446
|
-
}
|
|
6447
|
-
}
|
|
6448
|
-
// Protected-entity-types guard: governance state (`agent_grant`, etc.)
|
|
6449
|
-
// is gated by an explicit capability on the admitted grant. Mirrors
|
|
6450
|
-
// the same check made deep in `createObservation` so callers see a
|
|
6451
|
-
// structured `capability_denied` envelope before any writes.
|
|
6452
|
-
{
|
|
6453
|
-
const entityTypes = entities
|
|
6454
|
-
.map((entity) => entity?.entity_type)
|
|
6455
|
-
.filter((t) => typeof t === "string" && t.length > 0);
|
|
6456
|
-
assertCanWriteProtectedBatch({
|
|
6457
|
-
entity_types: entityTypes,
|
|
6458
|
-
op: "store",
|
|
6459
|
-
identity: getCurrentAgentIdentity(),
|
|
6460
|
-
admission: getCurrentAAuthAdmission(),
|
|
6461
|
-
});
|
|
6462
|
-
}
|
|
6463
|
-
// A key thumbprint may be pinned by agent_grants under one owner only.
|
|
6464
|
-
// Checked before any write so a refused pin persists nothing; the same
|
|
6465
|
-
// check runs again at the observation insert.
|
|
6466
|
-
if (entities.some((entity) => entity?.entity_type === "agent_grant")) {
|
|
6467
|
-
const { assertGrantWriteKeepsPinUnique } = await import("./services/agent_grants.js");
|
|
6468
|
-
for (const entity of entities) {
|
|
6469
|
-
if (entity?.entity_type !== "agent_grant")
|
|
6470
|
-
continue;
|
|
6471
|
-
await assertGrantWriteKeepsPinUnique({
|
|
6472
|
-
userId,
|
|
6473
|
-
entityType: "agent_grant",
|
|
6474
|
-
fields: entity,
|
|
6475
|
-
});
|
|
6476
|
-
}
|
|
6477
|
-
}
|
|
6400
|
+
await preflightStructuredStoreAdmission(params);
|
|
6478
6401
|
const { resolveEntityWithTrace, CanonicalNameUnresolvedError, MergeRefusedError } = await import("./services/entity_resolution.js");
|
|
6479
|
-
const { detectFlatPackedRows, FlatPackedRowsError } = await import("./services/flat_packed_detection.js");
|
|
6480
6402
|
// R4: lazy import so the structured-store handler can attach required
|
|
6481
6403
|
// identity-field hints when resolving fails under a reject policy.
|
|
6482
6404
|
const registryModule = await import("./services/schema_registry.js");
|
|
6483
6405
|
const deriveRequiredIdentityFields = registryModule.deriveRequiredIdentityFields;
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6406
|
+
if (params.expectedEntityAbsent === true) {
|
|
6407
|
+
const { storeConditionalStructured } = await import("./services/store_conditional.js");
|
|
6408
|
+
return storeConditionalStructured({
|
|
6409
|
+
userId,
|
|
6410
|
+
entities,
|
|
6411
|
+
sourcePriority,
|
|
6412
|
+
observationSource,
|
|
6413
|
+
idempotencyKey,
|
|
6414
|
+
originalFilename,
|
|
6415
|
+
strict,
|
|
6416
|
+
dataOrigin: dataOrigins?.[0],
|
|
6417
|
+
});
|
|
6493
6418
|
}
|
|
6494
6419
|
// Compute hash of incoming content before querying so we can detect
|
|
6495
|
-
// idempotency-key reuse with different content (issue #186).
|
|
6496
|
-
|
|
6420
|
+
// idempotency-key reuse with different content (issue #186). Each entity's
|
|
6421
|
+
// effective data_origin (#2242) is part of the stored payload, so a replay
|
|
6422
|
+
// with a different origin is a mismatch rather than a silent drop; with no
|
|
6423
|
+
// origin the stored JSON is unchanged.
|
|
6424
|
+
const { entitiesWithStoredDataOrigins } = await import("./services/provenance_data_origin.js");
|
|
6425
|
+
const storedEntities = entitiesWithStoredDataOrigins(entities, dataOrigins);
|
|
6426
|
+
const incomingJsonContent = JSON.stringify(storedEntities, (key, value) => {
|
|
6497
6427
|
if (typeof value === "bigint") {
|
|
6498
6428
|
return Number(value);
|
|
6499
6429
|
}
|
|
@@ -6501,9 +6431,14 @@ export async function storeStructuredForApi(params) {
|
|
|
6501
6431
|
});
|
|
6502
6432
|
const { computeContentHash: _computeContentHash } = await import("./services/raw_storage.js");
|
|
6503
6433
|
const incomingContentHash = _computeContentHash(Buffer.from(incomingJsonContent, "utf-8"));
|
|
6434
|
+
if (commit) {
|
|
6435
|
+
const { assertLegacyStoreKeyMode } = await import("./services/store_condition_keys.js");
|
|
6436
|
+
const { getDb } = await import("./repositories/db/connection.js");
|
|
6437
|
+
await assertLegacyStoreKeyMode(await getDb(), userId, idempotencyKey);
|
|
6438
|
+
}
|
|
6504
6439
|
const { data: existingSource, error: existingSourceError } = await db
|
|
6505
6440
|
.from("sources")
|
|
6506
|
-
.select("id, content_hash")
|
|
6441
|
+
.select("id, content_hash, provenance")
|
|
6507
6442
|
.eq("user_id", userId)
|
|
6508
6443
|
.eq("idempotency_key", idempotencyKey)
|
|
6509
6444
|
.maybeSingle();
|
|
@@ -6523,6 +6458,51 @@ export async function storeStructuredForApi(params) {
|
|
|
6523
6458
|
"To update an entity, omit the idempotency_key or use a new one.";
|
|
6524
6459
|
throw mismatchErr;
|
|
6525
6460
|
}
|
|
6461
|
+
}
|
|
6462
|
+
// A recorded key is not a finished write (#2144): the source row is created
|
|
6463
|
+
// before any observation, so a loop that stopped part way leaves a key that
|
|
6464
|
+
// looks done. `resumeIncompleteWrite` is true when the earlier attempt did
|
|
6465
|
+
// not finish; the replay branch below is then skipped and the ordinary write
|
|
6466
|
+
// path runs again over the same source. That is safe because observation ids
|
|
6467
|
+
// are deterministic (source, entity, fields), so entities already written
|
|
6468
|
+
// resolve to their existing observation and only the missing ones are added.
|
|
6469
|
+
let resumeIncompleteWrite = false;
|
|
6470
|
+
if (existingSource && commit) {
|
|
6471
|
+
const storeWriteState = await import("./services/store_write_state.js");
|
|
6472
|
+
const recordedState = storeWriteState.readStoreWriteState(existingSource.provenance);
|
|
6473
|
+
const incomplete = recordedState === "in_progress" ||
|
|
6474
|
+
(recordedState === undefined &&
|
|
6475
|
+
storeWriteState.legacyWriteLooksIncomplete({
|
|
6476
|
+
entities,
|
|
6477
|
+
observationCount: (await (await import("./services/observation_storage.js")).listObservationsForSource(existingSource.id, userId)).length,
|
|
6478
|
+
observationsOnKeyedSource: !interpretation?.source_id && !interpretationSourceId,
|
|
6479
|
+
}));
|
|
6480
|
+
if (incomplete) {
|
|
6481
|
+
if (interpretation) {
|
|
6482
|
+
// A resumed write would open a fresh interpretation run, whose id is
|
|
6483
|
+
// part of each observation id, so the entities already written would
|
|
6484
|
+
// be written a second time. Report the incomplete write instead.
|
|
6485
|
+
// Count where the interrupted attempt wrote its observations: the
|
|
6486
|
+
// interpretation's own source when it names one (same resolution as
|
|
6487
|
+
// `observationSourceId` below and the MCP path), else the keyed source.
|
|
6488
|
+
const summary = await storeWriteState.summarizeUnfinishedStore({
|
|
6489
|
+
userId,
|
|
6490
|
+
sourceId: interpretation.source_id ?? interpretationSourceId ?? existingSource.id,
|
|
6491
|
+
entities: entities,
|
|
6492
|
+
});
|
|
6493
|
+
const incompleteErr = new Error(`ERR_STORE_WRITE_INCOMPLETE: idempotency_key "${idempotencyKey}" belongs to a store ` +
|
|
6494
|
+
`that did not finish writing (${summary.entities_written} of ` +
|
|
6495
|
+
`${summary.entities_requested} entities have an observation). It cannot be resumed ` +
|
|
6496
|
+
"under the same key because the request carries an interpretation.");
|
|
6497
|
+
incompleteErr.code = "ERR_STORE_WRITE_INCOMPLETE";
|
|
6498
|
+
incompleteErr.hint = storeWriteState.incompleteStoreHint(summary);
|
|
6499
|
+
incompleteErr.details = { ...summary };
|
|
6500
|
+
throw incompleteErr;
|
|
6501
|
+
}
|
|
6502
|
+
resumeIncompleteWrite = true;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
if (existingSource && !resumeIncompleteWrite) {
|
|
6526
6506
|
const { listObservationsForSource } = await import("./services/observation_storage.js");
|
|
6527
6507
|
const existingObs = await listObservationsForSource(existingSource.id, userId);
|
|
6528
6508
|
// Issue #1860 (mirrors the #1840 MCP-path fix in src/server.ts): on a
|
|
@@ -6598,24 +6578,39 @@ export async function storeStructuredForApi(params) {
|
|
|
6598
6578
|
return entry?.schema_definition ?? null;
|
|
6599
6579
|
});
|
|
6600
6580
|
}
|
|
6581
|
+
const { preflightStructuredStoreOwnership } = await import("./services/store_ownership_admission.js");
|
|
6582
|
+
if (commit)
|
|
6583
|
+
await preflightStructuredStoreOwnership(params);
|
|
6601
6584
|
const { createObservation } = await import("./services/observation_storage.js");
|
|
6602
6585
|
const jsonContent = incomingJsonContent;
|
|
6603
6586
|
const filenameForStorage = originalFilename?.trim() || undefined;
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6587
|
+
// Plan mode (`commit: false`, CLI `--plan`/`--dry-run`) must be genuinely
|
|
6588
|
+
// side-effect-free: no `sources` row, no raw-storage upload. storeRawContent
|
|
6589
|
+
// persists both, so it must not run at all when not committing. Everything
|
|
6590
|
+
// storageResult feeds below — resolvedInterpretationSourceId,
|
|
6591
|
+
// observationSourceId, and interpretationId — is consumed exclusively
|
|
6592
|
+
// inside `if (commit)` blocks further down (observation creation, event
|
|
6593
|
+
// emission, auto-linking), so a null placeholder here is never written to
|
|
6594
|
+
// a row; pass 1 entity resolution (resolveEntityWithTrace, below) takes no
|
|
6595
|
+
// source id at all.
|
|
6596
|
+
const storageResult = commit
|
|
6597
|
+
? await storeRawContent({
|
|
6598
|
+
userId,
|
|
6599
|
+
fileBuffer: Buffer.from(jsonContent, "utf-8"),
|
|
6600
|
+
mimeType: "application/json",
|
|
6601
|
+
originalFilename: filenameForStorage,
|
|
6602
|
+
idempotencyKey,
|
|
6603
|
+
provenance: {
|
|
6604
|
+
upload_method: "api_store",
|
|
6605
|
+
client: "api",
|
|
6606
|
+
source_priority: sourcePriority,
|
|
6607
|
+
...(await import("./services/store_write_state.js")).inProgressStoreWriteProvenance(),
|
|
6608
|
+
},
|
|
6609
|
+
})
|
|
6610
|
+
: null;
|
|
6616
6611
|
const resolvedInterpretationSourceId = interpretation?.source_id ??
|
|
6617
6612
|
interpretationSourceId ??
|
|
6618
|
-
(interpretation?.source_ref === "structured" ? storageResult
|
|
6613
|
+
(interpretation?.source_ref === "structured" ? storageResult?.sourceId : undefined);
|
|
6619
6614
|
const interpretationId = commit && interpretation && resolvedInterpretationSourceId
|
|
6620
6615
|
? await createInterpretationRunForSource({
|
|
6621
6616
|
userId,
|
|
@@ -6623,7 +6618,7 @@ export async function storeStructuredForApi(params) {
|
|
|
6623
6618
|
interpretationConfig: interpretation.interpretation_config,
|
|
6624
6619
|
})
|
|
6625
6620
|
: null;
|
|
6626
|
-
const observationSourceId = resolvedInterpretationSourceId ?? storageResult
|
|
6621
|
+
const observationSourceId = resolvedInterpretationSourceId ?? storageResult?.sourceId;
|
|
6627
6622
|
// v0.5.1: structured guidance for the v0.5.0 breaking change where callers
|
|
6628
6623
|
// nested fields under `attributes`. If resolution failed and the only
|
|
6629
6624
|
// observed top-level keys are `attributes` (plus optionally `entity_type`),
|
|
@@ -6644,6 +6639,11 @@ export async function storeStructuredForApi(params) {
|
|
|
6644
6639
|
}
|
|
6645
6640
|
const resolved = [];
|
|
6646
6641
|
const issues = [];
|
|
6642
|
+
// One fresh set per resolution pass when resuming (#2144): an entity an
|
|
6643
|
+
// earlier payload of this request already resolved to is a collision between
|
|
6644
|
+
// two payloads, not this write's own row, so a later payload landing on it is
|
|
6645
|
+
// judged by the ordinary strict / collision rules.
|
|
6646
|
+
const resumeClaimedEntityIds = resumeIncompleteWrite ? new Set() : undefined;
|
|
6647
6647
|
for (let observation_index = 0; observation_index < entities.length; observation_index++) {
|
|
6648
6648
|
const entityData = entities[observation_index];
|
|
6649
6649
|
let entity_type = entityData.entity_type;
|
|
@@ -6690,7 +6690,12 @@ export async function storeStructuredForApi(params) {
|
|
|
6690
6690
|
commit,
|
|
6691
6691
|
strict: effectiveStrict,
|
|
6692
6692
|
targetId,
|
|
6693
|
+
// Resuming this key's own interrupted write: entity rows it created
|
|
6694
|
+
// are not merge targets (#2144).
|
|
6695
|
+
resumeSourceId: resumeIncompleteWrite ? existingSource?.id : undefined,
|
|
6696
|
+
resumeClaimedEntityIds,
|
|
6693
6697
|
});
|
|
6698
|
+
resumeClaimedEntityIds?.add(result.entityId);
|
|
6694
6699
|
resolved.push({
|
|
6695
6700
|
observation_index,
|
|
6696
6701
|
entity_type,
|
|
@@ -6947,6 +6952,15 @@ export async function storeStructuredForApi(params) {
|
|
|
6947
6952
|
let observation_id = null;
|
|
6948
6953
|
let snapshotAfter = null;
|
|
6949
6954
|
if (commit) {
|
|
6955
|
+
// Invariant: storageResult is only ever null when `commit` is false
|
|
6956
|
+
// (plan mode skips storeRawContent entirely — see above). Reaching this
|
|
6957
|
+
// branch with a null observationSourceId would mean an observation is
|
|
6958
|
+
// about to be written with no traceable source, which is worse than
|
|
6959
|
+
// failing loudly here.
|
|
6960
|
+
if (observationSourceId === undefined) {
|
|
6961
|
+
throw new Error("Internal invariant violated: commit=true but no source id was resolved " +
|
|
6962
|
+
"(storeRawContent should have run under commit mode).");
|
|
6963
|
+
}
|
|
6950
6964
|
const { data: priorSnapRow } = await db
|
|
6951
6965
|
.from("entity_snapshots")
|
|
6952
6966
|
.select("snapshot")
|
|
@@ -6970,6 +6984,7 @@ export async function storeStructuredForApi(params) {
|
|
|
6970
6984
|
user_id: userId,
|
|
6971
6985
|
identity_basis: r.trace.identity_basis,
|
|
6972
6986
|
identity_rule: r.trace.identity_rule,
|
|
6987
|
+
data_origin: dataOrigins?.[r.observation_index],
|
|
6973
6988
|
});
|
|
6974
6989
|
observation_id = obsData.id;
|
|
6975
6990
|
try {
|
|
@@ -6986,7 +7001,7 @@ export async function storeStructuredForApi(params) {
|
|
|
6986
7001
|
const obsRow = obsData;
|
|
6987
7002
|
const emitTs = obsRow.observed_at ?? new Date().toISOString();
|
|
6988
7003
|
const obsSourceLabel = observationSource === undefined || observationSource === null
|
|
6989
|
-
?
|
|
7004
|
+
? DEFAULT_OBSERVATION_SOURCE
|
|
6990
7005
|
: String(observationSource);
|
|
6991
7006
|
emitObservationCreated({
|
|
6992
7007
|
user_id: userId,
|
|
@@ -7154,6 +7169,14 @@ export async function storeStructuredForApi(params) {
|
|
|
7154
7169
|
}
|
|
7155
7170
|
}
|
|
7156
7171
|
}
|
|
7172
|
+
// Every observation (and the request's relationships) is written. Record
|
|
7173
|
+
// that on the keyed source so a replay of this key knows the write finished
|
|
7174
|
+
// (#2144). Before this point a replay resumes the write instead of reporting
|
|
7175
|
+
// whatever happened to land.
|
|
7176
|
+
if (commit && storageResult) {
|
|
7177
|
+
const { markStoreWriteComplete } = await import("./services/store_write_state.js");
|
|
7178
|
+
await markStoreWriteComplete(storageResult.sourceId, userId);
|
|
7179
|
+
}
|
|
7157
7180
|
// R3: aggregate non-fatal resolver warnings across the batch so callers
|
|
7158
7181
|
// (agents, Inspector, CI) can audit identity quality without walking every
|
|
7159
7182
|
// entity.trace. PII-free by construction — see ResolverWarning shape.
|
|
@@ -7385,6 +7408,20 @@ export async function storeStructuredForApi(params) {
|
|
|
7385
7408
|
}
|
|
7386
7409
|
}
|
|
7387
7410
|
}
|
|
7411
|
+
// #2241: OBSERVATION_SOURCE_OMITTED — the request did not say what kind of
|
|
7412
|
+
// write this was, so every observation was stored as `unspecified`. Judged
|
|
7413
|
+
// on the raw request value (an explicit `unspecified` does not warn) and
|
|
7414
|
+
// emitted once per request, because observation_source is request-scoped.
|
|
7415
|
+
{
|
|
7416
|
+
const { buildObservationSourceOmittedWarning } = await import("./services/observation_source_warning.js");
|
|
7417
|
+
const firstStored = [...resolved].sort((a, b) => a.observation_index - b.observation_index)[0];
|
|
7418
|
+
const obsSourceWarn = buildObservationSourceOmittedWarning({
|
|
7419
|
+
rawObservationSource: observationSource,
|
|
7420
|
+
firstStored,
|
|
7421
|
+
});
|
|
7422
|
+
if (obsSourceWarn)
|
|
7423
|
+
schemaStoreWarnings.push(obsSourceWarn);
|
|
7424
|
+
}
|
|
7388
7425
|
// #1963: surface auto-link edge retractions on the store response so a
|
|
7389
7426
|
// retraction — and especially a retraction *failure* — is never silent. A
|
|
7390
7427
|
// failure means a stale reference-field edge may still be live despite the
|
|
@@ -7511,7 +7548,7 @@ export async function storeStructuredForApi(params) {
|
|
|
7511
7548
|
success: true,
|
|
7512
7549
|
replayed: false,
|
|
7513
7550
|
commit,
|
|
7514
|
-
source_id: commit ? storageResult.sourceId : null,
|
|
7551
|
+
source_id: commit && storageResult ? storageResult.sourceId : null,
|
|
7515
7552
|
...(interpretationId
|
|
7516
7553
|
? { interpretation_id: interpretationId, interpretation_source_id: observationSourceId }
|
|
7517
7554
|
: {}),
|
|
@@ -7546,14 +7583,42 @@ export async function storeStructuredForApi(params) {
|
|
|
7546
7583
|
// target here today. An instance with enforcement: "enforced" still accepts
|
|
7547
7584
|
// arbitrary raw file content through this path. See the "Known scope
|
|
7548
7585
|
// boundary" section in src/services/instance_policy.ts's docblock.
|
|
7549
|
-
async function storeUnstructuredForApi(params) {
|
|
7550
|
-
const { fileContent, fileBuffer, filePath, mimeType, idempotencyKey, originalFilename, sourceType, userId, storageMode = "inline", } = params;
|
|
7586
|
+
export async function storeUnstructuredForApi(params) {
|
|
7587
|
+
const { fileContent, fileBuffer, filePath, mimeType, idempotencyKey, originalFilename, sourceType, userId, storageMode = "inline", commit = true, } = params;
|
|
7551
7588
|
// decodeFileContent rejects non-base64 instead of letting Node discard the
|
|
7552
7589
|
// invalid characters and store corrupted bytes (#2325).
|
|
7553
7590
|
const resolvedFileBuffer = fileBuffer ?? (fileContent !== undefined ? decodeFileContent(fileContent) : undefined);
|
|
7554
7591
|
if (!resolvedFileBuffer && storageMode !== "reference") {
|
|
7555
7592
|
throw new Error("fileContent or fileBuffer is required for inline storage");
|
|
7556
7593
|
}
|
|
7594
|
+
if (!commit) {
|
|
7595
|
+
// Nothing to persist in plan mode: no bytes uploaded, no `sources` row,
|
|
7596
|
+
// no reference row. `content_hash` is still safe (and useful) to report
|
|
7597
|
+
// since it's a pure function of the caller's own bytes/path, computed
|
|
7598
|
+
// without touching storage or the database.
|
|
7599
|
+
if (storageMode === "reference") {
|
|
7600
|
+
if (!filePath) {
|
|
7601
|
+
throw new Error("filePath is required for reference storage mode");
|
|
7602
|
+
}
|
|
7603
|
+
return {
|
|
7604
|
+
commit: false,
|
|
7605
|
+
source_id: null,
|
|
7606
|
+
storage_mode: "reference",
|
|
7607
|
+
reference_path: filePath,
|
|
7608
|
+
mime_type: mimeType,
|
|
7609
|
+
};
|
|
7610
|
+
}
|
|
7611
|
+
const { computeContentHash } = await import("./services/raw_storage.js");
|
|
7612
|
+
return {
|
|
7613
|
+
commit: false,
|
|
7614
|
+
source_id: null,
|
|
7615
|
+
content_hash: computeContentHash(resolvedFileBuffer),
|
|
7616
|
+
file_size: resolvedFileBuffer.length,
|
|
7617
|
+
deduplicated: false,
|
|
7618
|
+
entities_created: 0,
|
|
7619
|
+
observations_created: 0,
|
|
7620
|
+
};
|
|
7621
|
+
}
|
|
7557
7622
|
if (storageMode === "reference") {
|
|
7558
7623
|
if (!filePath) {
|
|
7559
7624
|
throw new Error("filePath is required for reference storage mode");
|
|
@@ -7613,12 +7678,41 @@ async function handleStorePost(req, res) {
|
|
|
7613
7678
|
});
|
|
7614
7679
|
return sendStoreValidationError(res, parsed.error.issues);
|
|
7615
7680
|
}
|
|
7681
|
+
let storeDataOrigins;
|
|
7682
|
+
// #2242: peel the reserved per-entity `data_origin` key off every entity and
|
|
7683
|
+
// resolve each entity's effective origin (entity → request → none) before
|
|
7684
|
+
// anything else reads the entities, so no downstream path sees it as a field.
|
|
7685
|
+
if (parsed.data.entities?.length) {
|
|
7686
|
+
const { splitEntityDataOrigins } = await import("./services/provenance_data_origin.js");
|
|
7687
|
+
const split = splitEntityDataOrigins(parsed.data.entities, parsed.data.data_origin);
|
|
7688
|
+
parsed.data.entities = split.entities;
|
|
7689
|
+
storeDataOrigins = split.origins;
|
|
7690
|
+
}
|
|
7616
7691
|
try {
|
|
7617
7692
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
7693
|
+
const { assertConditionalStoreRequest } = await import("./services/store_condition_request.js");
|
|
7694
|
+
assertConditionalStoreRequest(parsed.data);
|
|
7618
7695
|
const hasEntities = Boolean(parsed.data.entities?.length);
|
|
7619
7696
|
const hasFileContent = Boolean(parsed.data.file_content && parsed.data.mime_type);
|
|
7620
7697
|
const hasFilePath = Boolean(parsed.data.file_path);
|
|
7621
7698
|
const hasUnstructured = hasFileContent || hasFilePath;
|
|
7699
|
+
if (hasEntities && hasUnstructured && parsed.data.commit !== false) {
|
|
7700
|
+
const { claimCombinedStoreKeys } = await import("./services/store_combined_admission.js");
|
|
7701
|
+
await claimCombinedStoreKeys({
|
|
7702
|
+
userId,
|
|
7703
|
+
entities: parsed.data.entities,
|
|
7704
|
+
sourcePriority: parsed.data.source_priority,
|
|
7705
|
+
idempotencyKey: parsed.data.idempotency_key,
|
|
7706
|
+
relationships: parsed.data.relationships,
|
|
7707
|
+
interpretation: parsed.data.interpretation,
|
|
7708
|
+
sourcePeerId: parsed.data.source_peer_id,
|
|
7709
|
+
strict: parsed.data.strict,
|
|
7710
|
+
}, {
|
|
7711
|
+
key: parsed.data.file_idempotency_key,
|
|
7712
|
+
content: parsed.data.file_content,
|
|
7713
|
+
filePath: parsed.data.file_path,
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7622
7716
|
let structuredResult;
|
|
7623
7717
|
let unstructuredResult;
|
|
7624
7718
|
const storeUnstructuredFromRequest = async () => {
|
|
@@ -7679,6 +7773,7 @@ async function handleStorePost(req, res) {
|
|
|
7679
7773
|
originalFilename,
|
|
7680
7774
|
sourceType: parsed.data.source_type,
|
|
7681
7775
|
storageMode: parsed.data.source_storage,
|
|
7776
|
+
commit: parsed.data.commit,
|
|
7682
7777
|
});
|
|
7683
7778
|
};
|
|
7684
7779
|
if (hasUnstructured && parsed.data.interpretation?.source_ref === "unstructured") {
|
|
@@ -7695,6 +7790,7 @@ async function handleStorePost(req, res) {
|
|
|
7695
7790
|
const doStore = () => storeStructuredForApi({
|
|
7696
7791
|
userId,
|
|
7697
7792
|
entities: parsed.data.entities,
|
|
7793
|
+
expectedEntityAbsent: parsed.data.expected_entity_absent,
|
|
7698
7794
|
sourcePriority: parsed.data.source_priority ?? 100,
|
|
7699
7795
|
observationSource: parsed.data.observation_source,
|
|
7700
7796
|
sourcePeerId: parsed.data.source_peer_id,
|
|
@@ -7709,6 +7805,7 @@ async function handleStorePost(req, res) {
|
|
|
7709
7805
|
: undefined,
|
|
7710
7806
|
commit: parsed.data.commit,
|
|
7711
7807
|
strict: parsed.data.strict,
|
|
7808
|
+
dataOrigins: storeDataOrigins,
|
|
7712
7809
|
});
|
|
7713
7810
|
// A request body can only claim an external actor; verified tiers are
|
|
7714
7811
|
// assigned by server-side verification paths, never taken from input.
|
|
@@ -7753,6 +7850,15 @@ async function handleStorePost(req, res) {
|
|
|
7753
7850
|
return sendError(res, 400, error.code, error.message, error.details);
|
|
7754
7851
|
}
|
|
7755
7852
|
const errCode = error && typeof error === "object" ? error.code : undefined;
|
|
7853
|
+
const { StoreConditionError } = await import("./services/store_condition_keys.js");
|
|
7854
|
+
if (error instanceof StoreConditionError) {
|
|
7855
|
+
const status = error.code === "VALIDATION_ERROR"
|
|
7856
|
+
? 400
|
|
7857
|
+
: error.code === "STORE_RECEIPT_UNCERTAIN" || error.code === "STORE_CONDITIONAL_FAILED"
|
|
7858
|
+
? 503
|
|
7859
|
+
: 409;
|
|
7860
|
+
return res.status(status).json({ error: error.toErrorEnvelope() });
|
|
7861
|
+
}
|
|
7756
7862
|
if (isGrantPinConflict(error)) {
|
|
7757
7863
|
logWarn("AgentGrantPinConflict:store", req, { code: error.code });
|
|
7758
7864
|
return sendError(res, 409, error.code, error.message, { field: error.field });
|
|
@@ -7779,6 +7885,16 @@ async function handleStorePost(req, res) {
|
|
|
7779
7885
|
logWarn("IdempotencyCollision:store", req, { message });
|
|
7780
7886
|
return sendError(res, 409, "ERR_IDEMPOTENCY_COLLISION", message);
|
|
7781
7887
|
}
|
|
7888
|
+
if (error && typeof error === "object" && errCode === "ERR_STORE_WRITE_INCOMPLETE") {
|
|
7889
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7890
|
+
const hint = error.hint;
|
|
7891
|
+
logWarn("StoreWriteIncomplete:store", req, { message });
|
|
7892
|
+
const incompleteDetails = error.details;
|
|
7893
|
+
return sendError(res, 409, "ERR_STORE_WRITE_INCOMPLETE", message, {
|
|
7894
|
+
...(hint ? { hint } : {}),
|
|
7895
|
+
...(incompleteDetails ?? {}),
|
|
7896
|
+
});
|
|
7897
|
+
}
|
|
7782
7898
|
if (error && typeof error === "object" && errCode === "ERR_STORE_POLICY_DENIED") {
|
|
7783
7899
|
const err = error;
|
|
7784
7900
|
// Log counts and reason codes only. The denied entities' field values are
|
|
@@ -7867,6 +7983,16 @@ async function handleStorePost(req, res) {
|
|
|
7867
7983
|
hint: error.hint,
|
|
7868
7984
|
});
|
|
7869
7985
|
}
|
|
7986
|
+
// A capability refusal is the caller's authorization, not a server fault:
|
|
7987
|
+
// 403 with the same envelope `handleApiError` gives every other route.
|
|
7988
|
+
// Falling through made a denied edge (or entity type) read as a retryable
|
|
7989
|
+
// 500 DB_QUERY_FAILED.
|
|
7990
|
+
if (error instanceof AgentCapabilityError) {
|
|
7991
|
+
logWarn("AgentCapabilityRejection:store", req, error.toErrorEnvelope());
|
|
7992
|
+
return res
|
|
7993
|
+
.status(403)
|
|
7994
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
7995
|
+
}
|
|
7870
7996
|
logError("APIError:store", req, error);
|
|
7871
7997
|
const message = error instanceof Error ? error.message : "Failed to store payload";
|
|
7872
7998
|
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
@@ -8318,7 +8444,7 @@ app.post("/get_entity_snapshot", async (req, res) => {
|
|
|
8318
8444
|
});
|
|
8319
8445
|
return sendValidationError(res, parsed.error.issues);
|
|
8320
8446
|
}
|
|
8321
|
-
const { entity_id, at, at_ingested } = parsed.data;
|
|
8447
|
+
const { entity_id, at, at_ingested, include_cleared_fields } = parsed.data;
|
|
8322
8448
|
let userId;
|
|
8323
8449
|
try {
|
|
8324
8450
|
userId = await getAuthenticatedUserId(req);
|
|
@@ -8330,22 +8456,42 @@ app.post("/get_entity_snapshot", async (req, res) => {
|
|
|
8330
8456
|
// helper so the offline path honours them with the same semantics as the MCP
|
|
8331
8457
|
// path in server.ts. Without cutoffs, fall through to the fast materialized
|
|
8332
8458
|
// entity_snapshots table read.
|
|
8333
|
-
if (at || at_ingested) {
|
|
8459
|
+
if (at || at_ingested || include_cleared_fields) {
|
|
8334
8460
|
try {
|
|
8335
|
-
const result = await computeEntitySnapshotAtTime(entity_id, userId, at, at_ingested
|
|
8461
|
+
const result = await computeEntitySnapshotAtTime(entity_id, userId, at, at_ingested, {
|
|
8462
|
+
includeClearedFields: include_cleared_fields,
|
|
8463
|
+
});
|
|
8464
|
+
if (include_cleared_fields && !at && !at_ingested) {
|
|
8465
|
+
const { isEntityDeleted } = await import("./services/deletion.js");
|
|
8466
|
+
if (result && (await isEntityDeleted(result.entity_id, userId)))
|
|
8467
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
8468
|
+
}
|
|
8336
8469
|
if (result === null) {
|
|
8337
8470
|
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
8338
8471
|
}
|
|
8339
8472
|
logDebug("Success:get_entity_snapshot:at_time", req, { entity_id, at, at_ingested });
|
|
8340
|
-
return res.json(result);
|
|
8473
|
+
return res.json(await withSnapshotDataOrigins(result, userId));
|
|
8341
8474
|
}
|
|
8342
8475
|
catch (err) {
|
|
8343
8476
|
logError("Error:get_entity_snapshot:at_time", req, err);
|
|
8344
8477
|
return sendError(res, 500, "DB_QUERY_FAILED", err.message);
|
|
8345
8478
|
}
|
|
8346
8479
|
}
|
|
8347
|
-
//
|
|
8348
|
-
//
|
|
8480
|
+
// Materialization is derived state. Validate owned current authority before
|
|
8481
|
+
// serving its row, including restrictive corruption acquisition failures.
|
|
8482
|
+
try {
|
|
8483
|
+
const { isEntityDeleted } = await import("./services/deletion.js");
|
|
8484
|
+
const { hasRecordedEntityLifecycleCutover } = await import("./services/entity_lifecycle_storage.js");
|
|
8485
|
+
const { getDb } = await import("./repositories/db/connection.js");
|
|
8486
|
+
if ((await hasRecordedEntityLifecycleCutover(await getDb())) &&
|
|
8487
|
+
(await isEntityDeleted(entity_id, userId)))
|
|
8488
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
8489
|
+
}
|
|
8490
|
+
catch (err) {
|
|
8491
|
+
logError("Error:get_entity_snapshot:authority", req, err);
|
|
8492
|
+
return sendError(res, 500, "DB_QUERY_FAILED", "Entity lifecycle authority acquisition failed");
|
|
8493
|
+
}
|
|
8494
|
+
// No cutoffs: return the existing owned materialized factual snapshot.
|
|
8349
8495
|
const { data, error } = await db
|
|
8350
8496
|
.from("entity_snapshots")
|
|
8351
8497
|
.select("*")
|
|
@@ -8360,8 +8506,23 @@ app.post("/get_entity_snapshot", async (req, res) => {
|
|
|
8360
8506
|
return sendError(res, 500, "DB_QUERY_FAILED", error.message);
|
|
8361
8507
|
}
|
|
8362
8508
|
logDebug("Success:get_entity_snapshot", req, { entity_id });
|
|
8363
|
-
return res.json(data);
|
|
8509
|
+
return res.json(await withSnapshotDataOrigins(data, userId));
|
|
8364
8510
|
});
|
|
8511
|
+
/**
|
|
8512
|
+
* #2242: attach `data_origins` (observation_id → caller-claimed origin) for the
|
|
8513
|
+
* observations a snapshot's field provenance cites. The key is omitted when no
|
|
8514
|
+
* cited observation carries an origin, so pre-#2242 responses are unchanged.
|
|
8515
|
+
* Mirrors MCP `retrieve_entity_snapshot`.
|
|
8516
|
+
*/
|
|
8517
|
+
async function withSnapshotDataOrigins(snapshot, userId) {
|
|
8518
|
+
if (!snapshot || typeof snapshot !== "object")
|
|
8519
|
+
return snapshot;
|
|
8520
|
+
const { collectSnapshotDataOrigins } = await import("./services/provenance_data_origin.js");
|
|
8521
|
+
const dataOrigins = await collectSnapshotDataOrigins(snapshot.provenance, userId);
|
|
8522
|
+
if (Object.keys(dataOrigins).length === 0)
|
|
8523
|
+
return snapshot;
|
|
8524
|
+
return { ...snapshot, data_origins: dataOrigins };
|
|
8525
|
+
}
|
|
8365
8526
|
// List observations for entity
|
|
8366
8527
|
app.post("/list_observations", async (req, res) => {
|
|
8367
8528
|
const parsed = ListObservationsRequestSchema.safeParse(req.body);
|
|
@@ -8505,6 +8666,14 @@ app.post("/create_relationship", async (req, res) => {
|
|
|
8505
8666
|
"same store call referenced by index, then link it.",
|
|
8506
8667
|
});
|
|
8507
8668
|
}
|
|
8669
|
+
// Relationship-type + endpoint-type capability refusal, raised inside
|
|
8670
|
+
// relationshipsService.createRelationship (the shared edge-write gate).
|
|
8671
|
+
if (error instanceof AgentCapabilityError) {
|
|
8672
|
+
logWarn("AgentCapabilityRejection:create_relationship", req, error.toErrorEnvelope());
|
|
8673
|
+
return res
|
|
8674
|
+
.status(403)
|
|
8675
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
8676
|
+
}
|
|
8508
8677
|
logError("RelationshipCreationError:create_relationship", req, error);
|
|
8509
8678
|
return sendError(res, 500, "DB_QUERY_FAILED", error instanceof Error ? error.message : "Failed to create relationship");
|
|
8510
8679
|
}
|
|
@@ -8540,11 +8709,19 @@ app.post("/create_relationships", async (req, res) => {
|
|
|
8540
8709
|
});
|
|
8541
8710
|
}
|
|
8542
8711
|
catch (error) {
|
|
8543
|
-
|
|
8712
|
+
const item = {
|
|
8544
8713
|
index,
|
|
8545
8714
|
relationship,
|
|
8546
8715
|
error: error instanceof Error ? error.message : String(error),
|
|
8547
|
-
}
|
|
8716
|
+
};
|
|
8717
|
+
// Batch semantics stay HTTP 200 so successful siblings are retained,
|
|
8718
|
+
// but a capability denial must keep the same machine-actionable repair
|
|
8719
|
+
// contract as the single-item endpoint. Agents rely on code/op/hint;
|
|
8720
|
+
// reducing this to error.message turns a recoverable refusal opaque.
|
|
8721
|
+
if (error instanceof AgentCapabilityError) {
|
|
8722
|
+
Object.assign(item, error.toErrorEnvelope());
|
|
8723
|
+
}
|
|
8724
|
+
errors.push(item);
|
|
8548
8725
|
}
|
|
8549
8726
|
}
|
|
8550
8727
|
logDebug("Success:create_relationships", req, {
|
|
@@ -9350,6 +9527,15 @@ app.post("/delete_relationship", async (req, res) => {
|
|
|
9350
9527
|
"one of the returned types.",
|
|
9351
9528
|
});
|
|
9352
9529
|
}
|
|
9530
|
+
// Tombstoning a live edge changes the same relationship state that the
|
|
9531
|
+
// create/restore capability governs. Enforce the admitted grant after the
|
|
9532
|
+
// discovery guard (so a missing edge stays not-found) and before writing
|
|
9533
|
+
// the deletion observation.
|
|
9534
|
+
const { enforceCurrentAgentRelationshipWrites } = await import("./services/relationship_write_capability.js");
|
|
9535
|
+
await enforceCurrentAgentRelationshipWrites({
|
|
9536
|
+
userId,
|
|
9537
|
+
relationships: [{ relationship_type, source_entity_id, target_entity_id }],
|
|
9538
|
+
});
|
|
9353
9539
|
const { softDeleteRelationship } = await import("./services/deletion.js");
|
|
9354
9540
|
// Construct relationship key
|
|
9355
9541
|
const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
|
|
@@ -9910,6 +10096,27 @@ app.post("/register_schema", async (req, res) => {
|
|
|
9910
10096
|
}
|
|
9911
10097
|
});
|
|
9912
10098
|
// POST /correct - Create correction observation to override field value
|
|
10099
|
+
app.post("/corrections/transaction", async (req, res) => {
|
|
10100
|
+
try {
|
|
10101
|
+
const userId = await getAuthenticatedUserId(req, req.body?.user_id);
|
|
10102
|
+
const { applyCorrectionTransaction, CorrectionTransactionError } = await import("./services/correction_transaction.js");
|
|
10103
|
+
try {
|
|
10104
|
+
return res.json(await applyCorrectionTransaction({ ...req.body, user_id: userId }));
|
|
10105
|
+
}
|
|
10106
|
+
catch (error) {
|
|
10107
|
+
if (error instanceof CorrectionTransactionError) {
|
|
10108
|
+
const status = error.code === "VALIDATION_ERROR" ? 400 : error.code === "RESOURCE_NOT_FOUND" ? 404 : 409;
|
|
10109
|
+
return sendError(res, status, error.code, error.message, {
|
|
10110
|
+
hint: "Read current entity snapshots and resolve conflicts; retry an uncertain request with exactly the original payload and key.",
|
|
10111
|
+
});
|
|
10112
|
+
}
|
|
10113
|
+
throw error;
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
catch (error) {
|
|
10117
|
+
return handleApiError(req, res, error, "Correction transaction failed and was rolled back.");
|
|
10118
|
+
}
|
|
10119
|
+
});
|
|
9913
10120
|
app.post("/correct", async (req, res) => {
|
|
9914
10121
|
const parsed = CorrectEntityRequestSchema.safeParse(req.body);
|
|
9915
10122
|
if (!parsed.success) {
|
|
@@ -9920,7 +10127,7 @@ app.post("/correct", async (req, res) => {
|
|
|
9920
10127
|
}
|
|
9921
10128
|
try {
|
|
9922
10129
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
9923
|
-
const { entity_id, entity_type, field, value, idempotency_key } = parsed.data;
|
|
10130
|
+
const { entity_id, entity_type, field, value, idempotency_key, expected_version, overwrite } = parsed.data;
|
|
9924
10131
|
const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
|
|
9925
10132
|
if (correctCtx) {
|
|
9926
10133
|
enforceAgentCapability("correct", [entity_type], correctCtx);
|
|
@@ -9937,9 +10144,9 @@ app.post("/correct", async (req, res) => {
|
|
|
9937
10144
|
// propagates (it is NOT coerced to "declared field" — product principle
|
|
9938
10145
|
// 10.2); a missing schema throws CorrectionSchemaNotFoundError, surfaced as
|
|
9939
10146
|
// ERR_NO_SCHEMA_FOR_ENTITY_TYPE. Both reach the catch below.
|
|
9940
|
-
const { createCorrection, resolveCorrectionSchema, buildCorrectionResponse } = await import("./services/correction.js");
|
|
10147
|
+
const { createCorrection, createCorrectionWithVersionPrecondition, resolveCorrectionSchema, buildCorrectionResponse, } = await import("./services/correction.js");
|
|
9941
10148
|
const { schemaVersion: correctionSchemaVersion, isUnknownField } = await resolveCorrectionSchema(entity_type, field, userId);
|
|
9942
|
-
const
|
|
10149
|
+
const correctionParams = {
|
|
9943
10150
|
entity_id,
|
|
9944
10151
|
entity_type,
|
|
9945
10152
|
field,
|
|
@@ -9947,7 +10154,14 @@ app.post("/correct", async (req, res) => {
|
|
|
9947
10154
|
schema_version: correctionSchemaVersion,
|
|
9948
10155
|
user_id: userId,
|
|
9949
10156
|
idempotency_key,
|
|
9950
|
-
}
|
|
10157
|
+
};
|
|
10158
|
+
const result = typeof expected_version === "string"
|
|
10159
|
+
? await createCorrectionWithVersionPrecondition({
|
|
10160
|
+
...correctionParams,
|
|
10161
|
+
expected_version,
|
|
10162
|
+
overwrite,
|
|
10163
|
+
})
|
|
10164
|
+
: await createCorrection(correctionParams);
|
|
9951
10165
|
if (isUnknownField) {
|
|
9952
10166
|
try {
|
|
9953
10167
|
const { storeFragment } = await import("./services/raw_fragments.js");
|
|
@@ -9978,8 +10192,10 @@ app.post("/correct", async (req, res) => {
|
|
|
9978
10192
|
entity_id,
|
|
9979
10193
|
entity_type,
|
|
9980
10194
|
field,
|
|
9981
|
-
value,
|
|
10195
|
+
value: result.value,
|
|
9982
10196
|
isUnknownField,
|
|
10197
|
+
replayed: result.replayed,
|
|
10198
|
+
entity_version: result.entity_version,
|
|
9983
10199
|
}),
|
|
9984
10200
|
});
|
|
9985
10201
|
}
|
|
@@ -9988,13 +10204,43 @@ app.post("/correct", async (req, res) => {
|
|
|
9988
10204
|
// (ERR_NO_SCHEMA_FOR_ENTITY_TYPE), not an internal failure. A schema-registry
|
|
9989
10205
|
// IO failure is NOT swallowed here — it falls through to the generic 500
|
|
9990
10206
|
// handler below instead of being coerced into a "declared field" outcome.
|
|
9991
|
-
const { CorrectionSchemaNotFoundError } = await import("./services/correction.js");
|
|
10207
|
+
const { CorrectionSchemaNotFoundError, CorrectionEntityTypeMismatchError, CorrectionIdempotencyMismatchError, FieldVersionConflictError, } = await import("./services/correction.js");
|
|
9992
10208
|
if (error instanceof CorrectionSchemaNotFoundError) {
|
|
9993
10209
|
logWarn("ValidationError:correct", req, { code: error.code });
|
|
9994
10210
|
return res
|
|
9995
10211
|
.status(400)
|
|
9996
10212
|
.json(buildErrorEnvelope(error.code, error.message, { entity_type: error.entityType }));
|
|
9997
10213
|
}
|
|
10214
|
+
if (error instanceof CorrectionEntityTypeMismatchError) {
|
|
10215
|
+
logWarn("ValidationError:correct", req, { code: error.code });
|
|
10216
|
+
return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
|
|
10217
|
+
entity_id: error.entityId,
|
|
10218
|
+
supplied_entity_type: error.suppliedEntityType,
|
|
10219
|
+
stored_entity_type: error.storedEntityType,
|
|
10220
|
+
}));
|
|
10221
|
+
}
|
|
10222
|
+
if (error instanceof CorrectionIdempotencyMismatchError) {
|
|
10223
|
+
return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
|
|
10224
|
+
idempotency_key: error.idempotencyKey,
|
|
10225
|
+
hint: "Use a new idempotency_key for a distinct correction payload.",
|
|
10226
|
+
}));
|
|
10227
|
+
}
|
|
10228
|
+
// Waxwing ADR: stale expected_version on /correct. Nothing was written —
|
|
10229
|
+
// the precondition check runs before createCorrection. Flat standard
|
|
10230
|
+
// envelope (error_code/message/details), matching the OpenAPI contract
|
|
10231
|
+
// and the sibling ERR_ARRAY_ITEM_CONFLICT 409 on /patch_array_item — NOT
|
|
10232
|
+
// wrapped under an `error` key (that shape is reserved for envelopes
|
|
10233
|
+
// like entity_owner_conflict that predate the standard envelope).
|
|
10234
|
+
if (error instanceof FieldVersionConflictError) {
|
|
10235
|
+
logWarn("VersionConflict:correct", req, { code: error.code, field: error.field });
|
|
10236
|
+
return res.status(409).json(buildErrorEnvelope(error.code, error.message, {
|
|
10237
|
+
entity_id: error.entityId,
|
|
10238
|
+
field: error.field,
|
|
10239
|
+
stored_version: error.storedVersion,
|
|
10240
|
+
expected_version: error.expectedVersion,
|
|
10241
|
+
hint: "Re-read the entity snapshot and retry with its current entity_version as expected_version.",
|
|
10242
|
+
}));
|
|
10243
|
+
}
|
|
9998
10244
|
// Instance store-policy denial (#1975) — same envelope as /store, so a
|
|
9999
10245
|
// caller handling one handles both.
|
|
10000
10246
|
const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
|
|
@@ -10008,6 +10254,131 @@ app.post("/correct", async (req, res) => {
|
|
|
10008
10254
|
return handleApiError(req, res, error, "Failed to create correction", "DB_QUERY_FAILED", "APIError:correct");
|
|
10009
10255
|
}
|
|
10010
10256
|
});
|
|
10257
|
+
// POST /patch_array_item - Atomically patch one item of a structured array
|
|
10258
|
+
// field by key (Waxwing ADR, ent_4b41bb83a4faf4428a73bfc8). Sibling of
|
|
10259
|
+
// /correct; see openapi.yaml for the full contract.
|
|
10260
|
+
app.post("/patch_array_item", writeRateLimit, async (req, res) => {
|
|
10261
|
+
const parsed = PatchArrayItemRequestSchema.safeParse(req.body);
|
|
10262
|
+
if (!parsed.success) {
|
|
10263
|
+
logWarn("ValidationError:patch_array_item", req, { issues: parsed.error.issues });
|
|
10264
|
+
return sendValidationError(res, parsed.error.issues);
|
|
10265
|
+
}
|
|
10266
|
+
try {
|
|
10267
|
+
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
10268
|
+
const { entity_id, entity_type, field, key_field, key_value, item, expected_item_version, expected_item_absent, } = parsed.data;
|
|
10269
|
+
// Resolve the authoritative entity type under the authenticated user
|
|
10270
|
+
// before capability/protected-type checks. The request's entity_type is a
|
|
10271
|
+
// consistency assertion, never an authorization input.
|
|
10272
|
+
const { loadArrayItemPatchTarget } = await import("./services/array_item_patch.js");
|
|
10273
|
+
const target = await loadArrayItemPatchTarget({
|
|
10274
|
+
entityId: entity_id,
|
|
10275
|
+
userId,
|
|
10276
|
+
suppliedEntityType: entity_type,
|
|
10277
|
+
});
|
|
10278
|
+
const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
|
|
10279
|
+
if (correctCtx) {
|
|
10280
|
+
enforceAgentCapability("correct", [target.entity_type], correctCtx);
|
|
10281
|
+
}
|
|
10282
|
+
assertCanWriteProtectedBatch({
|
|
10283
|
+
entity_types: [target.entity_type],
|
|
10284
|
+
op: "correct",
|
|
10285
|
+
identity: getCurrentAgentIdentity(),
|
|
10286
|
+
admission: getCurrentAAuthAdmission(),
|
|
10287
|
+
});
|
|
10288
|
+
const { patchArrayItem } = await import("./services/array_item_patch.js");
|
|
10289
|
+
const result = await patchArrayItem({
|
|
10290
|
+
entity_id,
|
|
10291
|
+
entity_type,
|
|
10292
|
+
user_id: userId,
|
|
10293
|
+
field,
|
|
10294
|
+
key_field,
|
|
10295
|
+
key_value,
|
|
10296
|
+
item,
|
|
10297
|
+
expected_item_version,
|
|
10298
|
+
expected_item_absent,
|
|
10299
|
+
idempotency_key: parsed.data.idempotency_key,
|
|
10300
|
+
});
|
|
10301
|
+
if (result.status === "conflict") {
|
|
10302
|
+
logWarn("VersionConflict:patch_array_item", req, { entity_id, field, key_field });
|
|
10303
|
+
return res.status(409).json(buildErrorEnvelope("ERR_ARRAY_ITEM_CONFLICT", `Stale expected_item_version for ${entity_type}.${field}[${key_field}=${JSON.stringify(key_value)}]`, {
|
|
10304
|
+
entity_id,
|
|
10305
|
+
entity_type,
|
|
10306
|
+
field,
|
|
10307
|
+
key_field,
|
|
10308
|
+
key_value,
|
|
10309
|
+
current_item: result.conflict?.current_item ?? null,
|
|
10310
|
+
current_item_version: result.conflict?.current_item_version ?? null,
|
|
10311
|
+
expected_item_version: result.conflict?.expected_item_version ?? null,
|
|
10312
|
+
expected_item_absent: expected_item_absent ?? false,
|
|
10313
|
+
hint: "Re-apply the intended change to current_item and retry with current_item_version as expected_item_version.",
|
|
10314
|
+
}));
|
|
10315
|
+
}
|
|
10316
|
+
return res.json({
|
|
10317
|
+
success: true,
|
|
10318
|
+
entity_id: result.entity_id,
|
|
10319
|
+
entity_type: result.entity_type,
|
|
10320
|
+
field: result.field,
|
|
10321
|
+
key_field: result.key_field,
|
|
10322
|
+
key_value: result.key_value,
|
|
10323
|
+
observation_id: result.observation_id,
|
|
10324
|
+
item: result.item,
|
|
10325
|
+
item_version: result.item_version,
|
|
10326
|
+
array_length: result.array_length,
|
|
10327
|
+
snapshot: result.snapshot,
|
|
10328
|
+
replayed: result.replayed ?? false,
|
|
10329
|
+
});
|
|
10330
|
+
}
|
|
10331
|
+
catch (error) {
|
|
10332
|
+
if (error instanceof Error && error.message.startsWith("Entity not found")) {
|
|
10333
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", error.message);
|
|
10334
|
+
}
|
|
10335
|
+
const { CorrectionIdempotencyMismatchError } = await import("./services/correction.js");
|
|
10336
|
+
if (error instanceof CorrectionIdempotencyMismatchError) {
|
|
10337
|
+
return res.status(400).json(buildErrorEnvelope(error.code, error.message, {
|
|
10338
|
+
idempotency_key: error.idempotencyKey,
|
|
10339
|
+
hint: "Use a new idempotency_key for a distinct patch payload.",
|
|
10340
|
+
}));
|
|
10341
|
+
}
|
|
10342
|
+
if (error instanceof EntityOwnerConflictError) {
|
|
10343
|
+
return res.status(409).json({ error: error.toErrorEnvelope() });
|
|
10344
|
+
}
|
|
10345
|
+
{
|
|
10346
|
+
const { ArrayItemKeyFieldMismatchError, ArrayItemPolicyRequiredError, ArrayItemEntityTypeMismatchError, } = await import("./services/array_item_patch.js");
|
|
10347
|
+
if (error instanceof ArrayItemKeyFieldMismatchError ||
|
|
10348
|
+
error instanceof ArrayItemPolicyRequiredError ||
|
|
10349
|
+
error instanceof ArrayItemEntityTypeMismatchError) {
|
|
10350
|
+
logWarn("KeyFieldMismatch:patch_array_item", req, {
|
|
10351
|
+
code: error.code,
|
|
10352
|
+
});
|
|
10353
|
+
return res
|
|
10354
|
+
.status(error.statusCode)
|
|
10355
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
// Instance store-policy denial (#1975) — patchArrayItem() writes through
|
|
10359
|
+
// createCorrection exactly like /correct, so it can be refused by the
|
|
10360
|
+
// same instance-level write policy. Same envelope shape as /correct and
|
|
10361
|
+
// /store, so a caller handling one handles all three.
|
|
10362
|
+
const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
|
|
10363
|
+
if (error instanceof StorePolicyDeniedError) {
|
|
10364
|
+
logWarn("StorePolicyDenied:patch_array_item", req, {
|
|
10365
|
+
denied_count: error.denied.length,
|
|
10366
|
+
reason_codes: [...new Set(error.denied.map((d) => d.reason_code))].sort(),
|
|
10367
|
+
});
|
|
10368
|
+
return res.status(400).json({ error: error.toErrorEnvelope() });
|
|
10369
|
+
}
|
|
10370
|
+
if (error instanceof StorePolicyUnavailableError) {
|
|
10371
|
+
logError("StorePolicyUnavailable:patch_array_item", req, error, {
|
|
10372
|
+
code: error.code,
|
|
10373
|
+
cause: error.cause_message,
|
|
10374
|
+
});
|
|
10375
|
+
return res
|
|
10376
|
+
.status(503)
|
|
10377
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
10378
|
+
}
|
|
10379
|
+
return handleApiError(req, res, error, "Failed to patch array item", "DB_QUERY_FAILED", "APIError:patch_array_item");
|
|
10380
|
+
}
|
|
10381
|
+
});
|
|
10011
10382
|
// POST /get_authenticated_user - Get authenticated user ID
|
|
10012
10383
|
// FU-2026-05-003: GET /conversations/:conversation_id/turn-index — Inspector
|
|
10013
10384
|
// per-turn anchor sections and turn timeline sidebar.
|
|
@@ -10823,14 +11194,12 @@ function assertRouteTableIsReachable() {
|
|
|
10823
11194
|
* loopback while the socket was actually open on all interfaces).
|
|
10824
11195
|
*/
|
|
10825
11196
|
export function resolveHttpBindHost(env = process.env) {
|
|
10826
|
-
|
|
10827
|
-
return raw.length > 0 ? raw : "127.0.0.1";
|
|
10828
|
-
}
|
|
10829
|
-
/** True when the given bind host resolves to a loopback-only address. */
|
|
10830
|
-
export function isLoopbackHost(host) {
|
|
10831
|
-
const normalized = host.trim().toLowerCase();
|
|
10832
|
-
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
11197
|
+
return resolveBindHostFromEnv("NEOTOMA_HTTP_HOST", env);
|
|
10833
11198
|
}
|
|
11199
|
+
// Re-exported so existing importers of `isLoopbackHost` from this module keep
|
|
11200
|
+
// working; the implementation lives in `./shared/bind_host.ts`, which the
|
|
11201
|
+
// standalone MCP WebSocket bridge also uses.
|
|
11202
|
+
export { isLoopbackHost };
|
|
10834
11203
|
/**
|
|
10835
11204
|
* Try to bind on a port/host; resolves with server and port, or rejects on
|
|
10836
11205
|
* error (e.g. EADDRINUSE).
|