Skip to content
Merged
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
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Channel adapters that connect [World2Agent](https://github.com/machinepulse-ai/w
| --- | --- | --- |
| [`claude-code-channel`](./claude-code-channel) | [Claude Code](https://docs.claude.com/en/docs/claude-code) | MCP channel adapter + Claude Code plugin bundle. Signals arrive as in-session MCP notifications. |
| [`hermes-sensor-bridge`](./hermes-sensor-bridge) | [Hermes Agent](https://hermes-agent.nousresearch.com/) | Out-of-process supervisor + webhook bridge. Each signal triggers a fresh `AIAgent.run_conversation()` with the generated handler skill auto-loaded. |
| [`openclaw-sensor-bridge`](./openclaw-sensor-bridge) | [OpenClaw](https://openclaw.ai) | Out-of-process supervisor + `/hooks/agent` bridge. Each signal triggers a fresh isolated agent turn with the generated handler skill auto-loaded. |

---

Expand Down Expand Up @@ -51,6 +52,29 @@ Each signal triggers a fresh agent run against the generated handler skill. See

---

## Quick start — OpenClaw

Three steps:

```bash
npm install -g @world2agent/openclaw-sensor-bridge
openclaw skills install world2agent-manage
```

Then send this in your OpenClaw chat:

```
Use world2agent-manage skill install @quill-io/sensor-frontier-ai-news
```

The skill walks the SETUP.md Q&A, generates a handler skill, registers the sensor in `~/.world2agent/config.json`, and starts the supervisor. Subsequent signals each trigger a fresh `/hooks/agent` call against the handler.

> First time only: the bridge's `bootstrap.sh` writes a managed `hooks` block into `~/.openclaw/openclaw.json` (auto-generates `hooks.token` if absent, sets `allowRequestSessionKey`, adds `"w2a:"` to `allowedSessionKeyPrefixes`) and asks you to run `openclaw gateway restart` once. A timestamped backup of the original config is kept next to the file. Every install after that is seamless.

If you already have a paired chat platform (Feishu, iMessage, Telegram, …) configured via `<PLATFORM>_HOME_CHANNEL` in `~/.openclaw/.env`, replies are auto-pushed to that chat by default. See [`openclaw-sensor-bridge/README.md`](./openclaw-sensor-bridge/README.md) for the full delivery options and lifecycle scripts.

---

## Repository layout

```
Expand All @@ -63,7 +87,7 @@ Each signal triggers a fresh agent run against the generated handler skill. See
│ ├── skills/ # MCP-side handler skills
│ ├── src/
│ └── package.json
── hermes-sensor-bridge/ # @world2agent/hermes-sensor-bridge
── hermes-sensor-bridge/ # @world2agent/hermes-sensor-bridge
│ ├── src/
│ │ ├── runner/ # per-sensor subprocess
│ │ └── supervisor/ # daemon (signal → HMAC → POST → Hermes)
Expand All @@ -72,6 +96,15 @@ Each signal triggers a fresh agent run against the generated handler skill. See
│ │ └── scripts/ # all host-side work (install, remove, list, …)
│ ├── e2e/
│ └── package.json
└── openclaw-sensor-bridge/ # @world2agent/openclaw-sensor-bridge
├── src/
│ ├── runner/ # per-sensor subprocess
│ └── supervisor/ # daemon (signal → Bearer → POST /hooks/agent → OpenClaw)
├── skills/world2agent-manage/
│ ├── SKILL.md # agent-facing skill
│ └── scripts/ # all host-side work (install, remove, list, …)
├── e2e/
└── package.json
```

---
Expand All @@ -93,6 +126,10 @@ Users pull updates with:

Bump `version` in `hermes-sensor-bridge/package.json`, then `pnpm publish --access public --tag alpha` (alpha) or `latest` (stable). Users pull the runtime with `npm install -g @world2agent/hermes-sensor-bridge@<tag>`. The skill is installed separately via `hermes skills install …`; re-run that command with `--force` to refresh to the latest skill content.

### OpenClaw bridge (`openclaw-sensor-bridge`)

Bump `version` in `openclaw-sensor-bridge/package.json`, then `pnpm publish --access public --tag alpha` (alpha) or `latest` (stable). Users pull the runtime with `npm install -g @world2agent/openclaw-sensor-bridge@<tag>`. The skill is installed separately via `openclaw skills install world2agent-manage`; re-run that command to refresh to the latest skill content.

## License

Apache-2.0
4 changes: 2 additions & 2 deletions 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.0",
"version": "0.1.0-alpha.1",
"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 All @@ -26,7 +26,7 @@
},
"type": "module",
"bin": {
"world2agent-sensor-runner": "./dist/runner/bin.js",
"world2agent-openclaw-runner": "./dist/runner/bin.js",
"world2agent-openclaw-supervisor": "./dist/supervisor/bin.js"
},
"scripts": {
Expand Down
77 changes: 53 additions & 24 deletions openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,19 @@ idempotent; second runs just confirm existing state.
bash "$SCRIPTS/bootstrap.sh"
```

What it does:
What it does (each step is a no-op when already done):

- verifies `world2agent-openclaw-supervisor` and `world2agent-sensor-runner`
are on PATH;
- ensures `world2agent-openclaw-supervisor` and `world2agent-openclaw-runner`
are on PATH; **auto-installs `@world2agent/openclaw-sensor-bridge`
globally via `npm install -g`** if either binary is missing;
- creates / preserves `~/.world2agent/.openclaw-bridge-state.json`
(`control_token` / `control_port`, mode 0600);
- verifies OpenClaw's hooks subsystem is ready: `hooks.enabled=true`,
`hooks.token` set, `hooks.allowRequestSessionKey=true`, and at least one
prefix in `hooks.allowedSessionKeyPrefixes` (read-only — never modifies
`~/.openclaw/openclaw.json`);
- ensures the OpenClaw hooks block is ready by mutating
`~/.openclaw/openclaw.json` only when needed: sets `hooks.enabled=true`,
generates `hooks.token` if absent (existing tokens are preserved),
sets `hooks.allowRequestSessionKey=true`, and adds `"w2a:"` to
`hooks.allowedSessionKeyPrefixes`. A timestamped backup of the file is
written next to it before any mutation;
- starts the supervisor (foreground, `nohup`-detached).

Output shape:
Expand All @@ -72,25 +75,38 @@ Output shape:
{
"ok": true,
"steps": {
"binary": "present",
"binary": "present" | "installed",
"state": "created" | "present",
"openclaw_hooks": "ready",
"openclaw_hooks": "ready" | "wrote-managed-fields",
"supervisor": "started" | "already-running" | "started-but-not-yet-healthy" | "start-failed"
},
"openclaw_home": "/Users/.../.openclaw",
"control_port": 8646,
"session_key_prefix": "w2a:" | "hook:" | <first allowed>
"session_key_prefix": "w2a:" | "hook:" | <first allowed>,
"gateway_restart_needed": true | false,
"openclaw_config_backup": "/Users/.../.openclaw/openclaw.json.w2a-backup-..." | null
}
```

When `gateway_restart_needed` is `true`, **stop and tell the user to run
`openclaw gateway restart` before installing any sensors** — we just wrote
fields into their gateway config and the running gateway hasn't picked
them up yet, so `/hooks/agent` will reject our POSTs with 4xx until the
restart. Mention the backup path so they know how to roll back.

Failure modes that need a user message:

- `error: "world2agent-openclaw-supervisor / world2agent-sensor-runner not on PATH..."`
→ bridge runtime not installed. Tell the user to
`npm install -g @world2agent/openclaw-sensor-bridge`.
- `error: "OpenClaw hooks not ready: ..."`
→ quote the reason. The user must edit `~/.openclaw/openclaw.json` to
enable hooks. Show them the minimal block:
- `error: "auto-install of @world2agent/openclaw-sensor-bridge failed..."`
→ npm install failed (commonly EACCES on system Node). Suggest the user
re-run with `sudo npm install -g @world2agent/openclaw-sensor-bridge`,
then re-invoke bootstrap.
- `error: "bridge binaries still not on PATH after install..."`
→ npm's global bin dir isn't on PATH. Have the user check
`npm bin -g` and add it to their shell profile.
- `error: "could not configure OpenClaw hooks: ..."`
→ quote the reason. The auto-mutation refused (e.g. file isn't valid
JSON). The user must edit `~/.openclaw/openclaw.json` themselves to
ensure this block exists, then restart the gateway:

```json
"hooks": {
Expand All @@ -101,8 +117,6 @@ Failure modes that need a user message:
}
```

Then `openclaw gateway restart`.

---

## Install a sensor — full flow
Expand Down Expand Up @@ -137,13 +151,20 @@ reply to a real channel. Three options:

| Mode | Effect | Pick when |
|---|---|---|
| dashboard-only (default) | Agent runs, reply persists to the W2A session lane (`agent:main:<sessionKey>`). User must check the dashboard / `openclaw sessions` to see it. | User is just trying it out, or wants the handler skill to gate notifications by emitting `imsg`/`feishu`/etc. tool calls itself. |
| `--notify-channel <ch> --notify-to <handle>` | Agent runs, reply auto-delivered to channel/handle via OpenClaw's outbound layer. | User wants every signal-driven reply pushed to a real chat (iMessage, Feishu, Slack, …). |
| (none — handler skill emits its own send) | Agent runs, handler decides if/where to send. | High-traffic sensors where most signals should be silent. |
| 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. |

If the user already has paired channels (Feishu, iMessage, etc.) and wants
push, ask them which one and the handle (phone number, chat id, etc.).
Otherwise default to dashboard-only.
**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.

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

Expand Down Expand Up @@ -214,9 +235,17 @@ bash "$SCRIPTS/install-sensor.sh" "<package>" \
[--agent-id <id>] \
[--session-key <key>] \
[--model <id>] \
[--thinking <level>] \
[--timeout-seconds <n>] \
[--fallbacks <model1,model2,...>] \
[--notify-channel <ch> --notify-to <handle> [--notify-account <id>]]
```

`--thinking`, `--timeout-seconds`, and `--fallbacks` map directly to the
documented `/hooks/agent` request fields and are stored on the manifest's
`_openclaw_bridge` block. Only set them when the user has a specific
reason — by default the OpenClaw gateway picks sensible defaults.

Successful output:

```json
Expand Down
71 changes: 71 additions & 0 deletions openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,77 @@ openclaw_hooks_ready() {
return 0
}

# Idempotently fix the OpenClaw hooks block so the bridge can deliver signals.
# Mutates ~/.openclaw/openclaw.json only when something actually needs to
# change; an existing user-set token is preserved verbatim.
#
# Effects when needed:
# - create the file (with `{}`) if missing
# - 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
#
# A timestamped backup is written next to the file before any mutation.
# Stdout: "noop" when nothing changed, or "wrote:<backup-path>" on mutation.
# Stderr: human-readable error on failure. Return: 0 ok, 1 fail.
ensure_openclaw_hooks() {
local cfg
cfg=$(openclaw_config_path)
mkdir -p "$(dirname "$cfg")" || { echo "could not mkdir $(dirname "$cfg")" >&2; return 1; }
if [ ! -f "$cfg" ]; then
printf '{}\n' >"$cfg" || { echo "could not create $cfg" >&2; return 1; }
fi

if ! jq -e . "$cfg" >/dev/null 2>&1; then
echo "$cfg is not valid JSON; refusing to mutate" >&2
return 1
fi

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

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

if [ "$changed" = false ]; then
printf 'noop\n'
return 0
fi

local new_token=$token
[ -z "$new_token" ] && new_token=$(random_hex 32)

local backup="$cfg.w2a-backup-$(date +%Y%m%d%H%M%S)"
cp -p "$cfg" "$backup" 2>/dev/null || { echo "could not back up $cfg to $backup" >&2; return 1; }

local tmp
tmp=$(mktemp)
if ! jq --arg token "$new_token" '
.hooks = ((.hooks // {})
+ { enabled: true,
allowRequestSessionKey: true,
token: ((.hooks.token // "") | if . == "" then $token else . end),
allowedSessionKeyPrefixes: (
((.hooks.allowedSessionKeyPrefixes // []) | map(select(type == "string")))
| if any(. == "w2a:") then . else . + ["w2a:"] end
)
})
' "$cfg" >"$tmp"; then
rm -f "$tmp"
echo "jq rewrite of $cfg failed" >&2
return 1
fi
mv "$tmp" "$cfg"
printf 'wrote:%s\n' "$backup"
}

# Picks the first viable sessionKey prefix from
# hooks.allowedSessionKeyPrefixes, preferring `w2a:` then `hook:`.
# Falls back to `w2a:` if the array is missing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,30 @@ add_step() {
steps=$(jq -c --arg k "$1" --arg v "$2" '. + {($k):$v}' <<<"$steps")
}

# Step 1: binary on PATH.
# Step 1: binary on PATH. If missing, auto-install the bridge package globally
# via npm. The agent will surface npm's stderr (sudo prompt, EACCES, etc.) so
# the user can re-run with elevated rights if needed.
binary_status="present"
if ! command -v world2agent-openclaw-supervisor >/dev/null 2>&1 \
|| ! command -v world2agent-sensor-runner >/dev/null 2>&1; then
out_err "world2agent-openclaw-supervisor / world2agent-sensor-runner not on PATH; install the bridge runtime first (see package README)"
|| ! command -v world2agent-openclaw-runner >/dev/null 2>&1; then
command -v npm >/dev/null 2>&1 \
|| out_err "world2agent-openclaw-supervisor not on PATH and 'npm' is unavailable; install Node.js + npm first, or install the bridge runtime manually"
printf '[bootstrap] installing @world2agent/openclaw-sensor-bridge globally...\n' >&2
install_log=$(mktemp)
if ! npm install -g @world2agent/openclaw-sensor-bridge >"$install_log" 2>&1; then
cat "$install_log" >&2
rm -f "$install_log"
out_err "auto-install of @world2agent/openclaw-sensor-bridge failed; you may need 'sudo npm install -g @world2agent/openclaw-sensor-bridge'"
fi
rm -f "$install_log"
hash -r 2>/dev/null || true
if ! command -v world2agent-openclaw-supervisor >/dev/null 2>&1 \
|| ! command -v world2agent-openclaw-runner >/dev/null 2>&1; then
out_err "bridge binaries still not on PATH after install; check that npm's global bin dir is on PATH (npm bin -g)"
fi
binary_status="installed"
fi
add_step binary "present"
add_step binary "$binary_status"

# Step 2: bridge state.
state_path=$(bridge_state_path)
Expand All @@ -34,14 +52,31 @@ state_existed=true
ensure_bridge_state || out_err "could not write $state_path"
add_step state "$([ "$state_existed" = true ] && echo "present" || echo "created")"

# Step 3: verify OpenClaw hooks are ready. Read-only — we don't silently
# modify the gateway config; the user opted into hooks themselves.
hooks_err=$(openclaw_hooks_ready 2>&1) && hooks_err=""
if [ -n "$hooks_err" ]; then
out_err "OpenClaw hooks not ready: $hooks_err. Edit $(openclaw_config_path) to set hooks.enabled=true, hooks.token=\"<secret>\", hooks.allowRequestSessionKey=true, and at least one entry in hooks.allowedSessionKeyPrefixes (e.g. \"w2a:\"). Then restart the gateway."
fi
# Step 3: ensure OpenClaw hooks are ready. We mutate the gateway config when
# needed (idempotent; existing tokens are preserved). If we did mutate, the
# user must restart `openclaw gateway` for the new block to take effect —
# 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."
}
rm -f /tmp/.w2a-hooks-err
gateway_restart_needed=false
hooks_backup=""
case "$hooks_action" in
noop)
add_step openclaw_hooks "ready"
;;
wrote:*)
hooks_backup=${hooks_action#wrote:}
add_step openclaw_hooks "wrote-managed-fields"
gateway_restart_needed=true
;;
*)
out_err "unexpected ensure_openclaw_hooks output: $hooks_action"
;;
esac
prefix=$(default_session_key_prefix)
add_step openclaw_hooks "ready"

# Step 4: supervisor process.
if supervisor_alive; then
Expand All @@ -66,4 +101,8 @@ out_ok "$(jq -nc \
--arg oh "$(openclaw_home)" \
--argjson port "$control_port" \
--arg prefix "$prefix" \
'{steps:$s,openclaw_home:$oh,control_port:$port,session_key_prefix:$prefix}')"
--argjson restart "$gateway_restart_needed" \
--arg backup "$hooks_backup" \
'{steps:$s,openclaw_home:$oh,control_port:$port,session_key_prefix:$prefix,
gateway_restart_needed:$restart,
openclaw_config_backup: (if $backup == "" then null else $backup end)}')"
Loading
Loading