You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OpenClaw wraps webhook payloads in EXTERNAL_UNTRUSTED_CONTENT which tells
models not to execute tools from untrusted content. Our web_fetch callback
URLs were inside this envelope, so agents correctly refused to invoke them.
Two-layer fix:
- Add allowUnsafeExternalContent: true to all 6 hook mappings in openclaw.json,
openclaw-airgapped.json, and install.sh template (safe: loopback + token auth)
- Restructure webhook messages to be data-only — callback URL templates now
live in each agent's AGENTS.md system prompt, not the webhook payload
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
10
+
### Fixed
11
+
-**Pipeline stalls after triage — OpenClaw security envelope blocks tool calls** (fixes #13): OpenClaw wraps webhook payloads in `EXTERNAL_UNTRUSTED_CONTENT` which instructs models not to execute tools mentioned within untrusted content. Our `web_fetch` callback URLs were inside this envelope, so agents correctly refused to call them. Fix: added `allowUnsafeExternalContent: true` to all 6 hook mappings (safe — webhooks are loopback-only and token-authenticated), and restructured webhook messages to be data-only (callback URL templates now live in each agent's AGENTS.md system prompt instead of the webhook payload).
12
+
10
13
### Added
11
14
-**Provider policy notice and OpenRouter recommendation**: README, QUICKSTART, and openclaw.json now warn users that Anthropic and Google have banned subscription OAuth tokens in third-party agent tools. OpenRouter is recommended as the safest single-key option. Direct provider API keys (pay-per-token) still work fine.
12
15
-**Stalled-pipeline detector**: Automatically detects cases stuck in transient statuses (open, triaged, correlated, etc.) for longer than a configurable threshold and re-dispatches the webhook to give the agent another attempt. Configurable via `STALLED_PIPELINE_ENABLED`, `STALLED_PIPELINE_THRESHOLD_MINUTES` (default 30), and `STALLED_PIPELINE_CHECK_INTERVAL_MS` (default 300000). New metrics: `autopilot_stalled_pipeline_detected_total`, `autopilot_stalled_pipeline_redispatched_total`.
### Agent receives webhook but doesn't call web_fetch (EXTERNAL_UNTRUSTED_CONTENT)
270
+
271
+
OpenClaw wraps all webhook payloads in a security envelope (`EXTERNAL_UNTRUSTED_CONTENT`) that instructs models **not** to execute tools or commands mentioned within the untrusted content. This is a safety feature to prevent prompt injection from external sources.
272
+
273
+
**Problem:** If your `openclaw.json` hook mappings don't include `"allowUnsafeExternalContent": true`, the model sees the callback URL inside the security envelope and correctly refuses to call `web_fetch`. The agent outputs a text summary but never advances the pipeline.
274
+
275
+
**Symptoms:**
276
+
- Agent sessions show the model producing text analysis but never invoking `web_fetch`
277
+
- Cases stay in `open` status despite triage agent running
278
+
- Stalled pipeline detector fires repeatedly with no progress
279
+
- Session logs show `stopReason: "stop"` (not `"error"`) with token usage > 0
280
+
281
+
**Fix:** Add `"allowUnsafeExternalContent": true` to each hook mapping in `~/.openclaw/openclaw.json`:
282
+
283
+
```json
284
+
{
285
+
"match": { "path": "wazuh-alert" },
286
+
"action": "agent",
287
+
"agentId": "wazuh-triage",
288
+
"messageTemplate": "{{message}}",
289
+
"name": "Wazuh Alert Triage",
290
+
"allowUnsafeExternalContent": true
291
+
}
292
+
```
293
+
294
+
This is safe because webhook payloads come from your own runtime service on loopback (`127.0.0.1`), authenticated by `hooks.token`. Apply to all 6 hook mappings. Version 2.4.4+ of the installer and reference configs include this flag by default.
The runtime includes a stalled-pipeline detector that automatically re-dispatches webhooks for cases stuck in transient statuses (`open`, `triaged`, `correlated`, `investigated`, `planned`, `approved`). If a case remains in one of these statuses longer than the threshold, the detector re-sends the appropriate webhook to give the agent another chance.
Copy file name to clipboardExpand all lines: runtime/autopilot-service/index.js
+19-35Lines changed: 19 additions & 35 deletions
Original file line number
Diff line number
Diff line change
@@ -1044,12 +1044,16 @@ async function updateCase(caseId, updates) {
1044
1044
};
1045
1045
constwebhookPath=statusWebhooks[updates.status];
1046
1046
if(webhookPath){
1047
+
// NOTE: Callback URLs are in each agent's AGENTS.md (system prompt), not here.
1048
+
// OpenClaw wraps webhook content in EXTERNAL_UNTRUSTED_CONTENT which blocks
1049
+
// tool invocations from the message body. Agents read case_id from the data
1050
+
// below and use the URL templates from their system prompt.
1047
1051
conststatusMessages={
1048
-
triaged: `Correlate case ${caseId} (${evidencePack.severity} severity). Search for related alerts, identify attack patterns, then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=correlated`,
1049
-
correlated: `Investigate case ${caseId} (${evidencePack.severity} severity). Perform deep analysis using MCP tools: check agent health, search security events, analyze threat indicators. Then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=investigated`,
1050
-
investigated: `Plan response for case ${caseId} (${evidencePack.severity} severity). Review investigation findings and create a response plan. Then use web_fetch to submit the plan: http://localhost:${config.metricsPort}/api/agent-action/create-plan?case_id=${caseId}&title={url_encoded_title}&risk_level={risk_level}&actions={url_encoded_actions_json}`,
1051
-
planned: `Evaluate proposed plan for case ${caseId} (${evidencePack.severity} severity). Check all policy rules, risk levels, and approval requirements. Then use web_fetch to submit your decision: http://localhost:${config.metricsPort}/api/agent-action/approve-plan?plan_id={plan_id}&approver_id=policy-guard&decision={allow|deny|escalate}&reason={url_encoded_reason}`,
1052
-
approved: `Execute approved plan for case${caseId} (${evidencePack.severity} severity). Check responder status, then execute the plan. Use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/execute-plan?plan_id={plan_id}&executor_id=responder-agent`,
1052
+
triaged: `New correlation task. Case ID: ${caseId}. Severity: ${evidencePack.severity}. Search for related alerts, identify attack patterns, and advance the pipeline per your AGENTS.md instructions.`,
1053
+
correlated: `New investigation task. Case ID: ${caseId}. Severity: ${evidencePack.severity}. Perform deep analysis using MCP tools, then advance the pipeline per your AGENTS.md instructions.`,
1054
+
investigated: `New response planning task. Case ID: ${caseId}. Severity: ${evidencePack.severity}. Review investigation findings and create a response plan per your AGENTS.md instructions.`,
1055
+
planned: `New policy evaluation task. Case ID: ${caseId}. Severity: ${evidencePack.severity}. Check all policy rules, risk levels, and approval requirements per your AGENTS.md instructions.`,
1056
+
approved: `New execution task. Case ID:${caseId}. Severity: ${evidencePack.severity}. Execute the approved plan per your AGENTS.md instructions.`,
1053
1057
};
1054
1058
dispatchToGateway(webhookPath,{
1055
1059
message: statusMessages[updates.status]||`Process case ${caseId} — status changed to ${updates.status}.`,
@@ -3354,8 +3358,14 @@ function createServer() {
3354
3358
log("info","triage","Created new case from alert",{case_id: caseId,alert_id: alertId, severity });
3355
3359
3356
3360
// Dispatch to triage agent via OpenClaw gateway
3361
+
// NOTE: Callback URLs are NOT included in the webhook message because
3362
+
// OpenClaw wraps webhook content in an EXTERNAL_UNTRUSTED_CONTENT security
3363
+
// envelope that instructs models not to execute tools from untrusted content.
3364
+
// Instead, each agent's AGENTS.md (loaded as system prompt) contains the
3365
+
// callback URL templates. The agent reads case_id from this data and
3366
+
// substitutes it into the URL pattern from its system prompt.
3357
3367
dispatchToGateway("/webhook/wazuh-alert",{
3358
-
message: `Triage new ${severity}-severity alert: ${caseData.title}. Case${caseId} with${entities.length}entities extracted. Analyze the alert, assess threat level, then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=triaged`,
3368
+
message: `New triage task. Case ID: ${caseId}. Severity: ${severity}. Title:${caseData.title}. Entities:${entities.length} extracted. Follow your AGENTS.md instructions to triage this alert and advance the pipeline.`,
3359
3369
case_id: caseId,
3360
3370
severity,
3361
3371
title: caseData.title,
@@ -4405,35 +4415,9 @@ async function checkStalledPipeline() {
msg+=` Use web_fetch to call: ${callbackUrls[caseSummary.status]}`;
4436
-
}
4418
+
// NOTE: Callback URLs are in each agent's AGENTS.md (system prompt), not
4419
+
// in the webhook message. See comment in updateCase() for rationale.
4420
+
constmsg=`[RETRY] Case ID: ${caseSummary.case_id}. Severity: ${evidencePack.severity||caseSummary.severity}. Status: ${caseSummary.status}. Stalled for ${ageMinutes}m. Follow your AGENTS.md instructions to process this case and advance the pipeline.`;
0 commit comments