Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openclaw-sensor-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Edit `~/.openclaw/openclaw.json` to include:
"enabled": true,
"token": "<a long random secret you keep private>",
"allowRequestSessionKey": true,
"allowedSessionKeyPrefixes": ["w2a:"]
"allowedSessionKeyPrefixes": ["hook:", "w2a:"]
}
```

Expand Down
2 changes: 1 addition & 1 deletion openclaw-sensor-bridge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@world2agent/openclaw-sensor-bridge",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"description": "World2Agent bridge for OpenClaw — runs sensors as supervised subprocesses and delivers their signals into OpenClaw via the gateway's /hooks/agent webhook",
"license": "Apache-2.0",
"author": "MachinePulse Pte. Ltd.",
Expand Down
86 changes: 62 additions & 24 deletions openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,37 @@ JSON
### Step 3: choose delivery target

OpenClaw's `/hooks/agent` accepts a `deliver` flag that routes the agent's
reply to a real channel. Three options:

| Mode | Effect | Pick when |
|---|---|---|
| auto-push to paired channel (**default**) | Agent runs, reply auto-delivered via OpenClaw's outbound layer. `install-sensor.sh` auto-detects the first `<PLATFORM>_HOME_CHANNEL=<handle>` entry in `~/.openclaw/.env` (priority: feishu, imessage, telegram, slack, discord, signal, whatsapp, wecom, dingtalk) and uses that as the target. | User has already paired a chat platform with OpenClaw — the env var is the user's signal that "this is my preferred inbox." |
| dashboard-only | Agent runs, reply persists to the W2A session lane only. User has to open the dashboard / `openclaw sessions` to see it. | No paired channel, or user explicitly wants the handler skill to gate notifications via `imsg`/`feishu`/etc. tool calls of its own. |
| explicit `--notify-channel <ch> --notify-to <handle>` | Same as auto-push but the user picks the channel/handle. | User has multiple paired channels and wants this sensor on a non-default one. |

**Default behavior:** if the user doesn't bring delivery up,
`install-sensor.sh` auto-fills `--notify-channel` / `--notify-to` from the
home-channel env vars and the agent reply is pushed to that chat. Only
when no `<PLATFORM>_HOME_CHANNEL` is set does it fall back to
dashboard-only. So **don't ask the user about delivery unless they raise
it** — a paired channel is a strong signal they've already chosen their
preferred inbox.

Pass `--notify-channel`/`--notify-to` explicitly to override, or omit
both on a host with no paired channels for dashboard-only.
reply to a real chat. Without it, signals only land in the OpenClaw
dashboard's session lane and the user has to manually open the dashboard
to see them — which is almost never what they want.

**You MUST ask the user about delivery before installing**, translating
into their conversation language (per "Conversation language" above).
Three short questions, in order:

1. *"When a signal arrives, do you want the reply pushed to a chat
(so you'll actually be notified), or only logged to the OpenClaw
dashboard (you'd have to open it yourself to see anything)?"*
- Push → continue to Q2.
- Dashboard-only → skip Q2/Q3, go to Step 4.
2. *"Which platform should the reply go to?"* — accept one of:
`feishu`, `telegram`, `discord`, `slack`, `imessage`, `wecom`,
`dingtalk`, `signal`, `whatsapp`.
3. *"What's the target ID on `<platform>`? For feishu that's your
user `open_id` (`ou_xxx`) or a group `chat_id` (`oc_xxx`); if you
don't know yours, run `/lark-contact` to look up your own `open_id`.
For other platforms, the corresponding user or group identifier."*

Then in Step 5 pass `--notify-channel <ch> --notify-to <handle>` for push
mode, or pass neither for dashboard-only.

**Do not** read or write `~/.openclaw/.env` from this SKILL. The script
keeps a `<PLATFORM>_HOME_CHANNEL` auto-fill as a power-user shortcut for
people who'd rather configure once than answer per-install — but treat
that as an invisible internal optimization. **Always ask the three
questions above**, even if you suspect `.env` might be set up. If the
script's auto-detect kicks in despite the user's answer, the explicit
CLI flags you pass take precedence.

### Step 4: compose the handler SKILL.md

Expand Down Expand Up @@ -257,24 +270,49 @@ Successful output:
"session_key": "w2a:hackernews",
"agent_id": "main",
"skill_path": "/.../SKILL.md",
"supervisor_reload": { "ok": true, "applied": {"started":[...]} } | null
"supervisor_reload": { "ok": true, "applied": {"started":[...]} } | null,
"delivery": {
"mode": "push" | "dashboard-only",
"channel": "feishu" | null,
"to": "ou_xxx" | null
}
}
```

`supervisor_reload` may be `null` when the supervisor's control HTTP isn't
reachable from this process — that's fine, the file watcher picks up the
new `~/.world2agent/config.json` entry within ~500 ms anyway.

`delivery.mode` reflects what the script actually wired up — read it
verbatim and report it back in Step 6 (don't assume the user's Step 3
answer succeeded; the `.env` auto-fill could have kicked in unexpectedly,
or — more importantly — the user could have answered "push" but you
forgot to pass the flags).

If the install script refuses with a frontmatter mismatch, fix the rendered
handler's `name` and retry.

### Step 6: report to the user

One sentence: `Installed <package> (sensor_id <sensor>); next matching
signal will trigger an agent run on session lane agent:<agent>:<session_key>.`

If they configured a notify target, add: `replies will be delivered to
<channel>:<to>`.
Branch on `result.delivery.mode`:

- **`mode == "push"`**:
*"Installed `<package>` (sensor_id `<sensor>`). Replies will be
delivered to `<delivery.channel>:<delivery.to>`. The next matching
signal will trigger an agent run on session lane
`agent:<agent>:<session_key>`."*

- **`mode == "dashboard-only"`** (⚠️ user MUST be told explicitly):
*"Installed `<package>` (sensor_id `<sensor>`), but **no IM target
configured** — replies will only land in the OpenClaw dashboard
(session lane `agent:<agent>:<session_key>`); you won't get a
notification anywhere. To enable push later: re-run
`/world2agent:sensor-add` and answer "push" to the delivery question,
or run `world2agent-manage` directly with
`--notify-channel <ch> --notify-to <handle>`."*

The dashboard-only case is the one that silently bites users — never
report it as a plain "installed!" without spelling out the consequence.

---

Expand Down
15 changes: 12 additions & 3 deletions openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,13 @@ openclaw_hooks_ready() {
# - set hooks.enabled = true
# - generate hooks.token if empty (32 hex chars from /dev/urandom)
# - set hooks.allowRequestSessionKey = true
# - ensure "w2a:" is present in hooks.allowedSessionKeyPrefixes
# - ensure both "hook:" and "w2a:" are present in
# hooks.allowedSessionKeyPrefixes
#
# `hook:` is OpenClaw's own default sessionKey namespace — without it in the
# allowlist, the gateway refuses to start (`hooks.allowedSessionKeyPrefixes
# must include 'hook:' when hooks.defaultSessionKey is unset`). `w2a:` is the
# bridge's per-sensor lane.
#
# A timestamped backup is written next to the file before any mutation.
# Stdout: "noop" when nothing changed, or "wrote:<backup-path>" on mutation.
Expand All @@ -251,15 +257,17 @@ ensure_openclaw_hooks() {
return 1
fi

local enabled token allow has_w2a
local enabled token allow has_hook has_w2a
enabled=$(jq -r '.hooks.enabled // false' "$cfg")
token=$(jq -r '.hooks.token // ""' "$cfg")
allow=$(jq -r '.hooks.allowRequestSessionKey // false' "$cfg")
has_hook=$(jq -r '((.hooks.allowedSessionKeyPrefixes // []) | any(. == "hook:"))' "$cfg")
has_w2a=$(jq -r '((.hooks.allowedSessionKeyPrefixes // []) | any(. == "w2a:"))' "$cfg")

local changed=false
[ "$enabled" != "true" ] && changed=true
[ "$allow" != "true" ] && changed=true
[ "$has_hook" != "true" ] && changed=true
[ "$has_w2a" != "true" ] && changed=true
[ -z "$token" ] && changed=true

Expand All @@ -283,7 +291,8 @@ ensure_openclaw_hooks() {
token: ((.hooks.token // "") | if . == "" then $token else . end),
allowedSessionKeyPrefixes: (
((.hooks.allowedSessionKeyPrefixes // []) | map(select(type == "string")))
| if any(. == "w2a:") then . else . + ["w2a:"] end
| if any(. == "hook:") then . else . + ["hook:"] end
| if any(. == "w2a:") then . else . + ["w2a:"] end
)
})
' "$cfg" >"$tmp"; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ add_step state "$([ "$state_existed" = true ] && echo "present" || echo "created
# we surface that via gateway_restart_needed in the output.
hooks_action=$(ensure_openclaw_hooks 2>/tmp/.w2a-hooks-err) || {
err=$(cat /tmp/.w2a-hooks-err 2>/dev/null); rm -f /tmp/.w2a-hooks-err
out_err "could not configure OpenClaw hooks: ${err:-unknown error}. Edit $(openclaw_config_path) manually to set hooks.enabled=true, hooks.token=\"<secret>\", hooks.allowRequestSessionKey=true, hooks.allowedSessionKeyPrefixes=[\"w2a:\"]; then restart the gateway."
out_err "could not configure OpenClaw hooks: ${err:-unknown error}. Edit $(openclaw_config_path) manually to set hooks.enabled=true, hooks.token=\"<secret>\", hooks.allowRequestSessionKey=true, hooks.allowedSessionKeyPrefixes=[\"hook:\",\"w2a:\"]; then restart the gateway."
}
rm -f /tmp/.w2a-hooks-err
gateway_restart_needed=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ if [ -z "$reload_result" ] || ! jq -e . <<<"$reload_result" >/dev/null 2>&1; the
reload_result='null'
fi

# Surface the resolved delivery target so the SKILL/agent can give the user
# accurate post-install feedback. Without this, a sensor that silently fell
# back to "dashboard-only" looks identical to a fully-wired push sensor in
# the script output — agent reports "installed!" and the user never realises
# signals aren't being pushed anywhere.
if [ -n "$notify_channel" ]; then
delivery_json=$(jq -nc \
--arg ch "$notify_channel" \
--arg to "$notify_to" \
'{mode:"push", channel:$ch, to:$to}')
else
delivery_json='{"mode":"dashboard-only","channel":null,"to":null}'
fi

out_ok "$(jq -nc \
--arg pkg "$pkg" \
--arg sensor_id "$sensor_id" \
Expand All @@ -253,4 +267,5 @@ out_ok "$(jq -nc \
--arg agent_id "$agent_id" \
--arg skill_path "$skill_dir/SKILL.md" \
--argjson reload "$reload_result" \
'{package:$pkg,sensor_id:$sensor_id,skill_id:$skill_id,session_key:$session_key,agent_id:$agent_id,skill_path:$skill_path,supervisor_reload:$reload}')"
--argjson delivery "$delivery_json" \
'{package:$pkg,sensor_id:$sensor_id,skill_id:$skill_id,session_key:$session_key,agent_id:$agent_id,skill_path:$skill_path,supervisor_reload:$reload,delivery:$delivery}')"
Loading