neotoma 0.24.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 +80 -48
- package/dist/actions.d.ts +116 -27
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +611 -203
- 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 +1011 -221
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/issues.d.ts +6 -0
- package/dist/cli/issues.d.ts.map +1 -1
- package/dist/cli/issues.js +47 -6
- package/dist/cli/issues.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/cli/skills_mirror.d.ts +29 -6
- package/dist/cli/skills_mirror.d.ts.map +1 -1
- package/dist/cli/skills_mirror.js +70 -13
- package/dist/cli/skills_mirror.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 +33 -5
- package/dist/docs/developer/cli_reference.md +219 -170
- package/dist/docs/developer/fleet_onboarding.md +7 -6
- package/dist/docs/developer/mcp/instructions.md +34 -23
- 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 +23 -10
- package/dist/docs/skills/skill_strategy.md +10 -6
- package/dist/docs/specs/MCP_SPEC.md +26 -9
- 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/issues.md +45 -5
- package/dist/docs/subsystems/reducer.md +208 -52
- package/dist/docs/subsystems/schema_registry.md +39 -8
- 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 +98 -20
- package/dist/docs/testing/testing_standard.md +33 -0
- package/dist/docs/use_cases/README.md +13 -13
- package/dist/docs/use_cases/agent_auth.md +1 -1
- package/dist/docs/use_cases/cases.md +1 -2
- package/dist/docs/use_cases/compliance.md +0 -1
- package/dist/docs/use_cases/contracts.md +1 -2
- package/dist/docs/use_cases/crypto_engineering.md +0 -1
- package/dist/docs/use_cases/customer_ops.md +1 -2
- package/dist/docs/use_cases/diligence.md +1 -2
- package/dist/docs/use_cases/government.md +1 -2
- package/dist/docs/use_cases/healthcare.md +1 -2
- package/dist/docs/use_cases/logistics.md +1 -2
- package/dist/docs/use_cases/portfolio.md +1 -2
- package/dist/docs/use_cases/procurement.md +1 -2
- package/dist/docs/use_cases/trading.md +1 -2
- 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 +24 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +658 -144
- 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/issues/config.d.ts.map +1 -1
- package/dist/services/issues/config.js +6 -0
- package/dist/services/issues/config.js.map +1 -1
- package/dist/services/issues/github_client.d.ts +7 -0
- package/dist/services/issues/github_client.d.ts.map +1 -1
- package/dist/services/issues/github_client.js +15 -3
- package/dist/services/issues/github_client.js.map +1 -1
- package/dist/services/issues/inspector_bulk.d.ts +2 -1
- package/dist/services/issues/inspector_bulk.d.ts.map +1 -1
- package/dist/services/issues/inspector_bulk.js +23 -5
- package/dist/services/issues/inspector_bulk.js.map +1 -1
- package/dist/services/issues/issue_operations.d.ts +10 -1
- package/dist/services/issues/issue_operations.d.ts.map +1 -1
- package/dist/services/issues/issue_operations.js +53 -21
- package/dist/services/issues/issue_operations.js.map +1 -1
- package/dist/services/issues/repo_allowlist.d.ts +55 -0
- package/dist/services/issues/repo_allowlist.d.ts.map +1 -0
- package/dist/services/issues/repo_allowlist.js +101 -0
- package/dist/services/issues/repo_allowlist.js.map +1 -0
- package/dist/services/issues/sync_issues_from_github.d.ts +83 -4
- package/dist/services/issues/sync_issues_from_github.d.ts.map +1 -1
- package/dist/services/issues/sync_issues_from_github.js +203 -28
- package/dist/services/issues/sync_issues_from_github.js.map +1 -1
- package/dist/services/issues/types.d.ts +21 -2
- package/dist/services/issues/types.d.ts.map +1 -1
- package/dist/services/issues/types.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 -18
- 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 +81 -6
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +235 -66
- 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 +709 -232
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +274 -11
- 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 +1073 -40
- package/dist/shared/openapi_types.d.ts.map +1 -1
- package/dist/shared/repo_slug.d.ts +27 -0
- package/dist/shared/repo_slug.d.ts.map +1 -0
- package/dist/shared/repo_slug.js +45 -0
- package/dist/shared/repo_slug.js.map +1 -0
- package/dist/shared/skill_deprecation.d.ts +26 -0
- package/dist/shared/skill_deprecation.d.ts.map +1 -0
- package/dist/shared/skill_deprecation.js +41 -0
- package/dist/shared/skill_deprecation.js.map +1 -0
- package/dist/tool_definitions.d.ts +1 -1
- package/dist/tool_definitions.d.ts.map +1 -1
- package/dist/tool_definitions.js +57 -12
- 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 +34 -23
- package/openapi.yaml +1424 -34
- package/package.json +10 -2
- package/skills/end/SKILL.md +2 -2
- package/skills/ensure-neotoma/SKILL.md +4 -2
- package/skills/query-memory/SKILL.md +13 -2
- package/skills/recover-sqlite-database/SKILL.md +1 -1
- package/skills/store-data/SKILL.md +12 -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, {
|
|
@@ -9267,9 +9444,13 @@ const handleIssuesSyncHttp = async (req, res) => {
|
|
|
9267
9444
|
since: parsed.data.since,
|
|
9268
9445
|
state: parsed.data.state,
|
|
9269
9446
|
labels: parsed.data.labels,
|
|
9447
|
+
repo: parsed.data.repo,
|
|
9270
9448
|
push: parsed.data.push,
|
|
9449
|
+
commit: parsed.data.commit,
|
|
9271
9450
|
});
|
|
9272
9451
|
logDebug("Success:issues_sync", req, {
|
|
9452
|
+
repo: result.repo,
|
|
9453
|
+
dry_run: result.dry_run,
|
|
9273
9454
|
issues_synced: result.issues_synced,
|
|
9274
9455
|
messages_synced: result.messages_synced,
|
|
9275
9456
|
issues_pushed: result.issues_pushed,
|
|
@@ -9346,6 +9527,15 @@ app.post("/delete_relationship", async (req, res) => {
|
|
|
9346
9527
|
"one of the returned types.",
|
|
9347
9528
|
});
|
|
9348
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
|
+
});
|
|
9349
9539
|
const { softDeleteRelationship } = await import("./services/deletion.js");
|
|
9350
9540
|
// Construct relationship key
|
|
9351
9541
|
const relationshipKey = `${relationship_type}:${source_entity_id}:${target_entity_id}`;
|
|
@@ -9554,20 +9744,27 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
9554
9744
|
}
|
|
9555
9745
|
try {
|
|
9556
9746
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
9557
|
-
const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version,
|
|
9747
|
+
const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version,
|
|
9748
|
+
// #2374: no local `= false` default. `user_specific` stays
|
|
9749
|
+
// `undefined` when the caller omitted it, so `updateSchemaIncremental`
|
|
9750
|
+
// can defer to whichever scope its own read resolved instead of being
|
|
9751
|
+
// told "global" by a default this route manufactured.
|
|
9752
|
+
user_specific, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
9558
9753
|
const { schemaRegistry, loadCodeDefinedSchemaEntry } = await import("./services/schema_registry.js");
|
|
9559
|
-
// #2454 REST parity with MCP
|
|
9560
|
-
//
|
|
9561
|
-
//
|
|
9562
|
-
//
|
|
9563
|
-
//
|
|
9564
|
-
|
|
9754
|
+
// #2454 / #2374 REST parity with the MCP handler: the existence guard
|
|
9755
|
+
// resolves the SAME row `updateSchemaIncremental` will read —
|
|
9756
|
+
// user-scope-preferred when `user_specific` is omitted or true,
|
|
9757
|
+
// global-only when the caller explicitly passed `user_specific: false`.
|
|
9758
|
+
// When the global-only lookup misses but a schema is active in the
|
|
9759
|
+
// caller's user scope, return ERR_SCHEMA_SCOPE_MISMATCH — never
|
|
9760
|
+
// ERR_NO_SCHEMA_FOR_ENTITY_TYPE with a register_schema hint.
|
|
9761
|
+
const registeredSchema = await schemaRegistry.loadActiveSchema(entity_type, user_specific === false ? undefined : userId);
|
|
9565
9762
|
if (!registeredSchema) {
|
|
9566
9763
|
const codeDefinedSchema = await loadCodeDefinedSchemaEntry(entity_type);
|
|
9567
9764
|
if (!codeDefinedSchema) {
|
|
9568
9765
|
const anyScopeSchema = await schemaRegistry.loadActiveSchema(entity_type, userId);
|
|
9569
9766
|
if (anyScopeSchema) {
|
|
9570
|
-
const guardScope = user_specific ? "
|
|
9767
|
+
const guardScope = user_specific === false ? "global" : "user";
|
|
9571
9768
|
const foundScope = anyScopeSchema.scope ?? "global";
|
|
9572
9769
|
return res.status(200).json({
|
|
9573
9770
|
error: {
|
|
@@ -9577,13 +9774,13 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
9577
9774
|
"and found none there.",
|
|
9578
9775
|
hint: `The existing SchemaDefinition for "${entity_type}" lives in ` +
|
|
9579
9776
|
`"${foundScope}" scope. This call did not check that scope because ` +
|
|
9580
|
-
`user_specific was ${user_specific ? "
|
|
9777
|
+
`user_specific was ${user_specific === false ? "false" : "true"}. ` +
|
|
9581
9778
|
"This entity_type already has an active schema — creating a second one for it " +
|
|
9582
9779
|
"risks leaving two active schema_registry rows for the same entity_type (see " +
|
|
9583
9780
|
"#2374/#2378), after which the next incremental update can merge onto stale " +
|
|
9584
|
-
"state and drop fields. Instead, retry update_schema_incremental
|
|
9585
|
-
|
|
9586
|
-
|
|
9781
|
+
"state and drop fields. Instead, retry update_schema_incremental without " +
|
|
9782
|
+
"user_specific (the call then writes to whichever scope your schema resolves " +
|
|
9783
|
+
`to), or with user_specific: ${foundScope === "user"}.`,
|
|
9587
9784
|
details: {
|
|
9588
9785
|
entity_type,
|
|
9589
9786
|
guard_scope: guardScope,
|
|
@@ -9623,9 +9820,20 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
9623
9820
|
canonical_name_fields,
|
|
9624
9821
|
schema_version,
|
|
9625
9822
|
user_specific,
|
|
9823
|
+
// #2374: pass userId unconditionally — same reasoning as the MCP
|
|
9824
|
+
// handler. The service resolves scope from its own read, not from
|
|
9825
|
+
// whether this route pre-gated userId on user_specific.
|
|
9626
9826
|
user_id: userId,
|
|
9627
9827
|
activate,
|
|
9628
9828
|
migrate_existing,
|
|
9829
|
+
// migrate_existing must scope to the authenticated caller so
|
|
9830
|
+
// raw_fragments stored by this user are promoted, matching the MCP
|
|
9831
|
+
// handler's migrate_user_id wiring (#2379 note: REST previously
|
|
9832
|
+
// relied solely on the service's `migrate_user_id ?? user_id`
|
|
9833
|
+
// fallback, which happened to cover this route, but stating it
|
|
9834
|
+
// explicitly here keeps the two surfaces in parity rather than
|
|
9835
|
+
// coincidentally agreeing).
|
|
9836
|
+
migrate_user_id: migrate_existing ? userId : undefined,
|
|
9629
9837
|
force,
|
|
9630
9838
|
});
|
|
9631
9839
|
}
|
|
@@ -9636,21 +9844,38 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
9636
9844
|
}
|
|
9637
9845
|
throw err;
|
|
9638
9846
|
}
|
|
9847
|
+
// #2379: derive migrated_existing from the ACTUAL result, never from
|
|
9848
|
+
// the `migrate_existing` request flag.
|
|
9849
|
+
const migrationResult = newSchema.migration_result;
|
|
9850
|
+
// #2374: report the scope actually written, read off the persisted row.
|
|
9851
|
+
const scopeWritten = newSchema.scope === "user" ? "user" : "global";
|
|
9639
9852
|
logDebug("Success:update_schema_incremental", req, {
|
|
9640
9853
|
entity_type,
|
|
9641
9854
|
fields_added: (fields_to_add || []).length,
|
|
9642
9855
|
fields_removed: (fields_to_remove || []).length,
|
|
9643
9856
|
migrate_existing,
|
|
9857
|
+
migrated_existing: (migrationResult?.migrated_count ?? 0) > 0,
|
|
9858
|
+
scope: scopeWritten,
|
|
9644
9859
|
});
|
|
9645
9860
|
return res.json({
|
|
9646
9861
|
success: true,
|
|
9647
9862
|
schema: newSchema,
|
|
9648
9863
|
schema_version: newSchema.schema_version,
|
|
9864
|
+
fields_added: (fields_to_add || []).map((f) => f.field_name),
|
|
9649
9865
|
fields_removed: fields_to_remove || [],
|
|
9650
9866
|
// Echo the resolved identity rule so CLI/HTTP callers can confirm the
|
|
9651
9867
|
// re-key without a second describe_entity_type round trip (#2020 ux).
|
|
9652
9868
|
canonical_name_fields: newSchema.schema_definition
|
|
9653
9869
|
.canonical_name_fields ?? null,
|
|
9870
|
+
activated: activate,
|
|
9871
|
+
// #2379: true only when the migration actually promoted something.
|
|
9872
|
+
migrated_existing: (migrationResult?.migrated_count ?? 0) > 0,
|
|
9873
|
+
// Present only when migrate_existing was requested this call.
|
|
9874
|
+
...(migrationResult ? { migration_result: migrationResult } : {}),
|
|
9875
|
+
// #2374: the scope this call actually wrote to, read off the
|
|
9876
|
+
// persisted row rather than re-derived from the request.
|
|
9877
|
+
scope: scopeWritten,
|
|
9878
|
+
...(scopeWritten === "user" ? { user_id: newSchema.user_id ?? null } : {}),
|
|
9654
9879
|
});
|
|
9655
9880
|
}
|
|
9656
9881
|
catch (error) {
|
|
@@ -9871,6 +10096,27 @@ app.post("/register_schema", async (req, res) => {
|
|
|
9871
10096
|
}
|
|
9872
10097
|
});
|
|
9873
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
|
+
});
|
|
9874
10120
|
app.post("/correct", async (req, res) => {
|
|
9875
10121
|
const parsed = CorrectEntityRequestSchema.safeParse(req.body);
|
|
9876
10122
|
if (!parsed.success) {
|
|
@@ -9881,7 +10127,7 @@ app.post("/correct", async (req, res) => {
|
|
|
9881
10127
|
}
|
|
9882
10128
|
try {
|
|
9883
10129
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
9884
|
-
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;
|
|
9885
10131
|
const correctCtx = contextFromAgentIdentity(getCurrentAgentIdentity());
|
|
9886
10132
|
if (correctCtx) {
|
|
9887
10133
|
enforceAgentCapability("correct", [entity_type], correctCtx);
|
|
@@ -9898,9 +10144,9 @@ app.post("/correct", async (req, res) => {
|
|
|
9898
10144
|
// propagates (it is NOT coerced to "declared field" — product principle
|
|
9899
10145
|
// 10.2); a missing schema throws CorrectionSchemaNotFoundError, surfaced as
|
|
9900
10146
|
// ERR_NO_SCHEMA_FOR_ENTITY_TYPE. Both reach the catch below.
|
|
9901
|
-
const { createCorrection, resolveCorrectionSchema, buildCorrectionResponse } = await import("./services/correction.js");
|
|
10147
|
+
const { createCorrection, createCorrectionWithVersionPrecondition, resolveCorrectionSchema, buildCorrectionResponse, } = await import("./services/correction.js");
|
|
9902
10148
|
const { schemaVersion: correctionSchemaVersion, isUnknownField } = await resolveCorrectionSchema(entity_type, field, userId);
|
|
9903
|
-
const
|
|
10149
|
+
const correctionParams = {
|
|
9904
10150
|
entity_id,
|
|
9905
10151
|
entity_type,
|
|
9906
10152
|
field,
|
|
@@ -9908,7 +10154,14 @@ app.post("/correct", async (req, res) => {
|
|
|
9908
10154
|
schema_version: correctionSchemaVersion,
|
|
9909
10155
|
user_id: userId,
|
|
9910
10156
|
idempotency_key,
|
|
9911
|
-
}
|
|
10157
|
+
};
|
|
10158
|
+
const result = typeof expected_version === "string"
|
|
10159
|
+
? await createCorrectionWithVersionPrecondition({
|
|
10160
|
+
...correctionParams,
|
|
10161
|
+
expected_version,
|
|
10162
|
+
overwrite,
|
|
10163
|
+
})
|
|
10164
|
+
: await createCorrection(correctionParams);
|
|
9912
10165
|
if (isUnknownField) {
|
|
9913
10166
|
try {
|
|
9914
10167
|
const { storeFragment } = await import("./services/raw_fragments.js");
|
|
@@ -9939,8 +10192,10 @@ app.post("/correct", async (req, res) => {
|
|
|
9939
10192
|
entity_id,
|
|
9940
10193
|
entity_type,
|
|
9941
10194
|
field,
|
|
9942
|
-
value,
|
|
10195
|
+
value: result.value,
|
|
9943
10196
|
isUnknownField,
|
|
10197
|
+
replayed: result.replayed,
|
|
10198
|
+
entity_version: result.entity_version,
|
|
9944
10199
|
}),
|
|
9945
10200
|
});
|
|
9946
10201
|
}
|
|
@@ -9949,13 +10204,43 @@ app.post("/correct", async (req, res) => {
|
|
|
9949
10204
|
// (ERR_NO_SCHEMA_FOR_ENTITY_TYPE), not an internal failure. A schema-registry
|
|
9950
10205
|
// IO failure is NOT swallowed here — it falls through to the generic 500
|
|
9951
10206
|
// handler below instead of being coerced into a "declared field" outcome.
|
|
9952
|
-
const { CorrectionSchemaNotFoundError } = await import("./services/correction.js");
|
|
10207
|
+
const { CorrectionSchemaNotFoundError, CorrectionEntityTypeMismatchError, CorrectionIdempotencyMismatchError, FieldVersionConflictError, } = await import("./services/correction.js");
|
|
9953
10208
|
if (error instanceof CorrectionSchemaNotFoundError) {
|
|
9954
10209
|
logWarn("ValidationError:correct", req, { code: error.code });
|
|
9955
10210
|
return res
|
|
9956
10211
|
.status(400)
|
|
9957
10212
|
.json(buildErrorEnvelope(error.code, error.message, { entity_type: error.entityType }));
|
|
9958
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
|
+
}
|
|
9959
10244
|
// Instance store-policy denial (#1975) — same envelope as /store, so a
|
|
9960
10245
|
// caller handling one handles both.
|
|
9961
10246
|
const { StorePolicyDeniedError } = await import("./services/instance_policy.js");
|
|
@@ -9969,6 +10254,131 @@ app.post("/correct", async (req, res) => {
|
|
|
9969
10254
|
return handleApiError(req, res, error, "Failed to create correction", "DB_QUERY_FAILED", "APIError:correct");
|
|
9970
10255
|
}
|
|
9971
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
|
+
});
|
|
9972
10382
|
// POST /get_authenticated_user - Get authenticated user ID
|
|
9973
10383
|
// FU-2026-05-003: GET /conversations/:conversation_id/turn-index — Inspector
|
|
9974
10384
|
// per-turn anchor sections and turn timeline sidebar.
|
|
@@ -10784,14 +11194,12 @@ function assertRouteTableIsReachable() {
|
|
|
10784
11194
|
* loopback while the socket was actually open on all interfaces).
|
|
10785
11195
|
*/
|
|
10786
11196
|
export function resolveHttpBindHost(env = process.env) {
|
|
10787
|
-
|
|
10788
|
-
return raw.length > 0 ? raw : "127.0.0.1";
|
|
10789
|
-
}
|
|
10790
|
-
/** True when the given bind host resolves to a loopback-only address. */
|
|
10791
|
-
export function isLoopbackHost(host) {
|
|
10792
|
-
const normalized = host.trim().toLowerCase();
|
|
10793
|
-
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
11197
|
+
return resolveBindHostFromEnv("NEOTOMA_HTTP_HOST", env);
|
|
10794
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 };
|
|
10795
11203
|
/**
|
|
10796
11204
|
* Try to bind on a port/host; resolves with server and port, or rejects on
|
|
10797
11205
|
* error (e.g. EADDRINUSE).
|