feat(data-plane): add provider contracts and ontology delivery
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
COPY catalog ./catalog
|
||||
COPY docs ./docs
|
||||
COPY examples ./examples
|
||||
COPY src ./src
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=18104
|
||||
|
||||
EXPOSE 18104
|
||||
|
||||
CMD ["node", "src/mcp-server.mjs"]
|
||||
@@ -4,6 +4,9 @@ Docs-first ontology service/module for NODE.DC.
|
||||
|
||||
Current status: `v0.4-pre` implementation slice.
|
||||
|
||||
The read-only dynamic MCP integration for AI Workspace is documented in
|
||||
[`../../docs/AI_WORKSPACE_ONTOLOGY_MCP.md`](../../docs/AI_WORKSPACE_ONTOLOGY_MCP.md).
|
||||
|
||||
This module owns:
|
||||
|
||||
- canonical entity catalog;
|
||||
@@ -62,6 +65,7 @@ src/adapters/hub-launcher-admin.mjs
|
||||
|
||||
```text
|
||||
npm run validate
|
||||
npm run smoke:mcp
|
||||
```
|
||||
|
||||
## Resolver Smoke
|
||||
@@ -159,8 +163,10 @@ Each package may contribute entities, relations, aliases, guardrails, evidence,
|
||||
|
||||
Current package:
|
||||
|
||||
- `integration` - provider-neutral external provider connection, capability, collection, read-model and red command-domain ontology used by all adapter services.
|
||||
- `seo` - NDC SEO mod domain ontology for site scans, project ontology instances, scope contracts, semantic analysis, market evidence, optimization planning, validation, changesets, and future app-owned SEO assistant actions.
|
||||
- `map` - provider-neutral NDC Module Studio map ontology for spatial subjects, layers, routes, zones, shared labels, visibility rules, selection, and replaceable renderer adapters.
|
||||
- `gelios` - provider-neutral Gelios fleet and telemetry ontology, bound to map.moving_object, map.zone and map.place_target without exposing provider credentials or renderer objects.
|
||||
|
||||
The package loader merges domain packages into the base catalog before validation. Domain packages extend core meanings; they do not own app data or execute mutations.
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
{ "alias": "app", "canonicalId": "hub.application" },
|
||||
{ "alias": "application tile", "canonicalId": "hub.application_card" },
|
||||
{ "alias": "app card", "canonicalId": "hub.application_card" },
|
||||
{ "alias": "n8n workflow id", "canonicalId": "engine.workflow_l2_runtime_id" },
|
||||
{ "alias": "runtime workflow id", "canonicalId": "engine.workflow_l2_runtime_id" },
|
||||
{ "alias": "n8n execution", "canonicalId": "engine.l2_execution" },
|
||||
{ "alias": "l2 execution", "canonicalId": "engine.l2_execution" },
|
||||
{ "alias": "Engine-side OPS", "canonicalId": "engine.tech_debt.ops_layer" },
|
||||
{ "alias": "Agent Monitor OPS", "canonicalId": "engine.tech_debt.ops_layer" },
|
||||
{ "alias": "Tender Agent Monitor UI", "canonicalId": "engine.tech_debt.tender_domain_ui" },
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"aliases": [
|
||||
{ "alias": "gelios", "canonicalId": "gelios.integration" },
|
||||
{ "alias": "gelius", "canonicalId": "gelios.integration" },
|
||||
{ "alias": "helius", "canonicalId": "gelios.integration" },
|
||||
{ "alias": "hel i os", "canonicalId": "gelios.integration" },
|
||||
{ "alias": "гелиос", "canonicalId": "gelios.integration" },
|
||||
{ "alias": "трайк robot2b", "canonicalId": "gelios.unit" },
|
||||
{ "alias": "юнит гелиос", "canonicalId": "gelios.unit" },
|
||||
{ "alias": "группа юнитов гелиос", "canonicalId": "gelios.unit_group" },
|
||||
{ "alias": "последняя телеметрия гелиос", "canonicalId": "gelios.telemetry_snapshot" },
|
||||
{ "alias": "сырая телеметрия гелиос", "canonicalId": "gelios.raw_telemetry_message" },
|
||||
{ "alias": "позиция трайка", "canonicalId": "gelios.position_fix" },
|
||||
{ "alias": "статус трайка", "canonicalId": "gelios.operational_status" },
|
||||
{ "alias": "датчик гелиос", "canonicalId": "gelios.sensor_definition" },
|
||||
{ "alias": "показание датчика", "canonicalId": "gelios.sensor_reading" },
|
||||
{ "alias": "калибровка датчика", "canonicalId": "gelios.sensor_conversion" },
|
||||
{ "alias": "топливный профиль", "canonicalId": "gelios.fuel_profile" },
|
||||
{ "alias": "геозона гелиос", "canonicalId": "gelios.geozone" },
|
||||
{ "alias": "геоточка гелиос", "canonicalId": "gelios.geopoint" },
|
||||
{ "alias": "шаблон команды гелиос", "canonicalId": "gelios.command_template" },
|
||||
{ "alias": "диспетчеризация команды гелиос", "canonicalId": "gelios.command_dispatch" },
|
||||
{ "alias": "аудит команды гелиос", "canonicalId": "gelios.command_audit" },
|
||||
{ "alias": "контур robot2b", "canonicalId": "gelios.access_scope" },
|
||||
{ "alias": "курсор загрузки гелиос", "canonicalId": "gelios.ingestion_cursor" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"entities": [
|
||||
{ "id": "gelios.integration", "name": "Gelios Integration", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "Gelios Gateway", "summary": "Server-side provider integration boundary for Gelios REST OAuth and legacy compatibility. It owns no UI, map renderer or workflow secret." },
|
||||
{ "id": "gelios.access_scope", "name": "Gelios Access Scope", "surface": "platform", "status": ["product-required", "source-evidenced"], "authority": "Robot2B owner + Gelios Gateway", "summary": "Approved collection boundary expressed as an allowlist or owner rule for units, groups and allowed capabilities; distinct from broad provider-account visibility." },
|
||||
{ "id": "gelios.unit", "name": "Gelios Unit", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Tracked operational object, including a Robot2B trike when its stable source unit ID is inside the approved scope." },
|
||||
{ "id": "gelios.unit_group", "name": "Gelios Unit Group", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider-managed grouping of units. It is evidence for navigation and access, not an automatic definition of the Robot2B business scope." },
|
||||
{ "id": "gelios.tracker_device", "name": "Tracker Device", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Hardware tracker associated with a unit, carrying device type/manufacturer and restricted hardware identity fields." },
|
||||
{ "id": "gelios.telemetry_snapshot", "name": "Telemetry Snapshot", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Normalized current telemetry state for one unit at observed and received time; derived from the Gelios pure last message contract." },
|
||||
{ "id": "gelios.raw_telemetry_message", "name": "Raw Telemetry Message", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider hardware message preserved only behind restricted raw-data policy; it is not the default Studio or analytics contract." },
|
||||
{ "id": "gelios.position_fix", "name": "Position Fix", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Time-qualified geographic position with latitude, longitude, height, course, speed, satellite and quality attributes for one unit." },
|
||||
{ "id": "gelios.telemetry_parameter", "name": "Telemetry Parameter", "surface": "telemetry", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Name/value parameter received with a message. Parameters are dynamic and require a namespaced registry and whitelist before product exposure." },
|
||||
{ "id": "gelios.operational_status", "name": "Operational Status", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Derived unit state such as online, offline, moving, parked, no-position or low-GPS; distinct from raw provider message fields." },
|
||||
{ "id": "gelios.sensor_definition", "name": "Sensor Definition", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Declared sensor on a unit with message parameter, type, unit of measure, visibility and optional fuel semantics." },
|
||||
{ "id": "gelios.sensor_reading", "name": "Sensor Reading", "surface": "telemetry", "status": ["source-evidenced", "product-required"], "authority": "Gelios Gateway", "summary": "Time-qualified normalized reading of a defined sensor, retaining both numeric value and human-readable representation when permitted." },
|
||||
{ "id": "gelios.sensor_conversion", "name": "Sensor Conversion", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Calibration/conversion rule or row for translating raw sensor values into operational measures." },
|
||||
{ "id": "gelios.fuel_profile", "name": "Fuel Profile", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Fuel and consumption configuration associated with a unit or fuel sensor; use only after sensor semantics are approved." },
|
||||
{ "id": "gelios.maintenance_plan", "name": "Maintenance Plan", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Planned maintenance configuration for a unit, separate from historical maintenance events." },
|
||||
{ "id": "gelios.custom_field", "name": "Custom Field", "surface": "domain", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider-configurable unit field. Its value is restricted until explicitly classified because semantics and PII risk are dynamic." },
|
||||
{ "id": "gelios.geozone", "name": "Gelios Geozone", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider geofence or zone geometry. Large geometry collections require a separate loading and LOD strategy." },
|
||||
{ "id": "gelios.geozone_group", "name": "Gelios Geozone Group", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Provider grouping of geozones." },
|
||||
{ "id": "gelios.geopoint", "name": "Gelios Geopoint", "surface": "spatial", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Named provider spatial point; it must remain distinct from a live unit position." },
|
||||
{ "id": "gelios.report_template", "name": "Gelios Report Template", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro", "summary": "Reusable provider report definition available to the account or scope." },
|
||||
{ "id": "gelios.report_result", "name": "Gelios Report Result", "surface": "analytics", "status": ["source-evidenced"], "authority": "Gelios Pro + Gelios Gateway", "summary": "Bounded result, table, graphic or map output produced from an approved report request; not a default realtime stream." },
|
||||
{ "id": "gelios.collection_run", "name": "Gelios Collection Run", "surface": "platform", "status": ["product-required"], "authority": "Gelios Gateway", "summary": "Auditable read-only collection attempt with scope, endpoint family, volume controls and outcome; it contains no token or raw payload." },
|
||||
{ "id": "gelios.ingestion_cursor", "name": "Gelios Ingestion Cursor", "surface": "platform", "status": ["product-required"], "authority": "Gelios Gateway + telemetry storage", "summary": "Checkpoint and watermark controlling resumable historical or incremental ingestion for an approved unit and data family." },
|
||||
{ "id": "gelios.command_template", "name": "Gelios Command Template", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Read-only catalogued command template assigned to a unit. Template content is red-domain configuration, not a telemetry field." },
|
||||
{ "id": "gelios.command_group", "name": "Gelios Command Group", "surface": "command", "status": ["source-evidenced", "product-required"], "authority": "Gelios Pro", "summary": "Provider grouping of command templates, managed separately from operational unit groups." },
|
||||
{ "id": "gelios.command_dispatch", "name": "Gelios Command Dispatch", "surface": "command", "status": ["product-required"], "authority": "Future Command Gateway", "summary": "Explicit, user-confirmed request to send a command to one approved unit or approved group. It must never be created by background ingestion or map rendering." },
|
||||
{ "id": "gelios.command_delivery", "name": "Gelios Command Delivery", "surface": "command", "status": ["product-required", "source-evidenced"], "authority": "Gelios Pro + Future Command Gateway", "summary": "Provider delivery/task status associated with a command dispatch." },
|
||||
{ "id": "gelios.command_audit", "name": "Gelios Command Audit", "surface": "command", "status": ["product-required"], "authority": "Future Command Gateway", "summary": "Immutable governance record of an explicit command decision, confirmation, target, outcome and actor; separate from provider command history." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"sourceRoots": [
|
||||
{
|
||||
"surface": "gelios-rest",
|
||||
"path": "https://api.geliospro.com/docs",
|
||||
"mode": "OpenAPI inspection and safe read-only runtime audit; no write routes executed"
|
||||
},
|
||||
{
|
||||
"surface": "engine",
|
||||
"path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_ENGINE_INFRA/nodedc-source",
|
||||
"mode": "Read-only MMAP workflow, Gelios node and historical snapshot donor inspection"
|
||||
},
|
||||
{
|
||||
"surface": "module-studio",
|
||||
"path": "/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_DESIGN_GUIDELINE",
|
||||
"mode": "Future provider-neutral map target surface"
|
||||
}
|
||||
],
|
||||
"ledgers": [
|
||||
{
|
||||
"id": "ledger.gelios_domain_v0",
|
||||
"path": "docs/GELIOS_DOMAIN_ONTOLOGY.md",
|
||||
"entityIds": [
|
||||
"gelios.integration",
|
||||
"gelios.access_scope",
|
||||
"gelios.unit",
|
||||
"gelios.telemetry_snapshot",
|
||||
"gelios.position_fix",
|
||||
"gelios.sensor_definition",
|
||||
"gelios.sensor_reading",
|
||||
"gelios.geozone",
|
||||
"gelios.report_template",
|
||||
"gelios.command_template",
|
||||
"gelios.command_dispatch",
|
||||
"gelios.ingestion_cursor"
|
||||
]
|
||||
}
|
||||
],
|
||||
"baselineDocs": ["docs/GELIOS_DOMAIN_ONTOLOGY.md"],
|
||||
"restrictions": [
|
||||
"Do not copy access/refresh tokens, passwords, hardware decrypt keys or runtime snapshots into Ontology Core.",
|
||||
"Do not make the current Engine Gelios node, Cesium entities or provider group names canonical domain identities.",
|
||||
"Do not invoke command send, create, update, delete or purge routes as ontology evidence.",
|
||||
"Do not treat the 107 currently visible provider-account units or the 95 legacy snapshot units as the final Robot2B scope without owner approval.",
|
||||
"Do not bulk-download history, media or full geozone geometry before collection policy and storage architecture are approved."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"rules": [
|
||||
{
|
||||
"id": "guardrail.gelios.ontology_not_runtime_store",
|
||||
"severity": "error",
|
||||
"summary": "Ontology Core describes Gelios meanings and contracts only. It must not contain access/refresh tokens, raw messages, live positions, provider command text, hardware decrypt keys or bulk geometry.",
|
||||
"entityIds": ["gelios.integration", "gelios.telemetry_snapshot", "gelios.raw_telemetry_message", "gelios.position_fix", "gelios.command_template", "gelios.geozone"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.scope_precedes_collection",
|
||||
"severity": "error",
|
||||
"summary": "A Gelios collection run must resolve an approved Robot2B access scope before addressing units. Provider-account visibility and provider group membership are not sufficient product scope on their own.",
|
||||
"entityIds": ["gelios.integration", "gelios.access_scope", "gelios.collection_run", "gelios.unit", "gelios.unit_group"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.commands_are_red_domain",
|
||||
"severity": "error",
|
||||
"summary": "Command template catalogue is read-only data. Command dispatch, command group mutation, history purge and every send route are separate red-domain actions requiring explicit human confirmation and audit; ingestion and map rendering may never create them.",
|
||||
"entityIds": ["gelios.command_template", "gelios.command_group", "gelios.command_dispatch", "gelios.command_delivery", "gelios.command_audit"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.map_uses_stable_subjects",
|
||||
"severity": "error",
|
||||
"summary": "Cesium pins and labels must consume stable gelios.unit and normalized gelios.position_fix through map.moving_object. They must not use raw message IDs, transient Cesium entity IDs or provider credentials as map identity.",
|
||||
"entityIds": ["gelios.unit", "gelios.position_fix", "gelios.telemetry_snapshot", "map.moving_object", "map.pin", "map.label"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.raw_and_pii_are_restricted",
|
||||
"severity": "warning",
|
||||
"summary": "IMEI/hardware identifiers, phone fields, precise addresses, raw params, custom fields and unclassified sensor values require explicit field policy before analytics, Studio or external exposure.",
|
||||
"entityIds": ["gelios.tracker_device", "gelios.position_fix", "gelios.telemetry_parameter", "gelios.custom_field", "gelios.sensor_reading"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.geometry_requires_bounded_loading",
|
||||
"severity": "warning",
|
||||
"summary": "Geozone and report-map loading must use explicit paging, filtering, volume limits, LOD and retention policy. The unpaged provider geozone response is not an acceptable realtime renderer contract.",
|
||||
"entityIds": ["gelios.geozone", "gelios.geozone_group", "gelios.collection_run", "map.zone", "map.visibility_rule"]
|
||||
},
|
||||
{
|
||||
"id": "guardrail.gelios.history_requires_cursor",
|
||||
"severity": "warning",
|
||||
"summary": "Any future history ingestion must be bounded by unit scope, time window, field whitelist and durable cursor. It must not turn a historical API family into an uncontrolled account dump.",
|
||||
"entityIds": ["gelios.collection_run", "gelios.ingestion_cursor", "gelios.raw_telemetry_message", "gelios.report_result"]
|
||||
}
|
||||
],
|
||||
"blockedConflations": [
|
||||
["gelios.integration", "gelios.access_scope"],
|
||||
["gelios.unit", "gelios.tracker_device"],
|
||||
["gelios.telemetry_snapshot", "gelios.raw_telemetry_message"],
|
||||
["gelios.position_fix", "map.moving_object"],
|
||||
["gelios.command_template", "gelios.command_dispatch"],
|
||||
["gelios.command_dispatch", "gelios.command_delivery"],
|
||||
["gelios.geozone", "map.zone"]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "gelios",
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"status": "product-required/source-evidenced",
|
||||
"summary": "Gelios Pro telemetry, fleet, spatial, reporting and command-domain ontology for the Robot2B client context. The package describes canonical meanings and contracts; it does not store credentials or runtime telemetry."
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"relations": [
|
||||
{ "id": "gelios.integration.is_provider_connection", "from": ["gelios.integration"], "to": ["integration.connection"], "status": "product-required", "summary": "A Gelios integration is a tenant-scoped instance of the common external provider connection contract." },
|
||||
{ "id": "gelios.access_scope.specializes_integration_scope", "from": ["gelios.access_scope"], "to": ["integration.access_scope"], "status": "product-required", "summary": "Robot2B/Gelios object approval specializes the common provider access-scope contract." },
|
||||
{ "id": "gelios.collection_run.is_integration_collection_run", "from": ["gelios.collection_run"], "to": ["integration.collection_run"], "status": "product-required", "summary": "Gelios collection runs inherit the bounded collection and audit lifecycle of external integrations." },
|
||||
{ "id": "gelios.raw_message.is_integration_raw_envelope", "from": ["gelios.raw_telemetry_message"], "to": ["integration.raw_envelope"], "status": "product-required", "summary": "A Gelios raw message is a restricted provider envelope, not a UI read model." },
|
||||
{ "id": "gelios.unit.is_integration_canonical_subject", "from": ["gelios.unit"], "to": ["integration.canonical_subject"], "status": "product-required", "summary": "A Gelios unit is a stable canonical subject produced by the provider adapter." },
|
||||
{ "id": "gelios.command_template.is_integration_command_capability", "from": ["gelios.command_template"], "to": ["integration.command_capability"], "status": "product-required", "summary": "Gelios command templates are catalogued provider command capabilities only." },
|
||||
{ "id": "gelios.command_dispatch.is_integration_command_intent", "from": ["gelios.command_dispatch"], "to": ["integration.command_intent"], "status": "future-concept", "summary": "Any future Gelios command dispatch must become an explicitly governed generic command intent." },
|
||||
{ "id": "gelios.command_audit.is_integration_command_audit", "from": ["gelios.command_audit"], "to": ["integration.command_audit"], "status": "future-concept", "summary": "Gelios command audit specializes the generic red-domain audit contract." },
|
||||
{ "id": "gelios.integration.enforces_scope", "from": ["gelios.integration"], "to": ["gelios.access_scope"], "status": "product-required", "summary": "The Gateway applies an approved Robot2B scope before reading or publishing data." },
|
||||
{ "id": "gelios.access_scope.includes_unit", "from": ["gelios.access_scope"], "to": ["gelios.unit", "gelios.unit_group"], "status": "product-required", "summary": "An approved scope can include explicit units and approved group criteria, but group membership alone is not authoritative." },
|
||||
{ "id": "gelios.unit.belongs_to_group", "from": ["gelios.unit"], "to": ["gelios.unit_group"], "status": "source-evidenced", "summary": "A provider unit can belong to one or more provider unit groups." },
|
||||
{ "id": "gelios.unit.uses_tracker_device", "from": ["gelios.unit"], "to": ["gelios.tracker_device"], "status": "source-evidenced", "summary": "A tracked unit is associated with a hardware tracker device." },
|
||||
{ "id": "gelios.unit.has_telemetry_snapshot", "from": ["gelios.unit"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "The Gateway maintains normalized current telemetry for an approved unit." },
|
||||
{ "id": "gelios.raw_message.normalizes_to_snapshot", "from": ["gelios.raw_telemetry_message"], "to": ["gelios.telemetry_snapshot"], "status": "product-required", "summary": "Restricted raw provider telemetry can be normalized into the stable snapshot contract." },
|
||||
{ "id": "gelios.telemetry_snapshot.has_position_fix", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.position_fix"], "status": "product-required", "summary": "A current telemetry snapshot can carry one time-qualified position fix." },
|
||||
{ "id": "gelios.telemetry_snapshot.has_operational_status", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.operational_status"], "status": "product-required", "summary": "Gateway derives normalized operational state from telemetry and data-quality policy." },
|
||||
{ "id": "gelios.telemetry_snapshot.has_parameter", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.telemetry_parameter"], "status": "source-evidenced", "summary": "A provider snapshot can contain dynamic telemetry parameters." },
|
||||
{ "id": "gelios.unit.has_sensor", "from": ["gelios.unit"], "to": ["gelios.sensor_definition"], "status": "source-evidenced", "summary": "A unit exposes zero or more sensor definitions." },
|
||||
{ "id": "gelios.sensor_definition.has_conversion", "from": ["gelios.sensor_definition"], "to": ["gelios.sensor_conversion"], "status": "source-evidenced", "summary": "A sensor can define conversion/calibration rows." },
|
||||
{ "id": "gelios.sensor_reading.observes_sensor", "from": ["gelios.sensor_reading"], "to": ["gelios.sensor_definition"], "status": "product-required", "summary": "Each normalized reading identifies the sensor definition it observes." },
|
||||
{ "id": "gelios.telemetry_snapshot.has_sensor_reading", "from": ["gelios.telemetry_snapshot"], "to": ["gelios.sensor_reading"], "status": "product-required", "summary": "Current telemetry can expose normalized readings for available sensors." },
|
||||
{ "id": "gelios.unit.has_fuel_profile", "from": ["gelios.unit"], "to": ["gelios.fuel_profile"], "status": "source-evidenced", "summary": "Fuel configuration belongs to a unit and is interpreted through approved sensor semantics." },
|
||||
{ "id": "gelios.unit.has_maintenance_plan", "from": ["gelios.unit"], "to": ["gelios.maintenance_plan"], "status": "source-evidenced", "summary": "A unit can have zero or more planned maintenance configurations." },
|
||||
{ "id": "gelios.unit.has_custom_field", "from": ["gelios.unit"], "to": ["gelios.custom_field"], "status": "source-evidenced", "summary": "A unit can have provider-defined custom fields." },
|
||||
{ "id": "gelios.geozone.belongs_to_group", "from": ["gelios.geozone"], "to": ["gelios.geozone_group"], "status": "source-evidenced", "summary": "A provider geozone can belong to a provider geozone group." },
|
||||
{ "id": "gelios.report_result.uses_template", "from": ["gelios.report_result"], "to": ["gelios.report_template"], "status": "source-evidenced", "summary": "A report result is produced from an approved report template and bounded request." },
|
||||
{ "id": "gelios.collection_run.checkpoints_with_cursor", "from": ["gelios.collection_run"], "to": ["gelios.ingestion_cursor"], "status": "product-required", "summary": "Collection runs advance resumable ingestion only through a durable cursor." },
|
||||
{ "id": "gelios.collection_run.collects_unit", "from": ["gelios.collection_run"], "to": ["gelios.unit"], "status": "product-required", "summary": "A read-only collection run records the approved unit scope it addressed." },
|
||||
{ "id": "gelios.unit.has_command_template", "from": ["gelios.unit"], "to": ["gelios.command_template"], "status": "source-evidenced", "summary": "Command templates are catalogued per unit through read-only routes." },
|
||||
{ "id": "gelios.command_template.belongs_to_group", "from": ["gelios.command_template"], "to": ["gelios.command_group"], "status": "source-evidenced", "summary": "A command template can belong to a provider command group." },
|
||||
{ "id": "gelios.command_dispatch.uses_template", "from": ["gelios.command_dispatch"], "to": ["gelios.command_template"], "status": "product-required", "summary": "A dispatch may reference a reviewed command template." },
|
||||
{ "id": "gelios.command_dispatch.targets_scope", "from": ["gelios.command_dispatch"], "to": ["gelios.unit", "gelios.unit_group", "gelios.access_scope"], "status": "product-required", "summary": "A dispatch target must resolve to an approved unit or explicitly approved group in scope." },
|
||||
{ "id": "gelios.command_dispatch.has_delivery", "from": ["gelios.command_dispatch"], "to": ["gelios.command_delivery"], "status": "product-required", "summary": "A dispatch is associated with delivery/task state rather than assumed successful on request creation." },
|
||||
{ "id": "gelios.command_dispatch.has_audit", "from": ["gelios.command_dispatch"], "to": ["gelios.command_audit"], "status": "product-required", "summary": "Every dispatch requires immutable user-confirmation and outcome audit." },
|
||||
{ "id": "gelios.unit.is_map_moving_object", "from": ["gelios.unit"], "to": ["map.moving_object"], "status": "product-required", "summary": "An approved Gelios unit is rendered through the provider-neutral moving-object contract, never as a Cesium entity identity." },
|
||||
{ "id": "gelios.position_fix.positions_map_subject", "from": ["gelios.position_fix"], "to": ["map.moving_object"], "status": "product-required", "summary": "A normalized position fix updates the stable map subject bound to the Gelios unit." },
|
||||
{ "id": "gelios.geozone.is_map_zone", "from": ["gelios.geozone"], "to": ["map.zone"], "status": "product-required", "summary": "An approved Gelios geozone is exposed to the map through the generic zone contract." },
|
||||
{ "id": "gelios.geopoint.is_map_place_target", "from": ["gelios.geopoint"], "to": ["map.place_target"], "status": "product-required", "summary": "An approved Gelios geopoint is exposed through a provider-neutral place target." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"aliases": [
|
||||
{"alias":"коннектор","canonicalId":"integration.connection","language":"ru","status":"product-required"},
|
||||
{"alias":"connector","canonicalId":"integration.connection","language":"en","status":"product-required"},
|
||||
{"alias":"внешний провайдер","canonicalId":"integration.provider","language":"ru","status":"product-required"},
|
||||
{"alias":"provider","canonicalId":"integration.provider","language":"en","status":"product-required"},
|
||||
{"alias":"источник данных","canonicalId":"integration.provider","language":"ru","status":"product-required"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"entities": [
|
||||
{"id":"integration.provider","name":"External Provider","surface":"integration","status":["product-required"],"authority":"Platform External Provider Data Plane","summary":"A third-party product or API family integrated through an app-owned adapter, not a tenant or a renderer."},
|
||||
{"id":"integration.connection","name":"Provider Connection","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"A tenant-scoped configured connection to one provider account or endpoint; it contains policy and secret references, never secret values."},
|
||||
{"id":"integration.credential_reference","name":"Provider Credential Reference","surface":"integration","status":["product-required"],"authority":"Provider Gateway / secret store","summary":"Opaque reference to a server-side secret or token lifecycle, not a token, login or password."},
|
||||
{"id":"integration.access_scope","name":"Integration Access Scope","surface":"integration","status":["product-required"],"authority":"Client owner + Provider Gateway","summary":"Approved product boundary for source accounts, objects, fields and permitted operations; provider visibility alone is insufficient."},
|
||||
{"id":"integration.capability","name":"Provider Capability","surface":"integration","status":["product-required"],"authority":"Provider Adapter","summary":"Versioned provider operation or feature classified as read, metadata, write, destructive or unknown."},
|
||||
{"id":"integration.field_definition","name":"Provider Field Definition","surface":"integration","status":["product-required"],"authority":"Provider Adapter + Ontology Core","summary":"A documented source field, its semantics, sensitivity, units and normalisation mapping; it is not a live value."},
|
||||
{"id":"integration.collection_profile","name":"Collection Profile","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Approved rate, paging, cursor, capability and field selection for a connection; prevents uncontrolled account-wide polling."},
|
||||
{"id":"integration.retention_policy","name":"Integration Retention Policy","surface":"integration","status":["product-required"],"authority":"Client owner + Provider Gateway","summary":"Lifecycle rule for current state, raw envelope, normalised history, aggregates and backups."},
|
||||
{"id":"integration.collection_run","name":"Integration Collection Run","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Auditable bounded execution of a safe-read collection profile with cursor, response metrics and outcome."},
|
||||
{"id":"integration.raw_envelope","name":"Raw Provider Envelope","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Restricted provenance-preserving representation of a safe-read provider response, stored in a bounded cold layer rather than exposed to UI."},
|
||||
{"id":"integration.canonical_subject","name":"Canonical Integrated Subject","surface":"integration","status":["product-required"],"authority":"Provider Domain Adapter","summary":"Stable domain subject normalised from a provider object and suitable for relations, analytics and interfaces."},
|
||||
{"id":"integration.read_model","name":"Integration Read Model","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Scoped, versioned current, history or aggregate projection delivered to approved consumers without raw provider bypass."},
|
||||
{"id":"integration.realtime_channel","name":"Integration Realtime Channel","surface":"integration","status":["product-required"],"authority":"Provider Gateway","summary":"Internal bounded event or subscription contract for projection changes; its emission rate is independent from provider collection rate."},
|
||||
{"id":"integration.command_capability","name":"Provider Command Capability","surface":"integration","status":["product-required"],"authority":"Provider Adapter","summary":"Catalogued command metadata, parameters and risk classification. It is not permission or a send route."},
|
||||
{"id":"integration.command_intent","name":"Provider Command Intent","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"A future explicitly confirmed, scoped and idempotent request to execute a catalogued command; no read collector may create it."},
|
||||
{"id":"integration.command_audit","name":"Provider Command Audit","surface":"integration","status":["future-concept"],"authority":"Future Provider Command Gateway","summary":"Immutable approval, execution and delivery audit for a red-domain command lifecycle."}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"sourceRoots": ["platform/docs", "platform/packages", "platform/services"],
|
||||
"ledgers": [
|
||||
{
|
||||
"id": "external_provider_data_plane_adr",
|
||||
"path": "../../docs/ADR_EXTERNAL_PROVIDER_DATA_PLANE.md",
|
||||
"entityIds": ["integration.provider", "integration.connection", "integration.access_scope", "integration.capability", "integration.collection_profile", "integration.read_model", "integration.command_capability"]
|
||||
}
|
||||
],
|
||||
"baselineDocs": ["../../docs/ADR_EXTERNAL_PROVIDER_DATA_PLANE.md", "../../packages/external-provider-contract/README.md"],
|
||||
"restrictions": ["The package is a semantic contract. Provider secrets, runtime data and command transport remain outside Ontology Core."]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"rules": [
|
||||
{"id":"guardrail.integration.ontology_is_not_data_plane","severity":"error","summary":"Ontology Core may describe providers and contracts but must not store secret values, live payloads, tenant telemetry, raw envelopes or provider execution state.","entityIds":["integration.provider","integration.connection","integration.credential_reference","integration.raw_envelope","integration.read_model"]},
|
||||
{"id":"guardrail.integration.scope_precedes_collection","severity":"error","summary":"A collection run must resolve tenant scope, field policy and bounded collection profile before it addresses a provider. Account visibility does not define product scope.","entityIds":["integration.connection","integration.access_scope","integration.collection_profile","integration.collection_run"]},
|
||||
{"id":"guardrail.integration.read_model_not_raw_bypass","severity":"error","summary":"L2 workflows, interfaces and renderer adapters consume scoped read models or realtime channels, never provider credentials, raw envelopes or direct adapter database access.","entityIds":["integration.credential_reference","integration.raw_envelope","integration.read_model","integration.realtime_channel"]},
|
||||
{"id":"guardrail.integration.commands_are_separate_red_domain","severity":"error","summary":"Cataloguing a command does not activate it. Read collectors, L2 workflows, maps and AI Workspace may not create command intent or delivery. A later command gateway requires explicit human confirmation, scope, idempotency and audit.","entityIds":["integration.command_capability","integration.command_intent","integration.command_audit","integration.collection_run"]},
|
||||
{"id":"guardrail.integration.full_catalog_not_full_rate","severity":"warning","summary":"Every provider field may be catalogued, but enabling continuous collection requires an explicit collection, retention and rate policy. Do not turn a capability catalog into unbounded account polling.","entityIds":["integration.capability","integration.field_definition","integration.collection_profile","integration.retention_policy"]}
|
||||
],
|
||||
"blockedConflations": [
|
||||
["integration.provider","integration.connection"],
|
||||
["integration.connection","integration.credential_reference"],
|
||||
["integration.raw_envelope","integration.read_model"],
|
||||
["integration.command_capability","integration.command_intent"],
|
||||
["integration.collection_run","integration.command_intent"]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "integration",
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"status": "product-required/source-evidenced",
|
||||
"summary": "Provider-neutral external integration ontology: tenant-scoped connections, capabilities, collection, controlled storage projections and a separated red command domain."
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"updatedAt": "2026-07-13",
|
||||
"relations": [
|
||||
{"id":"integration.provider.offers_connection","from":["integration.provider"],"to":["integration.connection"],"status":"product-required","summary":"A provider is connected through one or more tenant-scoped connection instances."},
|
||||
{"id":"integration.connection.references_credential","from":["integration.connection"],"to":["integration.credential_reference"],"status":"product-required","summary":"A connection uses an opaque server-side credential reference rather than exposing a secret."},
|
||||
{"id":"integration.connection.enforces_scope","from":["integration.connection"],"to":["integration.access_scope"],"status":"product-required","summary":"Every source request is bounded by an approved product scope."},
|
||||
{"id":"integration.provider.declares_capability","from":["integration.provider"],"to":["integration.capability","integration.command_capability"],"status":"product-required","summary":"Adapter manifests catalogue source read and command capabilities independently from their activation."},
|
||||
{"id":"integration.capability.exposes_field","from":["integration.capability"],"to":["integration.field_definition"],"status":"product-required","summary":"A safe-read capability documents the fields it may return before a collection profile enables them."},
|
||||
{"id":"integration.collection_profile.selects_capability","from":["integration.collection_profile"],"to":["integration.capability","integration.field_definition"],"status":"product-required","summary":"A profile selects bounded capabilities and fields rather than polling every API feature continuously."},
|
||||
{"id":"integration.collection_profile.applies_retention","from":["integration.collection_profile"],"to":["integration.retention_policy"],"status":"product-required","summary":"Collection and storage lifecycle are explicit per connection profile."},
|
||||
{"id":"integration.collection_run.uses_connection","from":["integration.collection_run"],"to":["integration.connection","integration.collection_profile"],"status":"product-required","summary":"A collection run records which connection and bounded policy it used."},
|
||||
{"id":"integration.collection_run.records_raw_envelope","from":["integration.collection_run"],"to":["integration.raw_envelope"],"status":"product-required","summary":"Safe-read response provenance is retained only according to the active policy."},
|
||||
{"id":"integration.raw_envelope.normalizes_subject","from":["integration.raw_envelope"],"to":["integration.canonical_subject"],"status":"product-required","summary":"Provider payloads are normalised into stable domain subjects before consumption."},
|
||||
{"id":"integration.read_model.projects_subject","from":["integration.read_model"],"to":["integration.canonical_subject"],"status":"product-required","summary":"Consumers receive a scoped projection rather than a provider payload or direct database access."},
|
||||
{"id":"integration.realtime_channel.emits_read_model","from":["integration.realtime_channel"],"to":["integration.read_model"],"status":"product-required","summary":"Realtime delivery emits approved projection changes with independently controlled sampling."},
|
||||
{"id":"integration.command_intent.uses_capability","from":["integration.command_intent"],"to":["integration.command_capability"],"status":"future-concept","summary":"A future command intent may select only a catalogued command capability."},
|
||||
{"id":"integration.command_intent.has_audit","from":["integration.command_intent"],"to":["integration.command_audit"],"status":"future-concept","summary":"Every future command lifecycle requires immutable approval and delivery audit."}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
{ "id": "map.grid_layer", "name": "Map Grid Layer", "surface": "map", "status": ["source-evidenced"], "authority": "NDC Module Studio", "summary": "Planetary or situational grid with configurable modes, levels of detail, step, radius and visual primitives." },
|
||||
{ "id": "map.place_target", "name": "Map Place Target", "surface": "map", "status": ["source-evidenced"], "authority": "NDC Module Studio", "summary": "Geographic place target such as a city or territory with location, pulse, radius and distance-dependent presentation." },
|
||||
{ "id": "map.moving_object", "name": "Map Moving Object", "surface": "map", "status": ["source-evidenced"], "authority": "NDC domain source", "summary": "Dynamic spatial object such as transport, robot or vehicle with identity, position, movement and status." },
|
||||
{ "id": "map.pin", "name": "Map Pin", "surface": "map", "status": ["source-evidenced"], "authority": "NDC Module Studio", "summary": "Reusable spatial pin presentation with stem, point, height, color, status and visibility rules." },
|
||||
{ "id": "map.pin", "name": "Map Pin", "surface": "map", "status": ["source-evidenced"], "authority": "NDC Module Studio", "summary": "Reusable provider-neutral elevated-spike presentation with ground anchor, stem, head, label anchor, semantic colour/status and camera-height visibility rules." },
|
||||
{ "id": "map.label", "name": "Map Label", "surface": "map", "status": ["source-evidenced", "product-required"], "authority": "NDC Module Studio", "summary": "Reusable information plate attached to a spatial subject with style, size variant, anchor, offset and visibility rules." },
|
||||
{ "id": "map.zone", "name": "Map Zone", "surface": "map", "status": ["source-evidenced"], "authority": "NDC domain source", "summary": "Polygon or multipolygon zone, sector or geofence with optional height, extrusion and level-dependent style." },
|
||||
{ "id": "map.route", "name": "Map Route", "surface": "map", "status": ["source-evidenced"], "authority": "NDC domain source", "summary": "Logical ordered path or service route independent of renderer geometry." },
|
||||
|
||||
@@ -81,14 +81,14 @@
|
||||
{ "id": "engine.workflow_owner", "name": "ENGINE Workflow Owner", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE", "summary": "Owner field in ACL." },
|
||||
{ "id": "engine.workflow_share", "name": "ENGINE Workflow Share", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE", "summary": "Share/invite/user access routes." },
|
||||
{ "id": "engine.workflow_access_request", "name": "ENGINE Workflow Access Request", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE", "summary": "Access request route/model for workflow access." },
|
||||
{ "id": "engine.workflow_l2", "name": "ENGINE L2 Workflow", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE/n8n bridge", "summary": "n8n subworkflow attached to L1 node." },
|
||||
{ "id": "engine.node_l2", "name": "ENGINE L2 Node", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE/n8n bridge", "summary": "Node inside compiled/deployed n8n workflow." },
|
||||
{ "id": "engine.l2_runtime_core", "name": "ENGINE L2 Runtime Core", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE/n8n bridge", "summary": "Target n8n runtime/core instance." },
|
||||
{ "id": "engine.workflow_l2_runtime_id", "name": "ENGINE L2 Runtime Workflow ID", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE/n8n bridge", "summary": "n8n runtime workflow id, separate from NodeDC workflow id." },
|
||||
{ "id": "engine.l2_execution", "name": "ENGINE L2 Execution", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE/n8n bridge", "summary": "n8n execution id mapped to run/execution records." },
|
||||
{ "id": "engine.workflow_l2", "name": "ENGINE L2 Workflow", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE execution bridge", "summary": "L2 execution workflow attached to an L1 node." },
|
||||
{ "id": "engine.node_l2", "name": "ENGINE L2 Node", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE execution bridge", "summary": "Node inside a compiled/deployed L2 workflow." },
|
||||
{ "id": "engine.l2_runtime_core", "name": "ENGINE L2 Runtime Core", "surface": "engine", "status": ["source-evidenced"], "authority": "ENGINE execution bridge", "summary": "Target Engine execution core." },
|
||||
{ "id": "engine.workflow_l2_runtime_id", "name": "ENGINE L2 Runtime Workflow ID", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE execution bridge", "summary": "L2 runtime workflow id, separate from the L1 workflow id." },
|
||||
{ "id": "engine.l2_execution", "name": "ENGINE L2 Execution", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE execution bridge", "summary": "L2 execution id mapped to run/execution records." },
|
||||
{ "id": "engine.l2_run", "name": "ENGINE L2 Run", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE-side OpsLayer", "summary": "Runtime run id; tech-debt-adjacent." },
|
||||
{ "id": "engine.l2_session", "name": "ENGINE L2 Session", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE-side OpsLayer", "summary": "Runtime session anchor." },
|
||||
{ "id": "engine.l2_runtime_event", "name": "ENGINE L2 Runtime Event", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE/n8n bridge", "summary": "Runtime event: workflow/node start/finish/success/error." },
|
||||
{ "id": "engine.l2_runtime_event", "name": "ENGINE L2 Runtime Event", "surface": "engine", "status": ["source-confirmed"], "authority": "ENGINE execution bridge", "summary": "Runtime event: workflow/node start/finish/success/error." },
|
||||
{ "id": "engine.source_stamp", "name": "ENGINE Source Stamp", "surface": "engine", "status": ["pending"], "authority": "ENGINE", "summary": "Future source/version traceability candidate." },
|
||||
|
||||
{ "id": "assistant.assistant", "name": "Assistant", "surface": "assistant", "status": ["product-required", "pending"], "authority": "AI Workspace", "summary": "User-facing assistant capability/persona." },
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
{ "id": "engine.workflow_acl.has_owner", "from": ["engine.workflow_acl"], "to": ["engine.workflow_owner"], "status": "source-confirmed", "summary": "ACL owns owner field." },
|
||||
{ "id": "engine.workflow_l1.has_share", "from": ["engine.workflow_l1"], "to": ["engine.workflow_share"], "status": "source-confirmed", "summary": "Workflow can be shared/invited." },
|
||||
{ "id": "engine.workflow_l1.has_access_request", "from": ["engine.workflow_l1"], "to": ["engine.workflow_access_request"], "status": "source-confirmed", "summary": "Access requests target workflow." },
|
||||
{ "id": "engine.node_l1.embeds_l2_workflow", "from": ["engine.node_l1"], "to": ["engine.workflow_l2"], "status": "source-evidenced", "summary": "n8n subworkflow is attached to L1 node." },
|
||||
{ "id": "engine.workflow_l2.deployed_as_runtime_workflow", "from": ["engine.workflow_l2"], "to": ["engine.workflow_l2_runtime_id"], "status": "source-confirmed", "summary": "Compiled n8n workflow deploy returns runtime workflow id." },
|
||||
{ "id": "engine.workflow_l2.runs_on_runtime_core", "from": ["engine.workflow_l2"], "to": ["engine.l2_runtime_core"], "status": "source-evidenced", "summary": "L2 workflow targets n8n instance/core." },
|
||||
{ "id": "engine.l2_execution.belongs_to_runtime_workflow", "from": ["engine.l2_execution"], "to": ["engine.workflow_l2_runtime_id"], "status": "source-confirmed", "summary": "Execution is emitted for n8n workflow id." },
|
||||
{ "id": "engine.node_l1.embeds_l2_workflow", "from": ["engine.node_l1"], "to": ["engine.workflow_l2"], "status": "source-evidenced", "summary": "L2 execution workflow is attached to an L1 node." },
|
||||
{ "id": "engine.workflow_l2.deployed_as_runtime_workflow", "from": ["engine.workflow_l2"], "to": ["engine.workflow_l2_runtime_id"], "status": "source-confirmed", "summary": "L2 deploy returns a runtime workflow id." },
|
||||
{ "id": "engine.workflow_l2.runs_on_runtime_core", "from": ["engine.workflow_l2"], "to": ["engine.l2_runtime_core"], "status": "source-evidenced", "summary": "L2 workflow targets the Engine execution core." },
|
||||
{ "id": "engine.l2_execution.belongs_to_runtime_workflow", "from": ["engine.l2_execution"], "to": ["engine.workflow_l2_runtime_id"], "status": "source-confirmed", "summary": "Execution is emitted for the L2 runtime workflow id." },
|
||||
{ "id": "engine.l2_runtime_event.describes_execution", "from": ["engine.l2_runtime_event"], "to": ["engine.l2_execution"], "status": "source-confirmed", "summary": "Runtime events carry execution id." },
|
||||
{ "id": "engine.l2_runtime_event.binds_run", "from": ["engine.l2_runtime_event"], "to": ["engine.l2_run"], "status": "source-confirmed", "summary": "Event binds to run id. Current storage is ENGINE-side OpsLayer." },
|
||||
{ "id": "engine.l2_run.has_session", "from": ["engine.l2_run"], "to": ["engine.l2_session"], "status": "source-confirmed", "summary": "Session anchors related runtime events/runs." },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"relationId": "ontology.resolves_ops_card_to_engine_context",
|
||||
"fromSurface": "ops",
|
||||
"inputEntityIds": ["ops.card", "ops.project"],
|
||||
"intentHints": ["engine", "workflow", "l1", "l2", "n8n", "inspect", "switch", "runtime", "докодим", "воркфлоу"],
|
||||
"intentHints": ["engine", "workflow", "l1", "l2", "inspect", "switch", "runtime", "докодим", "воркфлоу"],
|
||||
"outputSurface": "engine",
|
||||
"outputEntityIds": ["engine.workflow_l1", "engine.node_l1", "engine.workflow_l2", "engine.workflow_l2_runtime_id"],
|
||||
"requiredBindings": [
|
||||
|
||||
@@ -79,7 +79,7 @@ If L2 runtime is known, add:
|
||||
```json
|
||||
{
|
||||
"runtime_workflow_id": "...",
|
||||
"n8n_instance_id": "..."
|
||||
"runtime_instance_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Gelios Domain Ontology
|
||||
|
||||
Package: `catalog/domain-packages/gelios`
|
||||
|
||||
Status: `v0.1.0` source-evidenced and product-required slice for the Robot2B client context and the Gelios provider.
|
||||
|
||||
## Purpose and boundary
|
||||
|
||||
The package describes the meaning of Gelios fleet, telemetry, sensor, spatial, report and command concepts. It is not a Gelios client, token store, telemetry database, Cesium implementation or Engine workflow.
|
||||
|
||||
The practical audit established a wide REST read surface: 107 currently visible units, 105 with `lastMsg`, a visible administrative account, unit groups, telemetry variants, sensor catalogues, command-template catalogues, geozone groups and report templates. The REST OpenAPI documents 295 operations. This ontology therefore separates three things that must not be conflated:
|
||||
|
||||
1. documented provider capability;
|
||||
2. actual runtime read evidence for the current account;
|
||||
3. approved Robot2B product scope.
|
||||
|
||||
The 95-unit legacy Engine snapshot is a temporary compatibility reference only. It is not the final business scope. The provider's current groups also do not resolve that scope automatically. A Robot2B owner must approve an allowlist or equivalent rule represented by `gelios.access_scope`.
|
||||
|
||||
## Canonical value contracts
|
||||
|
||||
### Unit and device
|
||||
|
||||
`gelios.unit` is the stable business subject. The durable key is a namespaced provider identity such as `gelios.unit:<sourceUnitId>`, never a renderer entity ID or a display name. A unit can use a `gelios.tracker_device`, belong to multiple `gelios.unit_group` records and expose sensor, fuel, maintenance and custom-field configurations.
|
||||
|
||||
Hardware IDs, IMEI, phones and decrypt-related fields are restricted or secret-class data. They are not part of the default Studio contract.
|
||||
|
||||
### Current telemetry
|
||||
|
||||
`gelios.telemetry_snapshot` is the normalized current state produced by the Gateway, with at least:
|
||||
|
||||
```text
|
||||
unitSubjectId
|
||||
observedAt
|
||||
receivedAt
|
||||
position: { latitude, longitude, height?, course?, speed?, satellites? }
|
||||
operationalStatus
|
||||
sensorReadings[]
|
||||
counterValues[]
|
||||
qualityFlags[]
|
||||
source: gelios-rest
|
||||
```
|
||||
|
||||
The source supports a raw and a pure last-message variant. Pure data may include sensors, counters and accumulated fields. Raw messages and dynamic `params` remain behind restricted policy and may be retained separately for diagnostics only. `gelios.position_fix` is the time-qualified position portion used by spatial consumers; it is not itself a pin.
|
||||
|
||||
### Sensors and operational semantics
|
||||
|
||||
`gelios.sensor_definition`, `gelios.sensor_conversion` and `gelios.sensor_reading` separate stable device configuration, calibration and time-series readings. Fuel profile, maintenance plan and custom field are independent configuration concepts. No assumed sensor meaning should be added to analytics until its message parameter, measure and conversion semantics are approved.
|
||||
|
||||
### Spatial binding and Cesium pins
|
||||
|
||||
Gelios is a source domain; Cesium is a renderer adapter. The binding is deliberately provider-neutral:
|
||||
|
||||
```text
|
||||
gelios.unit
|
||||
-> map.moving_object (stable spatial subject)
|
||||
gelios.position_fix
|
||||
-> map.moving_object (current position update)
|
||||
map.moving_object
|
||||
-> map.pin + map.label + map.visibility_rule
|
||||
-> map.renderer_adapter (Cesium today, replaceable later)
|
||||
```
|
||||
|
||||
The future map payload selects a stable `subjectId`, position, normalized operational status, observed time and approved label fields. It must not contain Gelios credentials, raw payloads or Cesium transient entity references. This is the replacement boundary for the current M-map node-to-Cesium coupling.
|
||||
|
||||
The Map Page resolves the selected Studio profile to the shared map.pin contract. The initial accepted presentation is an elevated spike — ground anchor, stem, outlined head and label — but that visual configuration remains on the Studio side. Gelios publishes data for a stable subject; it never publishes Cesium primitive settings, pin pixels or a renderer entity ID.
|
||||
|
||||
The proposed renderer-neutral payload is in [`examples/gelios-map-moving-object.fixture.json`](../examples/gelios-map-moving-object.fixture.json). It uses synthetic coordinates only: it specifies the interface between Gateway and Map View, not a live trike position and not a Cesium entity.
|
||||
|
||||
### Commands
|
||||
|
||||
`gelios.command_template` and `gelios.command_group` are read-only catalogue concepts. `gelios.command_dispatch`, `gelios.command_delivery` and `gelios.command_audit` are a separate red domain.
|
||||
|
||||
No collection run, workflow, map click or autonomous agent may create a dispatch. A future dispatch requires an explicit human action, approved scope, selected unit/group and template/parameters, confirmation, an audit record and delivery-state reconciliation. This ontology package does not grant or test a write permission.
|
||||
|
||||
## Future data plane
|
||||
|
||||
The intended flow is:
|
||||
|
||||
```text
|
||||
Gelios REST OAuth
|
||||
-> server-side Gelios Gateway
|
||||
-> scope filter + field policy + normalizer
|
||||
-> durable telemetry/configuration storage
|
||||
-> NDC level-2 workflow / internal data contract
|
||||
-> Map View binding / Cesium renderer adapter
|
||||
```
|
||||
|
||||
`gelios.collection_run` and `gelios.ingestion_cursor` define collection governance independently of the transport implementation. They support bounded incremental history ingestion later: one approved unit scope, limited time window, field whitelist, volume limit and durable checkpoint. They do not imply that all history should be collected now.
|
||||
|
||||
Database technology is intentionally not chosen by Ontology Core. The proposed platform data-plane boundary and its validation gates are documented in [`platform/docs/ADR_GELIOS_DATA_PLANE.md`](../../../docs/ADR_GELIOS_DATA_PLANE.md). The ontology supplies the record boundaries needed by that ADR.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Ontology Core never stores secrets, runtime snapshots, full raw telemetry or large geometry datasets.
|
||||
- Current provider-account visibility is not the approved Robot2B scope; collection must enforce an owner-approved scope before every run.
|
||||
- GET alone does not make a route safe: login-as, temporary tokens, configuration and WLN download are excluded from telemetry collection.
|
||||
- Geozones require paging, filtering, volume controls and map LOD.
|
||||
- Command send and mutation routes remain red even if the account has documented access.
|
||||
- Map selection and pins target stable domain subjects, not provider payload IDs or renderer objects.
|
||||
|
||||
## Evidence and next implementation decisions
|
||||
|
||||
Evidence is limited to official REST OpenAPI, safe GET results, historical Engine donor inspection and the MAP package. The next decisions outside this ontology package are:
|
||||
|
||||
1. owner-approved Robot2B allowlist/rule;
|
||||
2. field policy for Studio, analytics and raw retention;
|
||||
3. data-plane ADR for realtime telemetry and history;
|
||||
4. Gelios Gateway implementation boundary and deployment location;
|
||||
5. fixture and renderer-adapter contract for Map View pins and labels.
|
||||
@@ -30,6 +30,33 @@ NDC domain source
|
||||
|
||||
Cesium is one possible renderer adapter. A future Cesium version or another provider can replace it without changing Map View, application manifests or domain subject identifiers.
|
||||
|
||||
## Interface semantic ownership
|
||||
|
||||
Interface entities belong in this shared Ontology Core, not in a parallel Studio-specific ontology:
|
||||
|
||||
| Layer | Owns |
|
||||
| --- | --- |
|
||||
| Ontology Core | Canonical meanings and relations: future.interface_view, future.interface_widget, future.interface_binding, map.view, map.moving_object, map.pin, map.label, map.visibility_rule and map.renderer_adapter. |
|
||||
| NDC Module Studio | React implementation, saved visual profiles, page templates, application manifests and the concrete layout chosen by an application owner. |
|
||||
| Engine / NDC workflow | Source integration, normalization and emission of stable subject IDs, current positions, statuses and approved display fields. |
|
||||
| Platform Map Gateway | Provider tokens, cache, proxying and the renderer runtime boundary. |
|
||||
|
||||
Ontology Core never becomes a UI database. It does not store a user's live page layout, camera snapshot, provider token, renderer object ID, telemetry feed or tile cache.
|
||||
|
||||
## Pin presentation contract v0.1
|
||||
|
||||
map.pin is a reusable interface semantic, not a Cesium entity. The initial canonical variant is elevated-spike, based on the accepted Gelios donor:
|
||||
|
||||
~~~text
|
||||
ground anchor
|
||||
-> coloured stem at a semantic height
|
||||
-> outlined point/head
|
||||
-> shared map.label with explicit anchor and offset
|
||||
-> separate pin and label visibility thresholds
|
||||
~~~
|
||||
|
||||
Studio resolves a selected map.pin style profile into this contract. The renderer adapter decides whether its concrete implementation is a polyline and point, a mesh, a sprite or another primitive. A source payload only identifies the stable moving subject, its position/status and approved label fields; it never specifies renderer primitives or provider-specific IDs.
|
||||
|
||||
## First acceptance slice
|
||||
|
||||
The first fixture-backed Map Page must cover:
|
||||
|
||||
@@ -112,14 +112,14 @@ ENGINE remains workflow/dev environment. It may reference OPS and assistants, bu
|
||||
| `engine.workflow_owner` | ENGINE Workflow Owner | source-confirmed | ENGINE | Owner field in ACL. |
|
||||
| `engine.workflow_share` | ENGINE Workflow Share | source-confirmed | ENGINE | Share/invite/user access routes. |
|
||||
| `engine.workflow_access_request` | ENGINE Workflow Access Request | source-confirmed | ENGINE | Access request route/model for workflow access. |
|
||||
| `engine.workflow_l2` | ENGINE L2 Workflow | source-evidenced | ENGINE / n8n bridge | n8n subworkflow attached to L1 node by `workflowId` + `nodeId`. |
|
||||
| `engine.node_l2` | ENGINE L2 Node | source-evidenced | ENGINE / n8n bridge | Node inside compiled/deployed n8n workflow. |
|
||||
| `engine.l2_runtime_core` | ENGINE L2 Runtime Core | source-evidenced | ENGINE / n8n bridge | Target n8n runtime/core instance. |
|
||||
| `engine.workflow_l2_runtime_id` | ENGINE L2 Runtime Workflow ID | source-confirmed | ENGINE / n8n bridge | n8n runtime workflow id, separate from NodeDC workflow id. |
|
||||
| `engine.l2_execution` | ENGINE L2 Execution | source-confirmed | ENGINE / n8n bridge | n8n execution id mapped to run/execution records. |
|
||||
| `engine.workflow_l2` | ENGINE L2 Workflow | source-evidenced | ENGINE execution bridge | L2 workflow attached to L1 node by `workflowId` + `nodeId`. |
|
||||
| `engine.node_l2` | ENGINE L2 Node | source-evidenced | ENGINE execution bridge | Node inside a compiled/deployed L2 workflow. |
|
||||
| `engine.l2_runtime_core` | ENGINE L2 Runtime Core | source-evidenced | ENGINE execution bridge | Target Engine execution core. |
|
||||
| `engine.workflow_l2_runtime_id` | ENGINE L2 Runtime Workflow ID | source-confirmed | ENGINE execution bridge | L2 runtime workflow id, separate from NodeDC L1 workflow id. |
|
||||
| `engine.l2_execution` | ENGINE L2 Execution | source-confirmed | ENGINE execution bridge | L2 execution id mapped to run/execution records. |
|
||||
| `engine.l2_run` | ENGINE L2 Run | source-confirmed | ENGINE-side OpsLayer | Runtime run id used to bind execution/session. Current implementation is tech-debt-adjacent. |
|
||||
| `engine.l2_session` | ENGINE L2 Session | source-confirmed | ENGINE-side OpsLayer | Runtime session anchor, often derived from run id if upstream omits session id. |
|
||||
| `engine.l2_runtime_event` | ENGINE L2 Runtime Event | source-confirmed | ENGINE / n8n bridge | `workflow:start`, `workflow:finish`, `node:start`, `node:success`, `node:error`. |
|
||||
| `engine.l2_runtime_event` | ENGINE L2 Runtime Event | source-confirmed | ENGINE execution bridge | `workflow:start`, `workflow:finish`, `node:start`, `node:success`, `node:error`. |
|
||||
| `engine.source_stamp` | ENGINE Source Stamp | pending | ENGINE | Candidate for future source/version traceability. |
|
||||
|
||||
## Assistant / AI Workspace
|
||||
@@ -161,4 +161,3 @@ These names identify current working areas that must not leak into canonical cor
|
||||
| `engine.tech_debt.tender_storage` | Tender Storage | tech-debt-noncanonical | ENGINE | Current domain storage in ENGINE-side implementation. |
|
||||
| `engine.tech_debt.tender_ai_review` | Tender AI Review | tech-debt-noncanonical | ENGINE | Current AI review implementation for tender domain. |
|
||||
| `engine.tech_debt.ops_tabs_control` | ENGINE OPS Tabs Control | tech-debt-noncanonical | ENGINE | UI/control surface tied to current OpsLayer experiment. |
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ runtime/data/storage/env/secrets/logs/dumps/node_modules/build outputs
|
||||
.env / .env.*
|
||||
credentials / keys / pem / ssh
|
||||
DB dumps / archives / backup contents
|
||||
postgres/n8n runtime data
|
||||
postgres/Engine L2 runtime data
|
||||
```
|
||||
|
||||
Не менять:
|
||||
@@ -600,7 +600,7 @@ search: /api/workflows/:id/share
|
||||
search: workflow-access-requests
|
||||
search: engine-role-access-requests
|
||||
|
||||
NODEDC/NODEDC_ENGINE_INFRA/nodedc-source/server/routes/n8n.js
|
||||
Engine execution deployment route
|
||||
search: runtimeWorkflowId
|
||||
search: resolveNodeDcByRuntimeWorkflowId
|
||||
search: resolveWorkflowIdByNodeDcBindingRest
|
||||
@@ -608,12 +608,12 @@ search: executionId
|
||||
|
||||
NODEDC/NODEDC_ENGINE_INFRA/nodedc-source/server/ops/runtimeTap.js
|
||||
search: runtimeWorkflowId
|
||||
search: n8nCoreId
|
||||
search: runtime core id
|
||||
search: executionId
|
||||
search: runId
|
||||
search: sessionId
|
||||
|
||||
NODEDC/NODEDC_ENGINE_INFRA/nodedc-source/services/n8n/runtime-plugin/hooks.js
|
||||
Engine execution runtime hooks
|
||||
search: workflow:start
|
||||
search: workflow:finish
|
||||
search: node:start
|
||||
@@ -641,7 +641,7 @@ source-evidenced by previous ENGINE pass; direct links pending.
|
||||
Риск смешения терминов:
|
||||
|
||||
```text
|
||||
n8n names are implementation aliases for Engine L2 runtime, not canonical product names.
|
||||
The Engine L2 runtime is the only canonical product term in ontology and documentation.
|
||||
```
|
||||
|
||||
### 3.9. ENGINE / Assistant P1 — Assistant, Provider, Executor, Bridge
|
||||
|
||||
@@ -13,7 +13,7 @@ This file indexes the source-backed evidence ledgers. It is not a replacement fo
|
||||
| `EVIDENCE_LEDGER_OPS_P0.md` | OPS Product | source-backed P0 | Confirms workspace/project/card model and `ops.card` as canonical work object. |
|
||||
| `EVIDENCE_LEDGER_OPS_GATEWAY_P1.md` | OPS Gateway / MCP access | source-backed P1 | Confirms scoped agent identity/token/grant/scope/tool/idempotency/audit boundary. |
|
||||
| `EVIDENCE_LEDGER_ENGINE_DIRTY_BOUNDARY_P1.md` | ENGINE-side OpsLayer / Agent Monitor / tender tech debt | source-backed P1 | Confirms this is non-canonical tech debt and must not define OPS Product. |
|
||||
| `EVIDENCE_LEDGER_ENGINE_WORKFLOW_P1.md` | ENGINE L1/L2 workflow and runtime evidence | source-backed P1 | Confirms L1 graph, workflow ACL/share, L2 n8n runtime ids/events/run/session bridge. |
|
||||
| `EVIDENCE_LEDGER_ENGINE_WORKFLOW_P1.md` | ENGINE L1/L2 workflow and runtime evidence | source-backed P1 | Confirms L1 graph, workflow ACL/share, L2 runtime ids/events/run/session bridge. |
|
||||
|
||||
## Source Roots Used
|
||||
|
||||
@@ -36,7 +36,7 @@ This file indexes the source-backed evidence ledgers. It is not a replacement fo
|
||||
- ENGINE-side Agent Monitor/tender UI is working tech debt, not target architecture.
|
||||
- ENGINE L1/L2 split is real enough for first ontology implementation:
|
||||
- L1 = NodeDC canvas/workflow graph;
|
||||
- L2 = n8n/subworkflow runtime attached to L1 node.
|
||||
- L2 = Engine execution workflow attached to an L1 node.
|
||||
- Future interface layer is a planned platform service/layer, motivated by current ENGINE-side custom UI debt.
|
||||
|
||||
## Evidence Gaps That Remain Non-Blocking
|
||||
@@ -46,4 +46,3 @@ This file indexes the source-backed evidence ledgers. It is not a replacement fo
|
||||
- `ops.card_type` needs taxonomy.
|
||||
- Gateway pairing/entitlement flow needs route-level hardening.
|
||||
- Domain ontology packages need separate passes; core must not absorb domain-specific tender/ecology/transport concepts.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ OPS product
|
||||
!= ENGINE-side OpsLayer / tender-agent / Agent Monitor
|
||||
```
|
||||
|
||||
The ENGINE repository contains a substantial embedded `OpsLayer` around n8n workflows, tender search/review, monitor profiles, agent runs, trace events, and Agent Monitor UI nodes.
|
||||
The ENGINE repository contains a substantial embedded `OpsLayer` around L2 execution workflows, tender search/review, monitor profiles, agent runs, trace events, and Agent Monitor UI nodes.
|
||||
|
||||
This code is working product/legacy infrastructure, but it must be treated as:
|
||||
|
||||
@@ -43,14 +43,14 @@ It should not be promoted into canonical OPS product truth and should not define
|
||||
| Boundary ID | Canonical treatment | Evidence status | Evidence file:line | Evidence explanation | Notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `engine.tech_debt.ops_layer` | Engine-side legacy OpsLayer | source-confirmed tech debt | `server/index.js:25`, `server/index.js:333`, `server/routes/ops.js:1-34`, `server/routes/ops.js:6412-11855` | ENGINE imports `opsRouter` and mounts it at `/api/ops`; `routes/ops.js` implements a large set of run/result/tender/AI review/monitor endpoints. | This is not the separate OPS product / Tasker model. |
|
||||
| `engine.tech_debt.ops_agent_instance` | Legacy Engine agent instance | source-confirmed tech debt | `server/ops/migrations/0001_init_opslayer.sql:89-137`, `src/store.ts:444-480`, `src/utils/opsApi.ts:759-855` | OpsLayer has `agents`, `agent_instances`, `agent_runs`; frontend prevents copied n8n nodes from inheriting `opsAgentInstanceId`; API can start/stop agent instances and record runs. | Do not merge with OPS Gateway `agent.identity`. |
|
||||
| `engine.tech_debt.ops_agent_instance` | Legacy Engine agent instance | source-confirmed tech debt | `server/ops/migrations/0001_init_opslayer.sql:89-137`, `src/store.ts:444-480`, `src/utils/opsApi.ts:759-855` | OpsLayer has `agents`, `agent_instances`, `agent_runs`; frontend prevents copied L2 nodes from inheriting `opsAgentInstanceId`; API can start/stop agent instances and record runs. | Do not merge with OPS Gateway `agent.identity`. |
|
||||
| `engine.tech_debt.agent_monitor_node` | Agent Monitor canvas node | source-confirmed tech debt | `src/nodes/AgentMonitorNode.tsx:10-14`, `src/nodes/AgentMonitorNode.tsx:21-38`, `src/driveinspector/nodes/AgentMonitor.definition.ts:1-13` | ENGINE has a visible `agentMonitor` React Flow node and inspector definition that binds monitor profiles/agents through `opsApi`. | This is current UI embedding, not final interface layer. |
|
||||
| `engine.tech_debt.monitor_profile` | Engine-side monitor profile | source-confirmed tech debt | `server/ops/migrations/0001_init_opslayer.sql:240-299`, `src/utils/opsApi.ts:260-360`, `src/utils/opsApi.ts:460-520` | OpsLayer stores monitor profiles, profile assignments, bound agent instances, n8n cores, and monitor nodes; frontend can list/create/update/bind profiles and save monitor layouts. | Useful evidence for future work-view requirements. |
|
||||
| `engine.tech_debt.tender_domain_ui` | Tender-specific embedded UI/domain slice | source-confirmed tech debt | `server/routes/ops.js:45-63`, `server/routes/ops.js:75-145`, `src/n8n/N8nAgentMonitorPanel.tsx:1-44`, `src/n8n/N8nAgentMonitorPanel.tsx:1160-1245`, `src/n8n/tenderElectronicPlaceCountries.ts:1-12` | ENGINE includes Kontur tender URLs, tender categories/electronic places, tender search settings, and a large Agent Monitor panel. | Treat as procurement/tender domain evidence, not core ontology. |
|
||||
| `engine.tech_debt.monitor_profile` | Engine-side monitor profile | source-confirmed tech debt | `server/ops/migrations/0001_init_opslayer.sql:240-299`, `src/utils/opsApi.ts:260-360`, `src/utils/opsApi.ts:460-520` | OpsLayer stores monitor profiles, profile assignments, bound agent instances, execution cores, and monitor nodes; frontend can list/create/update/bind profiles and save monitor layouts. | Useful evidence for future work-view requirements. |
|
||||
| `engine.tech_debt.tender_domain_ui` | Tender-specific embedded UI/domain slice | source-confirmed tech debt | Engine internal monitor implementation | ENGINE includes Kontur tender URLs, tender categories/electronic places, tender search settings, and a large Agent Monitor panel. | Treat as procurement/tender domain evidence, not core ontology. |
|
||||
| `engine.tech_debt.tender_storage` | Tender storage inside Engine OpsLayer | source-confirmed tech debt | `server/ops/migrations/0001_init_opslayer.sql:210-239`, `server/ops/migrations/0006_tender_dedup_registry.sql:1-17`, `server/ops/migrations/0007_tender_details.sql:1-24`, `server/ops/migrations/0008_tender_documents.sql:1-37` | OpsLayer stores tender results, dedup registry, details, and downloaded documents. | Do not confuse with future domain ontology persistence. |
|
||||
| `engine.tech_debt.tender_ai_review` | Tender AI review workflow state | source-confirmed tech debt | `server/ops/migrations/0009_tender_ai_review.sql:1-90`, `src/n8n/N8nAgentMonitorPanel.tsx:1214-1238`, `src/utils/opsApi.ts:577-688` | OpsLayer stores AI review tasks/latest state; UI has default manual AI criteria/response format; API enqueues/retries review tasks. | Useful sample for domain-specific assistant review flows. |
|
||||
| `engine.tech_debt.tender_ai_review` | Tender AI review workflow state | source-confirmed tech debt | Engine internal monitor implementation | OpsLayer stores AI review tasks/latest state; UI has default manual AI criteria/response format; API enqueues/retries review tasks. | Useful sample for domain-specific assistant review flows. |
|
||||
| `engine.tech_debt.ops_tabs_control` | Engine inspector ops control | source-confirmed tech debt | `src/driveinspector/ControlRegistry.tsx:27-50` | Drive inspector registers `opsTabs` control. | Another marker that Ops UI is embedded in ENGINE editor controls. |
|
||||
| `future.interface_layer` | Future platform UI/work-view layer | product-required / source-informed | `src/n8n/N8nAgentMonitorPanel.tsx:46-120`, `src/utils/opsApi.ts:460-520`, `server/ops/migrations/0001_init_opslayer.sql:240-299` | Current monitor panel/layout/profile code shows the kind of configurable work views the future interface layer must support. | Future home should be separate platform service/app, not Engine core. |
|
||||
| `future.interface_layer` | Future platform UI/work-view layer | product-required / source-informed | Engine internal monitor implementation; `src/utils/opsApi.ts:460-520`, `server/ops/migrations/0001_init_opslayer.sql:240-299` | Current monitor panel/layout/profile code shows the kind of configurable work views the future interface layer must support. | Future home should be separate platform service/app, not Engine core. |
|
||||
|
||||
## Route / Surface Evidence
|
||||
|
||||
@@ -58,9 +58,9 @@ It should not be promoted into canonical OPS product truth and should not define
|
||||
| --- | --- | --- | --- |
|
||||
| `/api/ops` is mounted inside ENGINE server | source-confirmed | `server/index.js:25`, `server/index.js:333` | ENGINE server imports and mounts `opsRouter`. |
|
||||
| Ops router owns runs/results/tender/AI review/monitor APIs | source-confirmed | `server/routes/ops.js:6412-11855` | Router exposes health, runs, tender results/details/docs, AI review, trace, monitor profiles/layout/admin, agent instance start/stop, actions, contour limits/search. |
|
||||
| Agent Monitor is an ENGINE canvas node | source-confirmed | `src/nodes/AgentMonitorNode.tsx:10-14`, `src/nodes/AgentMonitorNode.tsx:73-170` | Node type is `agentMonitor`; UI opens monitor through n8n subworkflow event. |
|
||||
| Agent Monitor panel directly consumes Ops APIs | source-confirmed | `src/n8n/N8nAgentMonitorPanel.tsx:5-31` | Panel imports many Ops API functions for tender details, AI review, layout, runs, trace, start/stop, take-in-work. |
|
||||
| Engine graph copy logic special-cases Ops bindings | source-confirmed | `src/store.ts:444-480` | Copied n8n nodes clear `opsAgentInstanceId`; copied `agentMonitor` nodes clear monitor profile/binding fields. |
|
||||
| Agent Monitor is an ENGINE canvas node | source-confirmed | `src/nodes/AgentMonitorNode.tsx:10-14`, `src/nodes/AgentMonitorNode.tsx:73-170` | Node type is `agentMonitor`; UI opens monitor through an L2 execution event. |
|
||||
| Agent Monitor panel directly consumes Ops APIs | source-confirmed | Engine internal monitor implementation | Panel imports many Ops API functions for tender details, AI review, layout, runs, trace, start/stop, take-in-work. |
|
||||
| Engine graph copy logic special-cases Ops bindings | source-confirmed | `src/store.ts:444-480` | Copied L2 nodes clear `opsAgentInstanceId`; copied `agentMonitor` nodes clear monitor profile/binding fields. |
|
||||
|
||||
## Guardrails Confirmed
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Status: source-backed ledger section
|
||||
Baseline: Canonical Entity Catalog v0.4-pre
|
||||
Date: 2026-06-18
|
||||
Scope: ENGINE L1/L2 workflow, ACL/share, n8n runtime bridge, runtime event tracking
|
||||
Scope: ENGINE L1/L2 workflow, ACL/share, execution bridge, runtime event tracking
|
||||
|
||||
Source root:
|
||||
|
||||
@@ -25,7 +25,7 @@ This pass confirms the minimum ENGINE ontology needed for the first implementati
|
||||
- `engine.workflow_l1`, `engine.node_l1`, and `engine.edge_l1` exist as NodeDC/React Flow graph concepts.
|
||||
- `engine.node_type_l1` is sourced from the auto node registry.
|
||||
- Workflow ACL/share/access-request concepts are implemented in source.
|
||||
- `engine.workflow_l2` is source-evidenced as an n8n subworkflow attached to an L1 workflow/node pair.
|
||||
- `engine.workflow_l2` is source-evidenced as a protected execution workflow attached to an L1 workflow/node pair.
|
||||
- Runtime events carry `workflowId`, `nodeId`, `runtimeWorkflowId`, `executionId`, `runId`, and `sessionId`.
|
||||
- Current runtime run/session tracking is tied to ENGINE-side OpsLayer tables and should remain tech-debt-adjacent.
|
||||
|
||||
@@ -41,14 +41,14 @@ This pass confirms the minimum ENGINE ontology needed for the first implementati
|
||||
| `engine.workflow_owner` | source-confirmed | `server/workflows/acl.js:109-165` | ACL owner field is normalized and role checked. |
|
||||
| `engine.workflow_share` | source-confirmed | `server/index.js:3185-3245`, `server/index.js:3250-3312`, `server/index.js:3371-3635` | Workflow share routes expose owner/users/groups/invites and email/internal invite operations. |
|
||||
| `engine.workflow_access_request` | source-confirmed | `server/index.js:2792`, `server/index.js:2860`, `server/index.js:2916`, `server/index.js:3314` | Workflow access request, approve, reject, and request routes are present. |
|
||||
| `engine.workflow_l2` | source-evidenced | `server/routes/n8n.js:11661-11737` | `/api/n8n/subworkflow/deploy` loads subworkflow graph for `workflowId` + `nodeId`, compiles it, injects runtime events, and writes compiled n8n workflow. |
|
||||
| `engine.node_l2` | source-evidenced | `server/routes/n8n.js:2288-2365`, `services/n8n/runtime-plugin/hooks.js:132-145` | Runtime event node and hooks capture n8n node name/id. |
|
||||
| `engine.l2_runtime_core` | source-evidenced | `server/routes/n8n.js:11685-11696`, `server/routes/n8n.js:11739-11748` | Deploy resolves n8n target/base URL/instance and docker compose service/container. |
|
||||
| `engine.workflow_l2_runtime_id` | source-confirmed | `services/n8n/runtime-plugin/hooks.js:52-61`, `server/routes/n8n.js:2323-2324`, `server/routes/n8n.js:12041-12055` | Runtime plugin extracts n8n workflow id; injected event uses `$workflow.id`; deploy sync stores resolved workflow id. |
|
||||
| `engine.l2_execution` | source-confirmed | `services/n8n/runtime-plugin/hooks.js:77-86`, `server/routes/n8n.js:2323-2325`, `server/ops/runtimeTap.js:773-818`, `server/ops/runtimeTap.js:820-855` | Runtime events carry execution id; runtime tap writes trace event and run execution binding. |
|
||||
| `engine.l2_run` | source-confirmed / tech-debt-adjacent | `services/n8n/runtime-plugin/hooks.js:88-106`, `server/ops/runtimeTap.js:247-270`, `server/ops/runtimeTap.js:467-542` | Hooks extract run id; runtime tap resolves/canonicalizes run id through ENGINE-side OpsLayer tables. |
|
||||
| `engine.l2_session` | source-confirmed / tech-debt-adjacent | `services/n8n/runtime-plugin/hooks.js:108-130`, `server/ops/runtimeTap.js:272-289`, `server/ops/runtimeTap.js:354-430` | Hooks extract session id or fall back to run id; runtime tap resolves session anchors and can create session-workflow split runs. |
|
||||
| `engine.l2_runtime_event` | source-confirmed | `services/n8n/runtime-plugin/hooks.js:220-231`, `services/n8n/runtime-plugin/hooks.js:233-279`, `services/n8n/runtime-plugin/hooks.js:285-387`, `server/routes/n8n.js:2298-2332`, `services/n8n/runtime-plugin/runtime-bridge.js:200-223` | Runtime events are emitted/enqueued for workflow/node start/finish/success/error and sent to NodeDC runtime endpoint with retry/failure handling. |
|
||||
| `engine.workflow_l2` | source-evidenced | Engine execution deployment route | Deploy loads the L2 graph for `workflowId` + `nodeId`, compiles it, injects runtime events, and writes the execution workflow. |
|
||||
| `engine.node_l2` | source-evidenced | Engine execution hooks | Runtime event hooks capture L2 node name/id. |
|
||||
| `engine.l2_runtime_core` | source-evidenced | Engine execution deployment route | Deploy resolves the execution target/base URL/instance and service/container. |
|
||||
| `engine.workflow_l2_runtime_id` | source-confirmed | Engine execution hooks and deploy sync | Runtime bridge extracts workflow id; injected event uses `$workflow.id`; deploy sync stores the resolved id. |
|
||||
| `engine.l2_execution` | source-confirmed | Engine execution hooks and runtime tap | Runtime events carry execution id; runtime tap writes trace event and run execution binding. |
|
||||
| `engine.l2_run` | source-confirmed / tech-debt-adjacent | Engine execution hooks and runtime tap | Hooks extract run id; runtime tap resolves/canonicalizes it through ENGINE-side OpsLayer tables. |
|
||||
| `engine.l2_session` | source-confirmed / tech-debt-adjacent | Engine execution hooks and runtime tap | Hooks extract session id or fall back to run id; runtime tap resolves session anchors and can create session-workflow split runs. |
|
||||
| `engine.l2_runtime_event` | source-confirmed | Engine execution hooks and runtime bridge | Runtime events are emitted/enqueued for workflow/node start/finish/success/error and sent to NodeDC runtime endpoint with retry/failure handling. |
|
||||
|
||||
## Guardrails Confirmed
|
||||
|
||||
@@ -62,6 +62,5 @@ Engine-side OpsLayer runtime tables do not define OPS Product ontology.
|
||||
## Non-Blocking Follow-Up
|
||||
|
||||
- Harden `engine.source_stamp` after implementation needs are clearer.
|
||||
- Separate stable L2 domain concepts from current n8n-specific deployment helpers.
|
||||
- Separate stable L2 domain concepts from current execution deployment helpers.
|
||||
- Decide whether runtime run/session belongs in future Ontology Core, Interface Layer, OPS Gateway, or a dedicated runtime observability package.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ These rules exist to stop future agents and developers from merging similar-look
|
||||
|
||||
- ENGINE is workflow/dev environment, not the final home for heavy product UIs.
|
||||
- `engine.workflow_l1` is the NodeDC canvas/workflow graph.
|
||||
- `engine.workflow_l2` is a runtime/subworkflow layer, currently n8n-backed.
|
||||
- `engine.workflow_l2` is the protected Engine execution layer.
|
||||
- `engine.workflow_l2_runtime_id` is not the same as NodeDC `engine.workflow_l1` id.
|
||||
- `engine.l2_run`, `engine.l2_session`, and current trace tables are useful evidence but are tech-debt-adjacent because they live in ENGINE-side OpsLayer.
|
||||
- Do not turn ENGINE-side Agent Monitor/tender UI into core ontology roots.
|
||||
@@ -74,4 +74,3 @@ These rules exist to stop future agents and developers from merging similar-look
|
||||
- Do not delete files unless explicitly asked by the user.
|
||||
- Do not edit application source code from the ontology workspace.
|
||||
- Do not run Docker/build/install/test for docs-only ontology work.
|
||||
|
||||
|
||||
@@ -68,10 +68,10 @@ Relations are written as product-level semantics, not database foreign keys. Imp
|
||||
| `engine.workflow_acl.has_owner` | `engine.workflow_acl` | `engine.workflow_owner` | ACL owns owner field. | source-confirmed |
|
||||
| `engine.workflow_l1.has_share` | `engine.workflow_l1` | `engine.workflow_share` | Workflow can be shared/invited. | source-confirmed |
|
||||
| `engine.workflow_l1.has_access_request` | `engine.workflow_l1` | `engine.workflow_access_request` | Access requests target workflow. | source-confirmed |
|
||||
| `engine.node_l1.embeds_l2_workflow` | `engine.node_l1` | `engine.workflow_l2` | n8n subworkflow is attached to L1 node. | source-evidenced |
|
||||
| `engine.workflow_l2.deployed_as_runtime_workflow` | `engine.workflow_l2` | `engine.workflow_l2_runtime_id` | Compiled n8n workflow deploy returns runtime workflow id. | source-confirmed |
|
||||
| `engine.workflow_l2.runs_on_runtime_core` | `engine.workflow_l2` | `engine.l2_runtime_core` | L2 workflow targets n8n instance/core. | source-evidenced |
|
||||
| `engine.l2_execution.belongs_to_runtime_workflow` | `engine.l2_execution` | `engine.workflow_l2_runtime_id` | Execution is emitted for n8n workflow id. | source-confirmed |
|
||||
| `engine.node_l1.embeds_l2_workflow` | `engine.node_l1` | `engine.workflow_l2` | L2 execution workflow is attached to an L1 node. | source-evidenced |
|
||||
| `engine.workflow_l2.deployed_as_runtime_workflow` | `engine.workflow_l2` | `engine.workflow_l2_runtime_id` | L2 deploy returns a runtime workflow id. | source-confirmed |
|
||||
| `engine.workflow_l2.runs_on_runtime_core` | `engine.workflow_l2` | `engine.l2_runtime_core` | L2 workflow targets the Engine execution core. | source-evidenced |
|
||||
| `engine.l2_execution.belongs_to_runtime_workflow` | `engine.l2_execution` | `engine.workflow_l2_runtime_id` | Execution is emitted for the L2 runtime workflow id. | source-confirmed |
|
||||
| `engine.l2_runtime_event.describes_execution` | `engine.l2_runtime_event` | `engine.l2_execution` | Runtime events carry execution id. | source-confirmed |
|
||||
| `engine.l2_runtime_event.binds_run` | `engine.l2_runtime_event` | `engine.l2_run` | Event binds to run id. Current storage is ENGINE-side OpsLayer. | source-confirmed / tech-debt-adjacent |
|
||||
| `engine.l2_run.has_session` | `engine.l2_run` | `engine.l2_session` | Session anchors related runtime events/runs. | source-confirmed / tech-debt-adjacent |
|
||||
@@ -87,4 +87,3 @@ These are the first useful implementation targets for Ontology Core.
|
||||
| `ontology.resolves_hub_app_to_project_context` | `hub.application` | `ops.project` / `engine.workflow_l1` | Application selection can suggest project/workflow context. | product-required |
|
||||
| `ontology.selects_gateway_grant_context` | `assistant.bridge` | `agent.grant` | Ontology can help choose proper MCP/Gateway grant before tool calls. | product-required |
|
||||
| `ontology.routes_interface_view_to_domain_package` | `future.interface_view` | `future.domain_package` | Future UI layer renders domain-specific views from ontology-backed data. | future-concept |
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ Date: 2026-06-18
|
||||
| Agent token | Opaque credential used by Gateway; not user identity. |
|
||||
| ENGINE | Workflow/dev environment. |
|
||||
| L1 workflow | NodeDC/React Flow canvas workflow. |
|
||||
| L2 workflow | Runtime/subworkflow layer, currently n8n-backed. |
|
||||
| Runtime workflow id | n8n runtime workflow id, separate from NodeDC L1 workflow id. |
|
||||
| L2 workflow | Protected Engine execution workflow layer. |
|
||||
| Runtime workflow id | Engine L2 runtime workflow id, separate from NodeDC L1 workflow id. |
|
||||
| Runtime event | Event emitted by L2 runtime: workflow/node start/finish/success/error. |
|
||||
| Assistant | Product assistant capability; exact source model pending AI Workspace pass. |
|
||||
| Ontology Core | Future platform service/module for canonical entities, relations, aliases, evidence, and resolver rules. |
|
||||
@@ -34,8 +34,8 @@ Date: 2026-06-18
|
||||
| Authentik user / OIDC subject / JWT subject | `ndcauth.identity` |
|
||||
| Launcher service / app | `hub.application` |
|
||||
| Application tile / app card | `hub.application_card` |
|
||||
| n8n workflow id | `engine.workflow_l2_runtime_id` |
|
||||
| n8n execution | `engine.l2_execution` |
|
||||
| runtime workflow id | `engine.workflow_l2_runtime_id` |
|
||||
| L2 execution | `engine.l2_execution` |
|
||||
| Engine-side OPS / Agent Monitor OPS | `engine.tech_debt.ops_layer`, not OPS Product |
|
||||
| Tender Agent Monitor UI | `engine.tech_debt.tender_domain_ui`, not future Interface Layer |
|
||||
|
||||
@@ -46,4 +46,3 @@ Date: 2026-06-18
|
||||
- Do not say "user" when the context needs HUB user, NDCAuth identity, OPS member, or Gateway agent identity.
|
||||
- Do not say "task" as the root concept when the object is an OPS card.
|
||||
- Do not treat current tender/Agent Monitor implementation as target architecture.
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"id": "example-workflow-id",
|
||||
"label": "Example Engine Workflow",
|
||||
"nodeId": "example-node-id",
|
||||
"runtimeWorkflowId": "example-n8n-runtime-workflow-id",
|
||||
"n8nInstanceId": "example-n8n-instance"
|
||||
"runtimeWorkflowId": "example-runtime-workflow-id",
|
||||
"runtimeInstanceId": "example-engine-runtime-instance"
|
||||
},
|
||||
"context": {
|
||||
"status": "planned",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"contractVersion": "0.1.0",
|
||||
"kind": "map.moving_object.current_position",
|
||||
"source": "gelios",
|
||||
"subjectId": "gelios.unit:example-unit-id",
|
||||
"observedAt": "2026-07-13T00:00:00.000Z",
|
||||
"receivedAt": "2026-07-13T00:00:03.000Z",
|
||||
"position": {
|
||||
"latitude": 55.000001,
|
||||
"longitude": 37.000001,
|
||||
"heightMeters": 0,
|
||||
"courseDegrees": 90,
|
||||
"speedKph": 0,
|
||||
"satellites": 0
|
||||
},
|
||||
"operationalStatus": "unknown",
|
||||
"display": {
|
||||
"label": "Robot2B unit",
|
||||
"visibilityProfile": "fleet-default"
|
||||
},
|
||||
"policy": {
|
||||
"scopeApproved": true,
|
||||
"fieldSet": "studio-current-position-v1"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/mcp-server.mjs",
|
||||
"assistant:action": "node src/assistant-action-resolver.mjs",
|
||||
"assistant:caller": "node src/assistant-action-caller.mjs",
|
||||
"assistant:execute": "node src/assistant-action-executor.mjs",
|
||||
@@ -17,6 +18,7 @@
|
||||
"smoke:assistant-executor": "node src/assistant-action-executor.mjs --smoke",
|
||||
"smoke:assistant-actions": "node src/assistant-action-resolver.mjs --smoke",
|
||||
"smoke:assistant-policy": "node src/assistant-policy.mjs --smoke",
|
||||
"smoke:mcp": "node src/scripts/smoke-mcp.mjs",
|
||||
"smoke:resolver": "node src/resolver.mjs --smoke"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import { fileURLToPath } from 'node:url'
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
export const serviceRoot = path.resolve(__dirname, '..')
|
||||
export const catalogRoot = path.join(serviceRoot, 'catalog')
|
||||
export const catalogRoot = path.resolve(
|
||||
process.env.NODEDC_ONTOLOGY_CATALOG_ROOT || path.join(serviceRoot, 'catalog'),
|
||||
)
|
||||
export const domainPackagesRoot = path.join(catalogRoot, 'domain-packages')
|
||||
|
||||
export async function readJson(relativePath) {
|
||||
const fullPath = path.join(serviceRoot, relativePath)
|
||||
const relative = String(relativePath || '').replace(/^catalog\//, '')
|
||||
const fullPath = path.join(catalogRoot, relative)
|
||||
const raw = await fs.readFile(fullPath, 'utf8')
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
@@ -180,7 +183,7 @@ function mergeCatalog(base, extension) {
|
||||
}
|
||||
|
||||
async function loadDomainPackage(packageDir) {
|
||||
const relativeDir = path.relative(serviceRoot, packageDir)
|
||||
const relativeDir = path.relative(catalogRoot, packageDir)
|
||||
const metadata = await readOptionalJsonAt(path.join(packageDir, 'package.json'), {})
|
||||
|
||||
return {
|
||||
@@ -225,6 +228,8 @@ async function loadDomainPackage(packageDir) {
|
||||
}),
|
||||
domainPackages: [{
|
||||
id: metadata.id || path.basename(packageDir),
|
||||
version: metadata.version || '',
|
||||
updatedAt: metadata.updatedAt || '',
|
||||
status: metadata.status || 'unknown',
|
||||
relativePath: relativeDir,
|
||||
summary: metadata.summary || '',
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash, timingSafeEqual } from 'node:crypto'
|
||||
import { createServer } from 'node:http'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { canonicalizeEntityId, loadCatalog } from './catalog.mjs'
|
||||
import { resolveContext } from './resolver.mjs'
|
||||
|
||||
const MCP_PROTOCOL_VERSION = '2025-06-18'
|
||||
const MAX_BODY_BYTES = 1024 * 1024
|
||||
const MAX_SEARCH_RESULTS = 50
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'ontology_status',
|
||||
title: 'Ontology catalog status',
|
||||
description: 'Returns a safe, read-only summary of the live NODE.DC ontology catalog and its loaded domain packages.',
|
||||
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
},
|
||||
{
|
||||
name: 'ontology_search',
|
||||
title: 'Search ontology catalog',
|
||||
description: 'Searches canonical entities, relations and aliases in the live ontology catalog. Use this before introducing names or data contracts into a workflow.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['query'],
|
||||
properties: {
|
||||
query: { type: 'string', minLength: 1, maxLength: 200 },
|
||||
limit: { type: 'integer', minimum: 1, maximum: MAX_SEARCH_RESULTS },
|
||||
},
|
||||
},
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
},
|
||||
{
|
||||
name: 'ontology_get_entity',
|
||||
title: 'Get ontology entity',
|
||||
description: 'Resolves an entity id or alias and returns its safe definition, aliases, relations and applicable guardrails. It never returns runtime data, credentials or source filesystem paths.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
entityId: { type: 'string', minLength: 1, maxLength: 240 },
|
||||
term: { type: 'string', minLength: 1, maxLength: 240 },
|
||||
},
|
||||
anyOf: [{ required: ['entityId'] }, { required: ['term'] }],
|
||||
},
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
},
|
||||
{
|
||||
name: 'ontology_get_guardrails',
|
||||
title: 'Get ontology guardrails',
|
||||
description: 'Returns semantic and safety guardrails for the full catalog or for one entity/alias. This is read-only advice; it does not grant capability access.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
entityId: { type: 'string', minLength: 1, maxLength: 240 },
|
||||
term: { type: 'string', minLength: 1, maxLength: 240 },
|
||||
},
|
||||
},
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
},
|
||||
{
|
||||
name: 'ontology_resolve_context',
|
||||
title: 'Resolve ontology context',
|
||||
description: 'Resolves a semantic request to canonical entities and context routes. It deliberately returns no source-system ids, secrets, runtime data or execution capability.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
surface: { type: 'string', maxLength: 120 },
|
||||
entityId: { type: 'string', maxLength: 240 },
|
||||
alias: { type: 'string', maxLength: 240 },
|
||||
term: { type: 'string', maxLength: 240 },
|
||||
intent: { type: 'string', maxLength: 1000 },
|
||||
contextId: { type: 'string', maxLength: 240 },
|
||||
},
|
||||
},
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
},
|
||||
]
|
||||
|
||||
export function createOntologyMcpServer(overrides = {}) {
|
||||
const config = { ...readConfig(), ...overrides }
|
||||
return createServer((req, res) => handleRequest(req, res, config).catch((error) => {
|
||||
if (!res.headersSent) {
|
||||
sendJson(res, 500, { ok: false, error: 'ontology_mcp_internal_error' })
|
||||
} else {
|
||||
res.destroy(error)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleRequest(req, res, config) {
|
||||
const url = new URL(req.url || '/', 'http://localhost')
|
||||
if (url.pathname === '/healthz') {
|
||||
const catalog = await loadCatalog()
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
service: 'nodedc-ontology-core-mcp',
|
||||
mcp: {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
tools: TOOLS.length,
|
||||
readOnly: true,
|
||||
},
|
||||
catalog: catalogStatus(catalog),
|
||||
internalApiConfigured: config.internalTokens.length > 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname !== '/mcp') {
|
||||
sendJson(res, 404, { ok: false, error: 'not_found' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAllowedOrigin(req.headers.origin, config.allowedOrigins)) {
|
||||
sendJson(res, 403, { ok: false, error: 'mcp_origin_forbidden' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAuthorized(req.headers.authorization, config.internalTokens)) {
|
||||
sendJson(res, config.internalTokens.length ? 401 : 503, {
|
||||
ok: false,
|
||||
error: config.internalTokens.length ? 'ontology_mcp_unauthorized' : 'ontology_mcp_token_not_configured',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const method = String(req.method || 'GET').toUpperCase()
|
||||
if (method === 'GET') {
|
||||
res.setHeader('allow', 'POST')
|
||||
sendJson(res, 405, { ok: false, error: 'mcp_sse_not_supported' })
|
||||
return
|
||||
}
|
||||
if (method !== 'POST') {
|
||||
res.setHeader('allow', 'POST')
|
||||
sendJson(res, 405, { ok: false, error: 'method_not_allowed' })
|
||||
return
|
||||
}
|
||||
|
||||
let payload
|
||||
try {
|
||||
payload = JSON.parse((await readRequestBody(req, config.maxBodyBytes)).toString('utf8'))
|
||||
} catch (error) {
|
||||
sendMcpError(res, null, -32700, error?.code === 'body_too_large' ? 'request_too_large' : 'parse_error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPlainObject(payload) || payload.jsonrpc !== '2.0' || !cleanString(payload.method, 160)) {
|
||||
sendMcpError(res, isPlainObject(payload) ? payload.id ?? null : null, -32600, 'invalid_request')
|
||||
return
|
||||
}
|
||||
|
||||
const isNotification = !Object.hasOwn(payload, 'id')
|
||||
const response = await dispatchMcpRequest(payload)
|
||||
if (isNotification) {
|
||||
res.statusCode = 202
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
sendJson(res, 200, response, { 'mcp-protocol-version': MCP_PROTOCOL_VERSION })
|
||||
}
|
||||
|
||||
async function dispatchMcpRequest(payload) {
|
||||
const id = payload.id ?? null
|
||||
const method = cleanString(payload.method, 160)
|
||||
const params = isPlainObject(payload.params) ? payload.params : {}
|
||||
|
||||
if (method === 'initialize') {
|
||||
return mcpResult(id, {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
serverInfo: {
|
||||
name: 'nodedc-ontology-core',
|
||||
version: '0.1.0',
|
||||
},
|
||||
instructions: 'Read-only semantic catalog. It exposes no runtime telemetry, database access, credentials, source paths, workflow mutations, command dispatch or Studio rendering controls.',
|
||||
})
|
||||
}
|
||||
|
||||
if (method === 'notifications/initialized') return mcpResult(id, {})
|
||||
if (method === 'ping') return mcpResult(id, {})
|
||||
if (method === 'tools/list') return mcpResult(id, { tools: TOOLS })
|
||||
if (method !== 'tools/call') return mcpError(id, -32601, 'method_not_found')
|
||||
|
||||
const toolName = cleanString(params.name, 120)
|
||||
const argumentsValue = isPlainObject(params.arguments) ? params.arguments : {}
|
||||
return mcpResult(id, await callTool(toolName, argumentsValue))
|
||||
}
|
||||
|
||||
async function callTool(name, input) {
|
||||
if (name === 'ontology_status') {
|
||||
const catalog = await loadCatalog()
|
||||
return toolResult({
|
||||
schemaVersion: 'nodedc.ontology-mcp.status.v1',
|
||||
readOnly: true,
|
||||
catalog: catalogStatus(catalog),
|
||||
})
|
||||
}
|
||||
|
||||
if (name === 'ontology_search') {
|
||||
const query = cleanString(input.query, 200)
|
||||
if (!query) return toolError('query_required')
|
||||
const catalog = await loadCatalog()
|
||||
return toolResult(searchCatalog(catalog, query, boundedLimit(input.limit)))
|
||||
}
|
||||
|
||||
if (name === 'ontology_get_entity') {
|
||||
const catalog = await loadCatalog()
|
||||
const requested = cleanString(input.entityId || input.term, 240)
|
||||
const entityId = canonicalizeEntityId(requested, catalog)
|
||||
const entity = catalog.entityById.get(entityId)
|
||||
if (!entity) return toolError('entity_not_found', { requested })
|
||||
return toolResult(entityDetails(catalog, entity))
|
||||
}
|
||||
|
||||
if (name === 'ontology_get_guardrails') {
|
||||
const catalog = await loadCatalog()
|
||||
const requested = cleanString(input.entityId || input.term, 240)
|
||||
const entityId = requested ? canonicalizeEntityId(requested, catalog) : ''
|
||||
if (requested && !catalog.entityById.has(entityId)) return toolError('entity_not_found', { requested })
|
||||
return toolResult(guardrailDetails(catalog, entityId))
|
||||
}
|
||||
|
||||
if (name === 'ontology_resolve_context') {
|
||||
const resolved = await resolveContext({
|
||||
surface: cleanString(input.surface, 120),
|
||||
entityId: cleanString(input.entityId, 240),
|
||||
alias: cleanString(input.alias, 240),
|
||||
term: cleanString(input.term, 240),
|
||||
intent: cleanString(input.intent, 1000),
|
||||
contextId: cleanString(input.contextId, 240),
|
||||
})
|
||||
return toolResult(sanitizeResolvedContext(resolved))
|
||||
}
|
||||
|
||||
return toolError('tool_not_found', { name })
|
||||
}
|
||||
|
||||
function catalogStatus(catalog) {
|
||||
const packages = (catalog.domainPackages || [])
|
||||
.map((item) => ({
|
||||
id: cleanString(item.id, 120),
|
||||
version: cleanString(item.version, 80),
|
||||
updatedAt: cleanString(item.updatedAt, 80),
|
||||
status: cleanString(item.status, 120),
|
||||
summary: cleanString(item.summary, 1000),
|
||||
}))
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
const snapshot = {
|
||||
entityIds: (catalog.entities?.entities || []).map((item) => item.id).sort(),
|
||||
relationIds: (catalog.relations?.relations || []).map((item) => item.id).sort(),
|
||||
aliases: (catalog.aliases?.aliases || []).map((item) => [item.alias, item.canonicalId]).sort((left, right) => String(left[0]).localeCompare(String(right[0]))),
|
||||
packages,
|
||||
}
|
||||
return {
|
||||
schemaVersion: 'nodedc.ontology-catalog.status.v1',
|
||||
catalogHash: createHash('sha256').update(JSON.stringify(snapshot)).digest('hex').slice(0, 16),
|
||||
entities: snapshot.entityIds.length,
|
||||
relations: snapshot.relationIds.length,
|
||||
aliases: snapshot.aliases.length,
|
||||
guardrails: (catalog.guardrails?.rules || []).length,
|
||||
blockedConflations: (catalog.guardrails?.blockedConflations || []).length,
|
||||
domainPackages: packages,
|
||||
}
|
||||
}
|
||||
|
||||
function searchCatalog(catalog, query, limit) {
|
||||
const needle = normalizeSearch(query)
|
||||
const items = []
|
||||
for (const entity of catalog.entities?.entities || []) {
|
||||
const aliasScore = (catalog.aliases?.aliases || [])
|
||||
.filter((alias) => alias.canonicalId === entity.id)
|
||||
.reduce((best, alias) => Math.max(best, searchScore(needle, alias.alias)), 0)
|
||||
const score = Math.max(
|
||||
aliasScore,
|
||||
searchScore(needle, entity.id, entity.name, entity.summary, entity.surface, entity.authority),
|
||||
)
|
||||
if (score > 0) items.push({ kind: 'entity', score, entity: safeEntity(entity) })
|
||||
}
|
||||
for (const relation of catalog.relations?.relations || []) {
|
||||
const score = searchScore(needle, relation.id, relation.summary, ...(relation.from || []), ...(relation.to || []))
|
||||
if (score > 0) items.push({ kind: 'relation', score, relation: safeRelation(relation) })
|
||||
}
|
||||
for (const alias of catalog.aliases?.aliases || []) {
|
||||
const score = searchScore(needle, alias.alias, alias.canonicalId)
|
||||
if (score > 0) items.push({ kind: 'alias', score, alias: safeAlias(alias) })
|
||||
}
|
||||
items.sort((left, right) => right.score - left.score || stableItemId(left).localeCompare(stableItemId(right)))
|
||||
return {
|
||||
schemaVersion: 'nodedc.ontology-mcp.search.v1',
|
||||
query,
|
||||
totalMatches: items.length,
|
||||
results: items.slice(0, limit).map(({ score, ...item }) => item),
|
||||
}
|
||||
}
|
||||
|
||||
function entityDetails(catalog, entity) {
|
||||
const entityId = entity.id
|
||||
return {
|
||||
schemaVersion: 'nodedc.ontology-mcp.entity.v1',
|
||||
entity: safeEntity(entity),
|
||||
aliases: (catalog.aliases?.aliases || [])
|
||||
.filter((item) => item.canonicalId === entityId)
|
||||
.map(safeAlias)
|
||||
.sort((left, right) => left.alias.localeCompare(right.alias)),
|
||||
relations: (catalog.relations?.relations || [])
|
||||
.filter((item) => (item.from || []).includes(entityId) || (item.to || []).includes(entityId))
|
||||
.map(safeRelation)
|
||||
.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
guardrails: (catalog.guardrails?.rules || [])
|
||||
.filter((item) => (item.entityIds || []).includes(entityId))
|
||||
.map(safeGuardrail)
|
||||
.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
blockedConflations: (catalog.guardrails?.blockedConflations || [])
|
||||
.filter((pair) => Array.isArray(pair) && pair.includes(entityId))
|
||||
.map((pair) => pair.map((item) => cleanString(item, 240))),
|
||||
}
|
||||
}
|
||||
|
||||
function guardrailDetails(catalog, entityId) {
|
||||
const rules = (catalog.guardrails?.rules || [])
|
||||
.filter((item) => !entityId || (item.entityIds || []).includes(entityId))
|
||||
.map(safeGuardrail)
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
const blockedConflations = (catalog.guardrails?.blockedConflations || [])
|
||||
.filter((pair) => !entityId || (Array.isArray(pair) && pair.includes(entityId)))
|
||||
.map((pair) => Array.isArray(pair) ? pair.map((item) => cleanString(item, 240)) : [])
|
||||
return {
|
||||
schemaVersion: 'nodedc.ontology-mcp.guardrails.v1',
|
||||
entityId: entityId || null,
|
||||
rules,
|
||||
blockedConflations,
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeResolvedContext(resolved) {
|
||||
return {
|
||||
schemaVersion: 'nodedc.ontology-mcp.context-resolution.v1',
|
||||
input: {
|
||||
surface: cleanString(resolved?.input?.surface, 120),
|
||||
entityId: cleanString(resolved?.input?.entityId, 240),
|
||||
intent: cleanString(resolved?.input?.intent, 1000),
|
||||
},
|
||||
canonicalEntity: resolved?.canonicalEntity ? safeEntity(resolved.canonicalEntity) : null,
|
||||
matchedContexts: (resolved?.matchedContexts || []).map((item) => ({
|
||||
surface: cleanString(item.surface, 120),
|
||||
entityId: cleanString(item.entityId, 240),
|
||||
label: cleanString(item.label, 1000),
|
||||
sourceSystem: cleanString(item.sourceSystem, 160),
|
||||
status: cleanString(item.status, 120),
|
||||
})),
|
||||
bindings: (resolved?.bindings || []).map((item) => ({
|
||||
typeId: cleanString(item.typeId, 240),
|
||||
status: cleanString(item.status, 120),
|
||||
relationId: cleanString(item.relationId, 240),
|
||||
summary: cleanString(item.summary, 1000),
|
||||
})),
|
||||
missingBindingTypes: (resolved?.missingBindingTypes || []).map((item) => ({
|
||||
id: cleanString(item.id, 240),
|
||||
relationId: cleanString(item.relationId, 240),
|
||||
summary: cleanString(item.summary, 1000),
|
||||
})),
|
||||
selectedRule: resolved?.selectedRule ? {
|
||||
id: cleanString(resolved.selectedRule.id, 240),
|
||||
fromSurface: cleanString(resolved.selectedRule.fromSurface, 120),
|
||||
outputSurface: cleanString(resolved.selectedRule.outputSurface, 120),
|
||||
inputEntityIds: stringList(resolved.selectedRule.inputEntityIds),
|
||||
outputEntityIds: stringList(resolved.selectedRule.outputEntityIds),
|
||||
requiredBindings: stringList(resolved.selectedRule.requiredBindings),
|
||||
summary: cleanString(resolved.selectedRule.summary, 1000),
|
||||
} : null,
|
||||
candidates: (resolved?.candidates || []).map((item) => ({
|
||||
id: cleanString(item.id, 240),
|
||||
score: Number.isFinite(Number(item.score)) ? Number(item.score) : 0,
|
||||
outputSurface: cleanString(item.outputSurface, 120),
|
||||
outputEntityIds: stringList(item.outputEntityIds),
|
||||
requiredBindings: stringList(item.requiredBindings),
|
||||
summary: cleanString(item.summary, 1000),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function safeEntity(value) {
|
||||
return {
|
||||
id: cleanString(value?.id, 240),
|
||||
name: cleanString(value?.name, 240),
|
||||
surface: cleanString(value?.surface, 120),
|
||||
status: stringList(value?.status),
|
||||
authority: cleanString(value?.authority, 240),
|
||||
summary: cleanString(value?.summary, 2000),
|
||||
}
|
||||
}
|
||||
|
||||
function safeRelation(value) {
|
||||
return {
|
||||
id: cleanString(value?.id, 240),
|
||||
from: stringList(value?.from),
|
||||
to: stringList(value?.to),
|
||||
status: cleanString(value?.status, 120),
|
||||
summary: cleanString(value?.summary, 2000),
|
||||
}
|
||||
}
|
||||
|
||||
function safeAlias(value) {
|
||||
return {
|
||||
alias: cleanString(value?.alias, 240),
|
||||
canonicalId: cleanString(value?.canonicalId, 240),
|
||||
}
|
||||
}
|
||||
|
||||
function safeGuardrail(value) {
|
||||
return {
|
||||
id: cleanString(value?.id, 240),
|
||||
severity: cleanString(value?.severity, 40),
|
||||
summary: cleanString(value?.summary, 2000),
|
||||
entityIds: stringList(value?.entityIds),
|
||||
}
|
||||
}
|
||||
|
||||
function toolResult(value) {
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
structuredContent: value,
|
||||
}
|
||||
}
|
||||
|
||||
function toolError(error, details = {}) {
|
||||
const value = { ok: false, error, ...details }
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
structuredContent: value,
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
function mcpResult(id, result) {
|
||||
return { jsonrpc: '2.0', id, result }
|
||||
}
|
||||
|
||||
function mcpError(id, code, message) {
|
||||
return { jsonrpc: '2.0', id, error: { code, message } }
|
||||
}
|
||||
|
||||
function sendMcpError(res, id, code, message) {
|
||||
sendJson(res, 200, mcpError(id, code, message), { 'mcp-protocol-version': MCP_PROTOCOL_VERSION })
|
||||
}
|
||||
|
||||
function sendJson(res, status, body, headers = {}) {
|
||||
res.statusCode = status
|
||||
res.setHeader('content-type', 'application/json; charset=utf-8')
|
||||
res.setHeader('cache-control', 'no-store')
|
||||
for (const [key, value] of Object.entries(headers)) res.setHeader(key, value)
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
|
||||
async function readRequestBody(req, maxBytes) {
|
||||
const chunks = []
|
||||
let size = 0
|
||||
for await (const chunk of req) {
|
||||
size += chunk.length
|
||||
if (size > maxBytes) {
|
||||
const error = new Error('body_too_large')
|
||||
error.code = 'body_too_large'
|
||||
throw error
|
||||
}
|
||||
chunks.push(chunk)
|
||||
}
|
||||
return Buffer.concat(chunks)
|
||||
}
|
||||
|
||||
function isAuthorized(header, candidates) {
|
||||
const match = String(header || '').match(/^Bearer\s+(.+)$/i)
|
||||
const token = match?.[1]?.trim() || ''
|
||||
if (!token) return false
|
||||
return (candidates || []).some((candidate) => safeEqual(token, candidate))
|
||||
}
|
||||
|
||||
function safeEqual(left, right) {
|
||||
const leftBuffer = Buffer.from(String(left || ''))
|
||||
const rightBuffer = Buffer.from(String(right || ''))
|
||||
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer)
|
||||
}
|
||||
|
||||
function isAllowedOrigin(origin, allowedOrigins) {
|
||||
if (!origin) return true
|
||||
const normalized = normalizeOrigin(origin)
|
||||
return Boolean(normalized && allowedOrigins.includes(normalized))
|
||||
}
|
||||
|
||||
function normalizeOrigin(value) {
|
||||
try {
|
||||
const url = new URL(String(value || ''))
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
|
||||
return url.origin
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function readConfig() {
|
||||
const internalTokens = uniqueStrings([
|
||||
process.env.ONTOLOGY_CORE_TOKEN,
|
||||
process.env.NODEDC_INTERNAL_ACCESS_TOKEN,
|
||||
process.env.NODEDC_PLATFORM_SERVICE_TOKEN,
|
||||
])
|
||||
return {
|
||||
port: boundedNumber(process.env.PORT || process.env.ONTOLOGY_CORE_PORT, 18104, 1, 65535),
|
||||
internalTokens,
|
||||
allowedOrigins: uniqueStrings(String(process.env.ONTOLOGY_MCP_ALLOWED_ORIGINS || '')
|
||||
.split(/[\n,;]+/)
|
||||
.map(normalizeOrigin)
|
||||
.filter(Boolean)),
|
||||
maxBodyBytes: boundedNumber(process.env.ONTOLOGY_MCP_MAX_BODY_BYTES, MAX_BODY_BYTES, 1024, 10 * 1024 * 1024),
|
||||
}
|
||||
}
|
||||
|
||||
function boundedLimit(value) {
|
||||
return boundedNumber(value, 20, 1, MAX_SEARCH_RESULTS)
|
||||
}
|
||||
|
||||
function boundedNumber(value, fallback, min, max) {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return fallback
|
||||
return Math.min(Math.max(Math.trunc(numeric), min), max)
|
||||
}
|
||||
|
||||
function normalizeSearch(value) {
|
||||
return String(value || '').trim().toLocaleLowerCase('ru-RU')
|
||||
}
|
||||
|
||||
function searchScore(needle, ...values) {
|
||||
let score = 0
|
||||
for (const value of values) {
|
||||
const haystack = normalizeSearch(value)
|
||||
if (!haystack) continue
|
||||
if (haystack === needle) score = Math.max(score, 100)
|
||||
else if (haystack.startsWith(needle)) score = Math.max(score, 60)
|
||||
else if (haystack.includes(needle)) score = Math.max(score, 30)
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
function stableItemId(item) {
|
||||
return item.entity?.id || item.relation?.id || item.alias?.alias || ''
|
||||
}
|
||||
|
||||
function stringList(value) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => cleanString(item, 240)).filter(Boolean)
|
||||
}
|
||||
|
||||
function cleanString(value, maxLength) {
|
||||
return String(value || '').trim().slice(0, maxLength)
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
return [...new Set((values || []).map((item) => cleanString(item, 4000)).filter(Boolean))]
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
const currentFile = fileURLToPath(import.meta.url)
|
||||
const isEntrypoint = process.argv[1] && path.resolve(process.argv[1]) === currentFile
|
||||
|
||||
if (isEntrypoint) {
|
||||
const config = readConfig()
|
||||
const server = createOntologyMcpServer(config)
|
||||
server.listen(config.port, '0.0.0.0', () => {
|
||||
console.log(`NODE.DC Ontology Core MCP listening on http://0.0.0.0:${config.port}`)
|
||||
})
|
||||
const shutdown = () => server.close(() => process.exit(0))
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('SIGINT', shutdown)
|
||||
}
|
||||
@@ -191,7 +191,7 @@ function buildEngineContextFromManifest(manifest) {
|
||||
}
|
||||
if (workflow.nodeId) refs.node_id = String(workflow.nodeId)
|
||||
if (workflow.runtimeWorkflowId) refs.runtime_workflow_id = String(workflow.runtimeWorkflowId)
|
||||
if (workflow.n8nInstanceId) refs.n8n_instance_id = String(workflow.n8nInstanceId)
|
||||
if (workflow.runtimeInstanceId) refs.runtime_instance_id = String(workflow.runtimeInstanceId)
|
||||
|
||||
return {
|
||||
id: `context.engine.${slugPart(workflow.id)}`,
|
||||
|
||||
@@ -185,7 +185,7 @@ async function runSmoke() {
|
||||
},
|
||||
{ surface: 'engine', entityId: 'engine.workflow_l1', intent: 'создай карточку в ops по текущему workflow' },
|
||||
{ surface: 'assistant', entityId: 'assistant.bridge', intent: 'select mcp gateway grant and tool' },
|
||||
{ term: 'n8n workflow id', intent: 'runtime workflow id lookup' },
|
||||
{ term: 'runtime workflow id', intent: 'runtime workflow id lookup' },
|
||||
]
|
||||
|
||||
const results = []
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { createOntologyMcpServer } from '../mcp-server.mjs'
|
||||
|
||||
const TOKEN = 'ontology-mcp-smoke-token'
|
||||
const server = createOntologyMcpServer({
|
||||
internalTokens: [TOKEN],
|
||||
allowedOrigins: [],
|
||||
maxBodyBytes: 1024 * 1024,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`
|
||||
|
||||
try {
|
||||
const health = await fetch(`${baseUrl}/healthz`)
|
||||
const healthPayload = await health.json()
|
||||
assert.equal(health.ok, true)
|
||||
assert.equal(healthPayload.ok, true)
|
||||
assert.equal(healthPayload.mcp.readOnly, true)
|
||||
|
||||
const initialized = await rpc(baseUrl, TOKEN, 1, 'initialize', { protocolVersion: '2025-06-18' })
|
||||
assert.equal(initialized.result.protocolVersion, '2025-06-18')
|
||||
assert.equal(initialized.result.capabilities.tools.listChanged, false)
|
||||
|
||||
const tools = await rpc(baseUrl, TOKEN, 2, 'tools/list', {})
|
||||
const toolNames = tools.result.tools.map((tool) => tool.name).sort()
|
||||
assert.deepEqual(toolNames, [
|
||||
'ontology_get_entity',
|
||||
'ontology_get_guardrails',
|
||||
'ontology_resolve_context',
|
||||
'ontology_search',
|
||||
'ontology_status',
|
||||
])
|
||||
|
||||
const search = await rpc(baseUrl, TOKEN, 3, 'tools/call', {
|
||||
name: 'ontology_search',
|
||||
arguments: { query: 'трайк', limit: 10 },
|
||||
})
|
||||
assert.equal(search.result.isError, undefined)
|
||||
assert.equal(search.result.structuredContent.query, 'трайк')
|
||||
assert.equal(search.result.structuredContent.results.some((item) => item.entity?.id === 'gelios.unit'), true)
|
||||
|
||||
const entity = await rpc(baseUrl, TOKEN, 4, 'tools/call', {
|
||||
name: 'ontology_get_entity',
|
||||
arguments: { term: 'helius' },
|
||||
})
|
||||
assert.equal(entity.result.structuredContent.entity.id, 'gelios.integration')
|
||||
assert.equal(JSON.stringify(entity.result.structuredContent).includes('/Users/'), false)
|
||||
|
||||
const guardrails = await rpc(baseUrl, TOKEN, 5, 'tools/call', {
|
||||
name: 'ontology_get_guardrails',
|
||||
arguments: { entityId: 'gelios.command_dispatch' },
|
||||
})
|
||||
assert.equal(guardrails.result.structuredContent.rules.some((rule) => rule.id === 'guardrail.gelios.commands_are_red_domain'), true)
|
||||
|
||||
const unauthorized = await fetch(`${baseUrl}/mcp`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', id: 6, method: 'tools/list', params: {} }),
|
||||
})
|
||||
assert.equal(unauthorized.status, 401)
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
'health',
|
||||
'mcp_initialize',
|
||||
'read_only_tool_catalog',
|
||||
'gelios_alias_resolution',
|
||||
'gelios_command_guardrail_visible',
|
||||
'evidence_paths_not_exposed',
|
||||
'internal_bearer_required',
|
||||
],
|
||||
}, null, 2))
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
||||
}
|
||||
|
||||
async function rpc(baseUrl, token, id, method, params) {
|
||||
const response = await fetch(`${baseUrl}/mcp`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
'content-type': 'application/json',
|
||||
accept: 'application/json, text/event-stream',
|
||||
'mcp-protocol-version': '2025-06-18',
|
||||
},
|
||||
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
||||
})
|
||||
assert.equal(response.ok, true)
|
||||
return response.json()
|
||||
}
|
||||
Reference in New Issue
Block a user