feat(npu): add cron and n8n advisory examples
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user