diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 304f3ef..a3063c5 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,5 +1,26 @@ # Project state +## M56 — make RAGcore procedure sync fail closed (2026-08-24) + +- M55 restored private connectivity and RAGcore became reachable/ready from both Fleet Ops + API replicas. The repeated live canary then returned honest `insufficient` answers because + source-filtered search found no documents, while unfiltered search proved the procedure + text itself was indexed under unrelated legacy/upload identities. +- The active n8n workflow had drifted onto `Fleet Ops Service Token` for the RAGcore upload + node instead of the existing `RAGcore Sync Token`. Its summary counted every body without + an `error` property as synced, so RAGcore Problem responses were falsely reported as 33 + successes. No unsafe relaxation of Fleet Ops citation validation was made. +- The committed workflow now sends the documented stable identity tuple (`source_id`, + `external_id`, `locale`), uses the dedicated sync credential by name and counts success + only when RAGcore returns a real `AcceptedJob` (`job_id` plus `status_url`). The contract + checker enforces all three invariants to prevent recurrence. +- Validation: workflow JSON parses with the expected credential/fields, `git diff --check` + is clean and the OpenAPI/event/MCP/n8n synchronization gate passes against the current + source tree. +- Exact next action: commit/push M56, import the exact workflow while binding the existing + RAGcore credential ID without exposing it, publish/execute it, verify all 33 stable source + documents through RAGcore and rerun the four live Chromium/Firefox acceptance checks. + ## M55 — restore private cross-project RAGcore routing (2026-08-24) - M54 promoted successfully as `81e3fd63bdbcb2e9c4ae1d709ea46f40537b6f62`, with two diff --git a/n8n/workflows/MANIFEST.md b/n8n/workflows/MANIFEST.md index 7728fa5..8e08085 100644 --- a/n8n/workflows/MANIFEST.md +++ b/n8n/workflows/MANIFEST.md @@ -72,8 +72,8 @@ the same way via the same CLI import path, re-verified. | Live workflow ID | `6wbkc4d1AouGpmWT` | | Active status (as of 2026-08-05) | **Active / Published** | | Error Workflow | `Fleet Ops — Workflow Error Handler` (wired) | -| Execution telemetry | Successful syncs report both the bounded sync result and the canonical workflow heartbeat. | -| Checksum (sha256) | `4afd46b6ef57b7e0a4705611732506d7fcc2a37567266847991b85050e1d37c3` | +| Execution telemetry | Only a valid RAGcore `AcceptedJob` response counts as synced; HTTP problem bodies count as failed. Fleet Ops separately verifies exact active documents before reporting index health. | +| Checksum (sha256) | `7e9e4beb02a9f9de69efe9f74626e44b6f34f2e0f905229b4605b8cd452b7128` | ## 4. Fleet Ops — Workflow Error Handler diff --git a/n8n/workflows/fleet-ops-ragcore-procedure-sync.json b/n8n/workflows/fleet-ops-ragcore-procedure-sync.json index 29145e5..26f732f 100644 --- a/n8n/workflows/fleet-ops-ragcore-procedure-sync.json +++ b/n8n/workflows/fleet-ops-ragcore-procedure-sync.json @@ -121,6 +121,14 @@ "name": "source_id", "value": "={{ $json.id }}" }, + { + "name": "external_id", + "value": "={{ $json.document_id + '.md' }}" + }, + { + "name": "locale", + "value": "={{ $json.language }}" + }, { "parameterType": "formBinaryData", "name": "file", @@ -150,7 +158,7 @@ }, { "parameters": { - "jsCode": "const items = $input.all();\nlet synced = 0;\nlet failed = 0;\nfor (const item of items) {\n if (item.json && item.json.error) {\n failed += 1;\n } else {\n synced += 1;\n }\n}\nreturn [{ json: { execution_id: String($execution.id), synced, failed } }];" + "jsCode": "const items = $input.all();\nlet synced = 0;\nlet failed = 0;\nfor (const item of items) {\n const body = item && item.json;\n const accepted = body && typeof body.job_id === 'string' && typeof body.status_url === 'string' && body.status_url.length > 0;\n if (accepted) {\n synced += 1;\n } else {\n failed += 1;\n }\n}\nreturn [{ json: { execution_id: String($execution.id), synced, failed } }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, diff --git a/scripts/check-contracts.py b/scripts/check-contracts.py index c29613b..17e692f 100755 --- a/scripts/check-contracts.py +++ b/scripts/check-contracts.py @@ -109,6 +109,34 @@ def check_workflows(failures: list[str]) -> None: fail(f"{filename} contains a cleartext Fleet Ops callback", failures) if filename == "fleet-ops-vehicle-return.json": check_return_workflow_timing(definition, failures) + if filename == "fleet-ops-ragcore-procedure-sync.json": + check_ragcore_sync_workflow(definition, failures) + + +def check_ragcore_sync_workflow(definition: dict, failures: list[str]) -> None: + """Keep the sync identity stable and reject HTTP error bodies as successful syncs.""" + nodes_by_name = {node.get("name"): node for node in definition.get("nodes", [])} + upload = nodes_by_name.get("Upload to RAGcore", {}) + credential = upload.get("credentials", {}).get("httpHeaderAuth", {}) + if credential.get("name") != "RAGcore Sync Token": + fail("RAGcore upload must use the dedicated sync credential", failures) + + parameters = upload.get("parameters", {}).get("bodyParameters", {}).get("parameters", []) + field_names = { + item.get("name") for item in parameters if isinstance(item, dict) + } + required = {"space_id", "source_id", "external_id", "locale", "file"} + if not required.issubset(field_names): + fail( + "RAGcore upload must preserve source_id, external_id and locale provenance", + failures, + ) + + summary_code = ( + nodes_by_name.get("Summarize sync result", {}).get("parameters", {}).get("jsCode", "") + ) + if not all(token in summary_code for token in ("job_id", "status_url", "failed += 1")): + fail("RAGcore sync summary must accept only a real AcceptedJob response", failures) def check_return_workflow_timing(definition: dict, failures: list[str]) -> None: