feat(npu): add cron and n8n advisory examples

This commit is contained in:
William Valentin
2026-06-05 15:52:43 -07:00
parent 5a14adaf58
commit 6155b54ab5
4 changed files with 386 additions and 0 deletions
+12
View File
@@ -33,6 +33,18 @@ POST /v1/advisory/generate
POST /v1/advisory/triage
```
## Cron and n8n advisory dry-run contract
For cron/n8n event classification, use the dry-run contract in `docs/cron-n8n-advisory-classifier.md`.
It defines the normalized event envelope, decision envelope, `suppress|log|summarize|escalate` recommendation mapping, and duplicate/stale/no-op/action-required examples.
Example artifacts:
- `examples/cron-advisory-dry-run.sh` — host-local cron wrapper that prints one compact decision line and performs no side effects.
- `examples/n8n-advisory-dry-run-fragment.json` — sanitized inactive n8n node fragment for Set -> HTTP Request -> Code decision mapping.
Both examples preserve the gateway authority boundary: advisory only, no send/restart/memory/tool/routing authority.
### Classifier shadow call
```bash
@@ -0,0 +1,256 @@
# Cron and n8n advisory classifier contract
Status: dry-run specification and integration examples
Scope: cron and n8n alert/event classification through the OpenVINO advisory gateway
Gateway: `http://172.19.0.1:18830` from `n8n-agent` and host-local cron on the current bridge-bound service. Override `NPU_ADVISORY_GATEWAY_URL=http://127.0.0.1:18830` only if a localhost-bound instance is explicitly running.
## Authority boundary
This contract is advisory only. It may recommend one of `suppress`, `log`, `summarize`, or `escalate`, but it must not perform the action itself.
Every integration must preserve these authority flags:
```json
{
"may_route": false,
"may_write_memory": false,
"may_send_external": false,
"may_process_private_dirs": false,
"may_execute_tools": false,
"may_restart_services": false
}
```
Allowed side effects in dry-run mode:
- read an explicit cron/n8n event payload;
- call the advisory gateway classifier/generator;
- write compact local stdout or n8n execution logs;
- store metadata-only advisory counters if an existing log sink already does so.
Forbidden without separate explicit approval:
- outbound sends/pages/Discord/Telegram/email;
- service restarts, command execution, or tool calls;
- Hermes/Atlas routing changes;
- memory writes;
- broad private-directory processing;
- vector database mutation or reindexing.
## Input event envelope
Cron and n8n producers should normalize events before classification. Keep this input small and avoid raw private payloads.
```json
{
"schema": "cron_n8n_event_v1",
"trace_id": "cron:service-health:2026-06-05T14:30:00Z",
"source": "cron",
"workflow": "npu-service-health",
"event_kind": "health_check",
"severity": "warning",
"subject": "openvino-reranker health check repeated warning",
"summary": "Two consecutive health probes reported timeout, no restart attempted.",
"dedupe_key": "service:openvino-reranker:timeout",
"observed_at": "2026-06-05T14:30:00Z",
"stale_after_s": 900,
"action_requested": false,
"dry_run": true
}
```
Field rules:
- `source`: `cron` or `n8n`.
- `workflow`: compact job/workflow name, not a private URL.
- `subject` + `summary`: the only text sent to the classifier.
- `dedupe_key`: stable non-secret key for duplicate detection by the caller.
- `stale_after_s`: caller-side freshness gate; stale events should not page.
- `action_requested`: true only when an upstream job is asking a human/Atlas to consider action.
- `dry_run`: must remain true for this phase.
## Gateway classifier call
The current gateway `/v1/advisory/classify` accepts explicit text and wraps the classifier response in `openvino_advisory_v1` with NPU proof and authority fields.
Host cron example for the current bridge-bound service:
```bash
curl -fsS http://172.19.0.1:18830/v1/advisory/classify \
-H 'Content-Type: application/json' \
-d '{
"trace_id":"cron:service-health:sample",
"text":"source=cron workflow=npu-service-health severity=warning kind=health_check subject=openvino-reranker repeated timeout summary=Two consecutive health probes reported timeout; no restart attempted; dry_run=true"
}' | jq '{schema, mode, trace_id, npu_ok: .npu_proof.ok, npu_delta: .npu_proof.npu_busy_delta_us, authority, labels: .result.labels}'
```
n8n Docker-bridge example:
```bash
curl -fsS http://172.19.0.1:18830/v1/advisory/classify \
-H 'Content-Type: application/json' \
-d '{"trace_id":"n8n:swarm-health:sample","text":"source=n8n workflow=swarm-health-watchdog severity=critical kind=health_check subject=multiple services unhealthy summary=Health probe failed for three services; dry_run=true"}' \
| jq '{mode, npu_ok: .npu_proof.ok, npu_delta: .npu_proof.npu_busy_delta_us, may_send_external: .authority.may_send_external}'
```
NPU proof gate: an HTTP 200 is not enough. Treat the classifier as NPU-backed only when `.npu_proof.ok == true` and `.npu_proof.npu_busy_delta_us > 0` for real inference.
## Advisory decision envelope
Cron/n8n wrappers should map the gateway response plus caller-side freshness/deduplication state into this compact decision envelope:
```json
{
"schema": "cron_n8n_advisory_decision_v1",
"trace_id": "cron:service-health:2026-06-05T14:30:00Z",
"source": "cron",
"workflow": "npu-service-health",
"dry_run": true,
"recommendation": "summarize",
"classification": "action_required",
"confidence": 0.84,
"reason_codes": ["warning_or_high_urgency", "fresh_event", "not_duplicate"],
"npu_proof": {"required": true, "ok": true, "npu_busy_delta_us": 1234},
"authority": {
"may_route": false,
"may_write_memory": false,
"may_send_external": false,
"may_process_private_dirs": false,
"may_execute_tools": false,
"may_restart_services": false
},
"next_gate": "human_or_atlas_review_required_before_any_side_effect"
}
```
Decision fields:
- `recommendation`: `suppress`, `log`, `summarize`, or `escalate`.
- `classification`: `duplicate`, `stale`, `no_op`, or `action_required` for v1 examples.
- `confidence`: use classifier urgency/category confidence when available; otherwise use a conservative wrapper score.
- `reason_codes`: compact machine-readable rationale, not raw payload text.
- `next_gate`: always a review/approval gate before side effects.
## Recommendation mapping
This is the v1 dry-run mapping. It is intentionally conservative and caller-side; the NPU classifier advises, the wrapper chooses a recommendation, and humans/Atlas retain authority.
| Caller/classifier signal | Classification | Recommendation | Dry-run behavior |
|---|---|---|---|
| Same `dedupe_key` observed inside caller cooldown | `duplicate` | `suppress` | Log compact duplicate count only. Do not send. |
| `observed_at + stale_after_s` is older than now | `stale` | `log` | Log stale event and age. Do not summarize/page. |
| Severity low/normal, no action requested, classifier urgency low/normal | `no_op` | `log` | Keep normal execution log only. |
| Warning/high urgency or action requested, NPU proof ok | `action_required` | `summarize` | Draft a local summary for review; no send/restart. |
| Critical severity or repeated failures and NPU proof ok | `action_required` | `escalate` | Recommend escalation to Atlas/human; wrapper still must not send/restart. |
| NPU proof missing or false | `action_required` or caller-specific | `log` | Log `npu_proof_failed`; do not claim NPU-backed advice. |
## Required examples
### Duplicate -> suppress
Input summary:
```json
{"source":"cron","workflow":"npu-service-health","severity":"warning","dedupe_key":"service:reranker:timeout","summary":"Same timeout as prior run inside cooldown.","dry_run":true}
```
Decision:
```json
{"classification":"duplicate","recommendation":"suppress","reason_codes":["dedupe_key_in_cooldown"],"next_gate":"none_in_dry_run"}
```
### Stale -> log
Input summary:
```json
{"source":"n8n","workflow":"swarm-health-watchdog","severity":"warning","observed_at":"older_than_stale_after","stale_after_s":900,"summary":"Delayed webhook replay for an old probe.","dry_run":true}
```
Decision:
```json
{"classification":"stale","recommendation":"log","reason_codes":["event_stale"],"next_gate":"none_in_dry_run"}
```
### No-op -> log
Input summary:
```json
{"source":"cron","workflow":"backup-check","severity":"normal","action_requested":false,"summary":"Backup completed and all expected files are present.","dry_run":true}
```
Decision:
```json
{"classification":"no_op","recommendation":"log","reason_codes":["normal_severity","no_action_requested"],"next_gate":"none_in_dry_run"}
```
### Action required -> summarize/escalate
Input summary:
```json
{"source":"n8n","workflow":"swarm-health-watchdog","severity":"critical","action_requested":true,"summary":"RAG and embeddings health failed repeatedly; no restart attempted.","dry_run":true}
```
Decision:
```json
{"classification":"action_required","recommendation":"escalate","reason_codes":["critical_severity","action_requested","fresh_event"],"next_gate":"human_or_atlas_review_required_before_any_side_effect"}
```
## Optional local summary draft
If the decision is `summarize` or `escalate`, a wrapper may request a bounded draft from `/v1/advisory/generate`:
```bash
curl -fsS http://172.19.0.1:18830/v1/advisory/generate \
-H 'Content-Type: application/json' \
-d '{"trace_id":"cron:service-health:sample","job":"summary","input":"Health check warning: openvino-reranker timed out twice; no restart attempted.","max_new_tokens":48}' \
| jq '{mode, trace_id, npu_ok: .npu_proof.ok, authority, draft: .result.draft_text, final_authority: .result.final_authority}'
```
The draft remains non-authoritative. It must not be automatically sent externally or written to memory.
## n8n integration pattern
Recommended node chain for dry-run workflows:
```text
Schedule/Webhook/Failure Trigger
-> Set normalized event envelope
-> HTTP Request POST /v1/advisory/classify
-> Code node maps decision envelope
-> IF node on recommendation
suppress/log: execution log only
summarize/escalate: optional local summary draft, then execution log only
```
The IF node must not connect to outbound messaging, service restart, memory write, or Hermes routing nodes until a separate approval changes the authority boundary.
See `../examples/n8n-advisory-dry-run-fragment.json` for a sanitized node fragment.
## Cron integration pattern
Cron jobs should call a wrapper script that prints one compact line and exits successfully unless the wrapper itself fails. The wrapper should not page or restart.
Example crontab shape:
```text
*/15 * * * * /home/will/lab/swarm/openvino-advisory-gateway/examples/cron-advisory-dry-run.sh npu-service-health warning health_check "openvino-reranker timeout twice" "service:openvino-reranker:timeout" >> /home/will/.local/state/npu-advisory/cron.log 2>&1
```
See `../examples/cron-advisory-dry-run.sh`.
## Verification checklist
- Gateway health is reachable on the intended interface.
- Classifier response includes `schema=openvino_advisory_v1`.
- `.authority.*` flags are all false for side-effect authority.
- `.npu_proof.ok` is true and `npu_busy_delta_us > 0` before claiming NPU-backed advice.
- Decision envelope is compact and contains only booleans/counts/paths/deltas/gates.
- Duplicate/stale/no-op/action-required examples remain dry-run only.
- No n8n workflow activation, outbound send, service restart, memory write, routing change, private-dir broadening, or vector DB mutation occurred.
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
# Dry-run cron/n8n advisory wrapper.
# It calls the advisory classifier and prints one compact decision line.
# It does not send, restart, route, execute tools, or write memory.
GATEWAY_URL="${NPU_ADVISORY_GATEWAY_URL:-http://172.19.0.1:18830}"
WORKFLOW="${1:-cron-advisory-sample}"
SEVERITY="${2:-normal}"
EVENT_KIND="${3:-health_check}"
SUBJECT="${4:-sample advisory event}"
DEDUPE_KEY="${5:-sample}"
TRACE_ID="${NPU_ADVISORY_TRACE_ID:-cron:${WORKFLOW}:$(date -u +%Y%m%dT%H%M%SZ)}"
TEXT="source=cron workflow=${WORKFLOW} severity=${SEVERITY} kind=${EVENT_KIND} subject=${SUBJECT} dedupe_key=${DEDUPE_KEY} dry_run=true authority=no-send,no-restart,no-memory"
payload=$(jq -nc --arg trace_id "$TRACE_ID" --arg text "$TEXT" '{trace_id:$trace_id,text:$text}')
response=$(curl -fsS "${GATEWAY_URL%/}/v1/advisory/classify" -H 'Content-Type: application/json' -d "$payload")
printf '%s\n' "$response" | jq -c --arg source cron --arg workflow "$WORKFLOW" --arg severity "$SEVERITY" --arg dedupe_key "$DEDUPE_KEY" '
. as $env
| ($env.result.labels.urgency.value // "normal") as $urgency
| ($env.result.labels.urgency.confidence // 0) as $confidence
| ($env.npu_proof.ok == true and (($env.npu_proof.npu_busy_delta_us // 0) > 0)) as $npu_ok
| (if ($npu_ok | not) then "log"
elif ($severity == "critical") then "escalate"
elif ($severity == "warning" or $urgency == "high" or $urgency == "critical") then "summarize"
else "log" end) as $recommendation
| (if ($recommendation == "log" and $severity == "normal") then "no_op" else "action_required" end) as $classification
| {
schema: "cron_n8n_advisory_decision_v1",
trace_id: $env.trace_id,
source: $source,
workflow: $workflow,
dry_run: true,
recommendation: $recommendation,
classification: $classification,
confidence: $confidence,
reason_codes: ([
(if $npu_ok then "npu_proof_ok" else "npu_proof_failed" end),
("severity_" + $severity),
("urgency_" + $urgency)
]),
npu_proof: $env.npu_proof,
authority: $env.authority,
next_gate: (if $recommendation == "escalate" or $recommendation == "summarize" then "human_or_atlas_review_required_before_any_side_effect" else "none_in_dry_run" end)
}'
@@ -0,0 +1,70 @@
{
"name": "OpenVINO Advisory Dry-Run Fragment",
"active": false,
"nodes": [
{
"parameters": {
"values": {
"string": [
{"name": "schema", "value": "cron_n8n_event_v1"},
{"name": "source", "value": "n8n"},
{"name": "workflow", "value": "swarm-health-watchdog"},
{"name": "event_kind", "value": "health_check"},
{"name": "severity", "value": "warning"},
{"name": "subject", "value": "OpenVINO service health warning"},
{"name": "summary", "value": "Health probe reported a warning; no restart or send is authorized."},
{"name": "dedupe_key", "value": "service:openvino:warning"},
{"name": "dry_run", "value": "true"}
]
},
"options": {}
},
"id": "set-normalized-event",
"name": "Set normalized advisory event",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [260, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://172.19.0.1:18830/v1/advisory/classify",
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify({ trace_id: 'n8n:' + $json.workflow + ':' + $now.toISO(), text: 'source=n8n workflow=' + $json.workflow + ' severity=' + $json.severity + ' kind=' + $json.event_kind + ' subject=' + $json.subject + ' summary=' + $json.summary + ' dedupe_key=' + $json.dedupe_key + ' dry_run=true authority=no-send,no-restart,no-memory' }) }}",
"options": {
"timeout": 20000
}
},
"id": "http-advisory-classify",
"name": "HTTP advisory classify dry-run",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [520, 300]
},
{
"parameters": {
"jsCode": "const env = $json;\nconst labels = env.result?.labels || {};\nconst urgency = labels.urgency?.value || 'normal';\nconst severity = $('Set normalized advisory event').first().json.severity || 'normal';\nconst npuOk = env.npu_proof?.ok === true && (env.npu_proof?.npu_busy_delta_us || 0) > 0;\nlet recommendation = 'log';\nlet classification = 'no_op';\nconst reason_codes = [npuOk ? 'npu_proof_ok' : 'npu_proof_failed', `severity_${severity}`, `urgency_${urgency}`];\nif (npuOk && severity === 'critical') { recommendation = 'escalate'; classification = 'action_required'; }\nelse if (npuOk && (severity === 'warning' || urgency === 'high' || urgency === 'critical')) { recommendation = 'summarize'; classification = 'action_required'; }\nif (!npuOk) reason_codes.push('log_only_no_npu_claim');\nreturn [{ json: { schema: 'cron_n8n_advisory_decision_v1', trace_id: env.trace_id, source: 'n8n', workflow: $('Set normalized advisory event').first().json.workflow, dry_run: true, recommendation, classification, confidence: labels.urgency?.confidence || 0, reason_codes, npu_proof: env.npu_proof, authority: env.authority, next_gate: (recommendation === 'summarize' || recommendation === 'escalate') ? 'human_or_atlas_review_required_before_any_side_effect' : 'none_in_dry_run' } } }];"
},
"id": "map-dry-run-decision",
"name": "Map dry-run decision (no side effects)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [780, 300]
}
],
"connections": {
"Set normalized advisory event": {
"main": [[{"node": "HTTP advisory classify dry-run", "type": "main", "index": 0}]]
},
"HTTP advisory classify dry-run": {
"main": [[{"node": "Map dry-run decision (no side effects)", "type": "main", "index": 0}]]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {},
"staticData": null,
"tags": ["dry-run", "openvino", "advisory"]
}