The agent runs in n8n on the box and reaches its model over the mesh from episode five, so no prompt leaves the network. It gets three read-only tools (recent commits, monitor status, wiki search) and returns one suggested action as a field. A switch compares that field against three allowed values, and only one of them reaches ops-actions, a small runner holding its own hard-coded allowlists for actions and targets. Measured while building this: the same prompt injection was harmless in every place the agent reached through a tool (twenty runs, no hits) and worked every time it arrived in the alert itself, through a poisoned monitor name (thirteen of thirteen). The allowlist rejected all thirteen. It guards the action field only, so the attacker-written summary still reached the wiki unchanged; the triage note now truncates and strips every field it did not write itself.
86 lines
2 KiB
JSON
86 lines
2 KiB
JSON
{
|
|
"name": "T2 - tool: ops log search",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"inputSource": "jsonExample",
|
|
"jsonExample": "{\n \"query\": \"Vault\"\n}"
|
|
},
|
|
"id": "when-called-by-the-agent",
|
|
"name": "When called by the agent",
|
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
-300,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "http://outline:3000/api/documents.search",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpHeaderAuth",
|
|
"sendBody": true,
|
|
"contentType": "json",
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ query: String($json.query || \"\"), limit: 3 }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "search-the-ops-log",
|
|
"name": "Search the Ops Log",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-80,
|
|
0
|
|
],
|
|
"credentials": {
|
|
"httpHeaderAuth": {
|
|
"id": "REPLACE_WITH_YOUR_CREDENTIAL_ID",
|
|
"name": "Outline API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\n// Outline hands back whole documents including author objects, roughly 15 kB\n// for three hits. The agent needs title, link and the beginning of the text.\nconst hits = $json.data ?? [];\nreturn [{ json: { hits: hits.slice(0, 3).map(h => ({\n title: h.document?.title ?? '',\n url: h.document?.url ?? '',\n text: String(h.document?.text ?? '').slice(0, 900),\n})) } }];\n"
|
|
},
|
|
"id": "shrink-to-three-snippets",
|
|
"name": "Shrink to three snippets",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
140,
|
|
0
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"When called by the agent": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search the Ops Log",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search the Ops Log": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Shrink to three snippets",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
}
|
|
}
|