From ae9cd772a4dc09d2c7dc5545d4d3903da245adbb Mon Sep 17 00:00:00 2001 From: "daibo@machinepulse.ai" Date: Thu, 30 Apr 2026 00:05:41 +0800 Subject: [PATCH 1/2] feat(openclaw-bridge): zero-touch bootstrap + payload alignment, bump to 0.1.0-alpha.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make `bootstrap.sh` self-sufficient instead of asking the user to install the package and hand-edit `~/.openclaw/openclaw.json`: - auto-install `@world2agent/openclaw-sensor-bridge` globally via npm when the binaries aren't on PATH (so the SKILL alone is enough to bring up the runtime) - idempotently fix the `hooks` block in `openclaw.json`: enable, generate `token` if absent (existing tokens preserved), set `allowRequestSessionKey`, ensure `"w2a:"` is in `allowedSessionKeyPrefixes`. Backup is written next to the file before any mutation; `gateway_restart_needed` is surfaced in the bootstrap output so the SKILL tells the user to restart their gateway Default delivery now mirrors hermes-sensor-bridge: `install-sensor.sh` auto-fills `--notify-channel` / `--notify-to` from the first `_HOME_CHANNEL=` it finds in `~/.openclaw/.env` (priority feishu, imessage, telegram, slack, discord, signal, whatsapp, wecom, dingtalk). Paired channel users now get pushes to their actual inbox by default instead of having signals sit in a session lane only the dashboard surfaces. Other changes: - rename `world2agent-sensor-runner` → `world2agent-openclaw-runner` to avoid global-bin collision with hermes-sensor-bridge - expose `thinking`, `timeout_seconds`, `fallbacks` on the `_openclaw_bridge` manifest block; thread them and an auto `name: "w2a-"` into every `/hooks/agent` payload to align with the documented request schema - delete the empty `tools/` leftover Co-Authored-By: Claude Opus 4.7 (1M context) --- openclaw-sensor-bridge/package.json | 4 +- .../skills/world2agent-manage/SKILL.md | 77 +++++++++++++------ .../skills/world2agent-manage/scripts/_lib.sh | 71 +++++++++++++++++ .../world2agent-manage/scripts/bootstrap.sh | 63 ++++++++++++--- .../scripts/install-sensor.sh | 58 +++++++++++++- .../src/supervisor/manifest.ts | 24 ++++++ .../src/supervisor/spawn.ts | 22 +++++- 7 files changed, 279 insertions(+), 40 deletions(-) diff --git a/openclaw-sensor-bridge/package.json b/openclaw-sensor-bridge/package.json index 86278bc..7c64677 100644 --- a/openclaw-sensor-bridge/package.json +++ b/openclaw-sensor-bridge/package.json @@ -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.", @@ -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": { diff --git a/openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md b/openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md index de4ae29..bfb6e4a 100644 --- a/openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md +++ b/openclaw-sensor-bridge/skills/world2agent-manage/SKILL.md @@ -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: @@ -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:" | + "session_key_prefix": "w2a:" | "hook:" | , + "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": { @@ -101,8 +117,6 @@ Failure modes that need a user message: } ``` - Then `openclaw gateway restart`. - --- ## Install a sensor — full flow @@ -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:`). 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 --notify-to ` | 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 `_HOME_CHANNEL=` 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 --notify-to ` | 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 `_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 @@ -214,9 +235,17 @@ bash "$SCRIPTS/install-sensor.sh" "" \ [--agent-id ] \ [--session-key ] \ [--model ] \ + [--thinking ] \ + [--timeout-seconds ] \ + [--fallbacks ] \ [--notify-channel --notify-to [--notify-account ]] ``` +`--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 diff --git a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh index c77d2e2..de173c0 100644 --- a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh +++ b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/_lib.sh @@ -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:" 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. diff --git a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/bootstrap.sh b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/bootstrap.sh index f439630..df6e2eb 100644 --- a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/bootstrap.sh +++ b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/bootstrap.sh @@ -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) @@ -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=\"\", 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=\"\", 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 @@ -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)}')" diff --git a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/install-sensor.sh b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/install-sensor.sh index c379dd1..be4676b 100644 --- a/openclaw-sensor-bridge/skills/world2agent-manage/scripts/install-sensor.sh +++ b/openclaw-sensor-bridge/skills/world2agent-manage/scripts/install-sensor.sh @@ -12,6 +12,9 @@ # prefix auto-picked from hooks.allowedSessionKeyPrefixes # (preferring `w2a:` then `hook:`) # [--model ] model override forwarded to /hooks/agent +# [--thinking ] /hooks/agent `thinking` field (e.g. low/medium/high) +# [--timeout-seconds ] /hooks/agent `timeoutSeconds` field; positive integer +# [--fallbacks ] comma-separated /hooks/agent `fallbacks` model chain # [--notify-channel ] e.g. imessage / feishu / telegram — paired with --notify-to # [--notify-to ] channel-specific recipient handle # [--notify-account ] optional account id when host has multiple @@ -31,6 +34,9 @@ sensor_id_arg="" agent_id_arg="" session_key_arg="" model_arg="" +thinking_arg="" +timeout_seconds_arg="" +fallbacks_arg="" notify_channel="" notify_to="" notify_account="" @@ -44,6 +50,9 @@ while [ $# -gt 0 ]; do --agent-id) agent_id_arg=$2; shift 2;; --session-key) session_key_arg=$2; shift 2;; --model) model_arg=$2; shift 2;; + --thinking) thinking_arg=$2; shift 2;; + --timeout-seconds) timeout_seconds_arg=$2; shift 2;; + --fallbacks) fallbacks_arg=$2; shift 2;; --notify-channel) notify_channel=$2; shift 2;; --notify-to) notify_to=$2; shift 2;; --notify-account) notify_account=$2; shift 2;; @@ -55,6 +64,20 @@ while [ $# -gt 0 ]; do esac done +if [ -n "$timeout_seconds_arg" ]; then + [[ "$timeout_seconds_arg" =~ ^[0-9]+$ ]] && [ "$timeout_seconds_arg" -gt 0 ] \ + || out_err "--timeout-seconds must be a positive integer: $timeout_seconds_arg" +fi + +fallbacks_json='null' +if [ -n "$fallbacks_arg" ]; then + fallbacks_json=$(jq -nc --arg s "$fallbacks_arg" \ + '$s | split(",") | map(. | gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))') \ + || out_err "could not parse --fallbacks: $fallbacks_arg" + jq -e 'length > 0' <<<"$fallbacks_json" >/dev/null 2>&1 \ + || out_err "--fallbacks parsed to an empty list: $fallbacks_arg" +fi + [ -n "$pkg" ] || out_err "usage: install-sensor.sh --config|--config-file ... --skill-md [...]" [ -n "$skill_md_path" ] || out_err "--skill-md is required" @@ -63,6 +86,28 @@ if { [ -n "$notify_channel" ] && [ -z "$notify_to" ]; } || \ out_err "--notify-channel and --notify-to must be provided together (use both or neither)" fi +# Auto-detect a default delivery target when neither --notify-channel nor +# --notify-to is supplied. Reads ~/.openclaw/.env for the first +# _HOME_CHANNEL= entry in priority order — same convention +# hermes-sensor-bridge uses, so users with paired chat platforms get signals +# pushed to their actual inbox by default instead of disappearing into a +# session lane only the dashboard surfaces. +if [ -z "$notify_channel" ] && [ -z "$notify_to" ]; then + env_file="$(openclaw_home)/.env" + if [ -f "$env_file" ]; then + for plat in feishu imessage telegram slack discord signal whatsapp wecom dingtalk; do + var=$(printf '%s_HOME_CHANNEL' "$plat" | tr '[:lower:]' '[:upper:]') + home=$(grep -E "^${var}=" "$env_file" 2>/dev/null | head -1 \ + | sed -E "s/^${var}=//; s/^['\"]//; s/['\"]$//") || home="" + if [ -n "$home" ]; then + notify_channel=$plat + notify_to=$home + break + fi + done + fi +fi + if [ -n "$config_inline" ] && [ -n "$config_file" ]; then out_err "pass exactly one of --config and --config-file" fi @@ -144,17 +189,28 @@ if [ -n "$notify_channel" ]; then '{channel:$ch,to:$to} + (if $ac == "" then {} else {account:$ac} end)') fi +timeout_seconds_json='null' +if [ -n "$timeout_seconds_arg" ]; then + timeout_seconds_json=$timeout_seconds_arg +fi + bridge_block=$(jq -nc \ --arg sensor_id "$sensor_id" \ --arg skill_id "$skill_id" \ --arg agent_id "$agent_id" \ --arg session_key "$session_key" \ --arg model "$model_arg" \ + --arg thinking "$thinking_arg" \ + --argjson timeout_seconds "$timeout_seconds_json" \ + --argjson fallbacks "$fallbacks_json" \ --argjson notify "$notify_block" \ '{sensor_id:$sensor_id, skill_id:$skill_id} + (if $agent_id == "main" then {} else {agent_id:$agent_id} end) + {session_key:$session_key} - + (if $model == "" then {} else {model:$model} end) + + (if $model == "" then {} else {model:$model} end) + + (if $thinking == "" then {} else {thinking:$thinking} end) + + (if $timeout_seconds == null then {} else {timeout_seconds:$timeout_seconds} end) + + (if $fallbacks == null then {} else {fallbacks:$fallbacks} end) + (if $notify == null then {} else {notify:$notify} end)') cfg_path=$(config_json_path) diff --git a/openclaw-sensor-bridge/src/supervisor/manifest.ts b/openclaw-sensor-bridge/src/supervisor/manifest.ts index f284547..f7f0400 100644 --- a/openclaw-sensor-bridge/src/supervisor/manifest.ts +++ b/openclaw-sensor-bridge/src/supervisor/manifest.ts @@ -22,6 +22,12 @@ export interface OpenClawBridgeSensorConfig { notify?: NotifyTarget; // Optional model override forwarded to /hooks/agent. model?: string; + // Optional per-sensor pass-through fields documented by /hooks/agent. + // None of these are required; omitting them lets the OpenClaw gateway + // apply its own defaults. + thinking?: string; + timeout_seconds?: number; + fallbacks?: string[]; } export interface SharedSensorEntry { @@ -387,6 +393,24 @@ function normalizeOpenClawBridgeConfig( const notify = normalizeNotify(raw.notify); if (notify) normalized.notify = notify; + const thinking = optionalNonEmptyString(raw.thinking); + if (thinking) normalized.thinking = thinking; + + if ( + typeof raw.timeout_seconds === "number" && + Number.isFinite(raw.timeout_seconds) && + raw.timeout_seconds > 0 + ) { + normalized.timeout_seconds = Math.floor(raw.timeout_seconds); + } + + if (Array.isArray(raw.fallbacks)) { + const fallbacks = raw.fallbacks.filter( + (item): item is string => typeof item === "string" && item.trim() !== "", + ); + if (fallbacks.length > 0) normalized.fallbacks = fallbacks; + } + return normalized; } diff --git a/openclaw-sensor-bridge/src/supervisor/spawn.ts b/openclaw-sensor-bridge/src/supervisor/spawn.ts index 7b185f1..9ccf181 100644 --- a/openclaw-sensor-bridge/src/supervisor/spawn.ts +++ b/openclaw-sensor-bridge/src/supervisor/spawn.ts @@ -16,6 +16,9 @@ export interface ChildHandle { sessionKey: string; notify?: NotifyTarget; model?: string; + thinking?: string; + timeoutSeconds?: number; + fallbacks?: string[]; process: ChildProcessWithoutNullStreams; startedAt: number; restartCount: number; @@ -133,6 +136,13 @@ export class SensorSupervisor { sessionKey, ...(entry._openclaw_bridge.notify ? { notify: entry._openclaw_bridge.notify } : {}), ...(entry._openclaw_bridge.model ? { model: entry._openclaw_bridge.model } : {}), + ...(entry._openclaw_bridge.thinking ? { thinking: entry._openclaw_bridge.thinking } : {}), + ...(entry._openclaw_bridge.timeout_seconds !== undefined + ? { timeoutSeconds: entry._openclaw_bridge.timeout_seconds } + : {}), + ...(entry._openclaw_bridge.fallbacks && entry._openclaw_bridge.fallbacks.length > 0 + ? { fallbacks: [...entry._openclaw_bridge.fallbacks] } + : {}), process: proc, startedAt: Date.now(), restartCount, @@ -277,7 +287,10 @@ export class SensorSupervisor { handle.sessionKey === resolveSessionKey(entry, this.openclaw.defaultSessionKeyPrefix) && handle.agentId === resolveAgentId(entry, "main") && hashConfig(handle.notify ?? null) === hashConfig(entry._openclaw_bridge.notify ?? null) && - (handle.model ?? null) === (entry._openclaw_bridge.model ?? null) + (handle.model ?? null) === (entry._openclaw_bridge.model ?? null) && + (handle.thinking ?? null) === (entry._openclaw_bridge.thinking ?? null) && + (handle.timeoutSeconds ?? null) === (entry._openclaw_bridge.timeout_seconds ?? null) && + hashConfig(handle.fallbacks ?? null) === hashConfig(entry._openclaw_bridge.fallbacks ?? null) ); } @@ -339,12 +352,19 @@ export class SensorSupervisor { } const message = renderPrompt(handle.skillId, obj); + // `name` groups runs in the OpenClaw job log by sensor — bare-minimum + // tagging so users can find a sensor's history without grepping the + // full agent log. The /hooks/agent docs list it as optional. const payload: Record = { message, + name: `w2a-${handle.sensorId}`, agentId: handle.agentId, sessionKey: handle.sessionKey, }; if (handle.model) payload.model = handle.model; + if (handle.thinking) payload.thinking = handle.thinking; + if (handle.timeoutSeconds !== undefined) payload.timeoutSeconds = handle.timeoutSeconds; + if (handle.fallbacks && handle.fallbacks.length > 0) payload.fallbacks = handle.fallbacks; if (handle.notify) { payload.deliver = true; payload.channel = handle.notify.channel; From b6e4df6ed8680f800e1f4e55d17f1b219a650776 Mon Sep 17 00:00:00 2001 From: "daibo@machinepulse.ai" Date: Thu, 30 Apr 2026 00:16:15 +0800 Subject: [PATCH 2/2] docs: add OpenClaw quick-start to marketplace README Three-step onboarding paralleling the Hermes section: install the runtime, drop the skill into OpenClaw, then drive it from chat. Also add openclaw-sensor-bridge to the catalog table, repository tree, and the publishing notes. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 316e288..b4629b1 100644 --- a/README.md +++ b/README.md @@ -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. | --- @@ -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 `_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 ``` @@ -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) @@ -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 ``` --- @@ -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@`. 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@`. 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