fix(telegram): apply bot token to the real alias + in-container reload path

Verified against a live board: the open workshop board accepts a remote
config write but refuses a remote /admin/reload (only loopback is
allowed, and the gateway runs inside a container so even host→localhost
is non-loopback). And the board's telegram channel is provisioned under
alias `default`, not `main`.

- bridge configureTelegram: write channels.telegram.default.bot_token
  (was .main), and make the remote /admin/reload best-effort — never
  fail on its rejection; the board applies the reload itself.

Companion change in zeroclaw firmware (main.py reload_watcher): the
in-container supervisor polls /api/config/reload-status and triggers the
loopback /admin/reload when a change is pending. Proven E2E on the live
board — remote PUT → pending_reload → watcher reload within ~3s, channel
back up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-22 00:33:51 -07:00
co-authored by Claude Opus 4.8
parent 59aa2365c2
commit a4d06ebddb
+20 -14
View File
@@ -140,17 +140,18 @@ export async function promptAndWait(node: NodeRef, message: string, agent = 'def
} }
/** /**
* Set the team's Telegram bot token on their node and restart it so the channel * Set the team's Telegram bot token on their node so the channel picks it up.
* comes up. Two calls against the board's ZeroClaw gateway (bearer = the node's * Writes `channels.telegram.default.bot_token` via PUT /api/config/prop — the
* server-side token): * gateway auto-creates the alias if absent (`ensure_map_key_for_path`) and
* 1. PUT /api/config/prop — writes `channels.telegram.main.bot_token`. The * enc2-encrypts the secret on disk. That write also flips the node's
* gateway auto-creates the `main` alias (`ensure_map_key_for_path`) and * `pending_reload` flag.
* enc2-encrypts the secret on disk. *
* 2. POST /admin/reload — in-place daemon reload; re-instantiates every * The reload that actually starts the channel is done ON the board, not here:
* subsystem (channels included) from fresh config, so the newly-added * the gateway runs inside a container, so a remote POST /admin/reload is refused
* Telegram channel starts. Same PID, sub-second downtime. * (only loopback is allowed on an open board). The node's in-container watcher
* Remote /admin/reload requires the board to have `gateway.allow_remote_admin` * (`reload_watcher` in the App-Lab app) sees `pending_reload` and triggers the
* enabled (+ pairing); on an open board it 403s — surfaced as a thrown error. * loopback reload within a few seconds. We still fire a best-effort remote
* reload for paired boards that permit it, but never fail on its rejection.
*/ */
export async function configureTelegram(node: NodeRef, token: string): Promise<void> { export async function configureTelegram(node: NodeRef, token: string): Promise<void> {
const auth = { authorization: `Bearer ${node.token}` } const auth = { authorization: `Bearer ${node.token}` }
@@ -158,14 +159,19 @@ export async function configureTelegram(node: NodeRef, token: string): Promise<v
method: 'PUT', method: 'PUT',
headers: { ...auth, 'content-type': 'application/json' }, headers: { ...auth, 'content-type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
path: 'channels.telegram.main.bot_token', path: 'channels.telegram.default.bot_token',
value: token, value: token,
comment: 'set via APESS onboarding', comment: 'set via APESS onboarding',
}), }),
}) })
if (!put.ok) throw new Error(`config write failed (${put.status})`) if (!put.ok) throw new Error(`config write failed (${put.status})`)
const reload = await fetch(`${node.url}/admin/reload`, { method: 'POST', headers: auth }) // Best-effort: instant reload on boards that allow remote admin; the board's
if (!reload.ok) throw new Error(`node reload failed (${reload.status})`) // own watcher applies it otherwise. Never throw on a refused remote reload.
try {
await fetch(`${node.url}/admin/reload`, { method: 'POST', headers: auth })
} catch {
/* watcher will apply it */
}
} }
export interface SubscribeOptions { export interface SubscribeOptions {