M56: make RAGcore sync fail closed
This commit is contained in:
@@ -1,5 +1,26 @@
|
|||||||
# Project state
|
# 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)
|
## M55 — restore private cross-project RAGcore routing (2026-08-24)
|
||||||
|
|
||||||
- M54 promoted successfully as `81e3fd63bdbcb2e9c4ae1d709ea46f40537b6f62`, with two
|
- M54 promoted successfully as `81e3fd63bdbcb2e9c4ae1d709ea46f40537b6f62`, with two
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ the same way via the same CLI import path, re-verified.
|
|||||||
| Live workflow ID | `6wbkc4d1AouGpmWT` |
|
| Live workflow ID | `6wbkc4d1AouGpmWT` |
|
||||||
| Active status (as of 2026-08-05) | **Active / Published** |
|
| Active status (as of 2026-08-05) | **Active / Published** |
|
||||||
| Error Workflow | `Fleet Ops — Workflow Error Handler` (wired) |
|
| Error Workflow | `Fleet Ops — Workflow Error Handler` (wired) |
|
||||||
| Execution telemetry | Successful syncs report both the bounded sync result and the canonical workflow heartbeat. |
|
| 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) | `4afd46b6ef57b7e0a4705611732506d7fcc2a37567266847991b85050e1d37c3` |
|
| Checksum (sha256) | `7e9e4beb02a9f9de69efe9f74626e44b6f34f2e0f905229b4605b8cd452b7128` |
|
||||||
|
|
||||||
## 4. Fleet Ops — Workflow Error Handler
|
## 4. Fleet Ops — Workflow Error Handler
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,14 @@
|
|||||||
"name": "source_id",
|
"name": "source_id",
|
||||||
"value": "={{ $json.id }}"
|
"value": "={{ $json.id }}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "external_id",
|
||||||
|
"value": "={{ $json.document_id + '.md' }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locale",
|
||||||
|
"value": "={{ $json.language }}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"parameterType": "formBinaryData",
|
"parameterType": "formBinaryData",
|
||||||
"name": "file",
|
"name": "file",
|
||||||
@@ -150,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
|
|||||||
@@ -109,6 +109,34 @@ def check_workflows(failures: list[str]) -> None:
|
|||||||
fail(f"{filename} contains a cleartext Fleet Ops callback", failures)
|
fail(f"{filename} contains a cleartext Fleet Ops callback", failures)
|
||||||
if filename == "fleet-ops-vehicle-return.json":
|
if filename == "fleet-ops-vehicle-return.json":
|
||||||
check_return_workflow_timing(definition, failures)
|
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:
|
def check_return_workflow_timing(definition: dict, failures: list[str]) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user