Compare commits
10 Commits
c52f23f794
...
2cbe4e2808
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cbe4e2808 | ||
|
|
70d7a0854c | ||
|
|
fc2b796f02 | ||
|
|
6472949f25 | ||
|
|
61d219cb39 | ||
|
|
4e872521f0 | ||
|
|
0c8ea8d987 | ||
|
|
bffce8ea4f | ||
|
|
4656317770 | ||
|
|
7509c4a057 |
@@ -84,6 +84,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.
|
- Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.
|
||||||
- Cron/doctor: stop flagging canonical `agentTurn` and `systemEvent` payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.
|
- Cron/doctor: stop flagging canonical `agentTurn` and `systemEvent` payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.
|
||||||
- ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving `end_turn`, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.
|
- ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving `end_turn`, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.
|
||||||
|
- Telegram/Discord status reactions: show a temporary compacting reaction during auto-compaction pauses and restore thinking afterward so the bot no longer appears frozen while context is being compacted. (#35474) thanks @Cypherm.
|
||||||
|
|
||||||
## 2026.3.11
|
## 2026.3.11
|
||||||
|
|
||||||
|
|||||||
312
appcast.xml
312
appcast.xml
@@ -2,6 +2,98 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>OpenClaw</title>
|
<title>OpenClaw</title>
|
||||||
|
<item>
|
||||||
|
<title>2026.3.12</title>
|
||||||
|
<pubDate>Fri, 13 Mar 2026 04:25:50 +0000</pubDate>
|
||||||
|
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||||
|
<sparkle:version>2026031290</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>2026.3.12</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||||
|
<description><![CDATA[<h2>OpenClaw 2026.3.12</h2>
|
||||||
|
<h3>Changes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Control UI/dashboard-v2: refresh the gateway dashboard with modular overview, chat, config, agent, and session views, plus a command palette, mobile bottom tabs, and richer chat tools like slash commands, search, export, and pinned messages. (#41503) Thanks @BunsDev.</li>
|
||||||
|
<li>OpenAI/GPT-5.4 fast mode: add configurable session-level fast toggles across <code>/fast</code>, TUI, Control UI, and ACP, with per-model config defaults and OpenAI/Codex request shaping.</li>
|
||||||
|
<li>Anthropic/Claude fast mode: map the shared <code>/fast</code> toggle and <code>params.fastMode</code> to direct Anthropic API-key <code>service_tier</code> requests, with live verification for both Anthropic and OpenAI fast-mode tiers.</li>
|
||||||
|
<li>Models/plugins: move Ollama, vLLM, and SGLang onto the provider-plugin architecture, with provider-owned onboarding, discovery, model-picker setup, and post-selection hooks so core provider wiring is more modular.</li>
|
||||||
|
<li>Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi</li>
|
||||||
|
<li>Agents/subagents: add <code>sessions_yield</code> so orchestrators can end the current turn immediately, skip queued tool work, and carry a hidden follow-up payload into the next session turn. (#36537) thanks @jriff</li>
|
||||||
|
<li>Slack/agent replies: support <code>channelData.slack.blocks</code> in the shared reply delivery path so agents can send Block Kit messages through standard Slack outbound delivery. (#44592) Thanks @vincentkoc.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Fixes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Security/device pairing: switch <code>/pair</code> and <code>openclaw qr</code> setup codes to short-lived bootstrap tokens so the next release no longer embeds shared gateway credentials in chat or QR pairing payloads. Thanks @lintsinghua.</li>
|
||||||
|
<li>Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (<code>GHSA-99qw-6mr3-36qr</code>)(#44174) Thanks @lintsinghua and @vincentkoc.</li>
|
||||||
|
<li>Models/Kimi Coding: send <code>anthropic-messages</code> tools in native Anthropic format again so <code>kimi-coding</code> stops degrading tool calls into XML/plain-text pseudo invocations instead of real <code>tool_use</code> blocks. (#38669, #39907, #40552) Thanks @opriz.</li>
|
||||||
|
<li>TUI/chat log: reuse the active assistant message component for the same streaming run so <code>openclaw tui</code> no longer renders duplicate assistant replies. (#35364) Thanks @lisitan.</li>
|
||||||
|
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
|
||||||
|
<li>Cron/proactive delivery: keep isolated direct cron sends out of the write-ahead resend queue so transient-send retries do not replay duplicate proactive messages after restart. (#40646) Thanks @openperf and @vincentkoc.</li>
|
||||||
|
<li>Models/Kimi Coding: send the built-in <code>User-Agent: claude-code/0.1.0</code> header by default for <code>kimi-coding</code> while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc.</li>
|
||||||
|
<li>Models/OpenAI Codex Spark: keep <code>gpt-5.3-codex-spark</code> working on the <code>openai-codex/*</code> path via resolver fallbacks and clearer Codex-only handling, while continuing to suppress the stale direct <code>openai/*</code> Spark row that OpenAI rejects live.</li>
|
||||||
|
<li>Ollama/Kimi Cloud: apply the Moonshot Kimi payload compatibility wrapper to Ollama-hosted Kimi models like <code>kimi-k2.5:cloud</code>, so tool routing no longer breaks when thinking is enabled. (#41519) Thanks @vincentkoc.</li>
|
||||||
|
<li>Moonshot CN API: respect explicit <code>baseUrl</code> (api.moonshot.cn) in implicit provider resolution so platform.moonshot.cn API keys authenticate correctly instead of returning HTTP 401. (#33637) Thanks @chengzhichao-xydt.</li>
|
||||||
|
<li>Kimi Coding/provider config: respect explicit <code>models.providers["kimi-coding"].baseUrl</code> when resolving the implicit provider so custom Kimi Coding endpoints no longer get overwritten by the built-in default. (#36353) Thanks @2233admin.</li>
|
||||||
|
<li>Gateway/main-session routing: keep TUI and other <code>mode:UI</code> main-session sends on the internal surface when <code>deliver</code> is enabled, so replies no longer inherit the session's persisted Telegram/WhatsApp route. (#43918) Thanks @obviyus.</li>
|
||||||
|
<li>BlueBubbles/self-chat echo dedupe: drop reflected duplicate webhook copies only when a matching <code>fromMe</code> event was just seen for the same chat, body, and timestamp, preventing self-chat loops without broad webhook suppression. Related to #32166. (#38442) Thanks @vincentkoc.</li>
|
||||||
|
<li>iMessage/self-chat echo dedupe: drop reflected duplicate copies only when a matching <code>is_from_me</code> event was just seen for the same chat, text, and <code>created_at</code>, preventing self-chat loops without broad text-only suppression. Related to #32166. (#38440) Thanks @vincentkoc.</li>
|
||||||
|
<li>Subagents/completion announce retries: raise the default announce timeout to 90 seconds and stop retrying gateway-timeout failures for externally delivered completion announces, preventing duplicate user-facing completion messages after slow gateway responses. Fixes #41235. Thanks @vasujain00 and @vincentkoc.</li>
|
||||||
|
<li>Mattermost/block streaming: fix duplicate message delivery (one threaded, one top-level) when block streaming is active by excluding <code>replyToId</code> from the block reply dedup key and adding an explicit <code>threading</code> dock to the Mattermost plugin. (#41362) Thanks @mathiasnagler and @vincentkoc.</li>
|
||||||
|
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
|
||||||
|
<li>macOS/Reminders: add the missing <code>NSRemindersUsageDescription</code> to the bundled app so <code>apple-reminders</code> can trigger the system permission prompt from OpenClaw.app. (#8559) Thanks @dinakars777.</li>
|
||||||
|
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
|
||||||
|
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
|
||||||
|
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
|
||||||
|
<li>Windows/native update: make package installs use the npm update path instead of the git path, carry portable Git into native Windows updates, and mirror the installer's Windows npm env so <code>openclaw update</code> no longer dies early on missing <code>git</code> or <code>node-llama-cpp</code> download setup.</li>
|
||||||
|
<li>Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed <code>write</code> no longer reports success while creating empty files. (#43876) Thanks @glitch418x.</li>
|
||||||
|
<li>Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible <code>\u{...}</code> escapes instead of spoofing the reviewed command. (<code>GHSA-pcqg-f7rg-xfvv</code>)(#43687) Thanks @EkiXu and @vincentkoc.</li>
|
||||||
|
<li>Hooks/loader: fail closed when workspace hook paths cannot be resolved with <code>realpath</code>, so unreadable or broken internal hook paths are skipped instead of falling back to unresolved imports. (#44437) Thanks @vincentkoc.</li>
|
||||||
|
<li>Hooks/agent deliveries: dedupe repeated hook requests by optional idempotency key so webhook retries can reuse the first run instead of launching duplicate agent executions. (#44438) Thanks @vincentkoc.</li>
|
||||||
|
<li>Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (<code>GHSA-9r3v-37xh-2cf6</code>)(#44091) Thanks @wooluo and @vincentkoc.</li>
|
||||||
|
<li>Security/exec allowlist: preserve POSIX case sensitivity and keep <code>?</code> within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (<code>GHSA-f8r2-vg7x-gh8m</code>)(#43798) Thanks @zpbrent and @vincentkoc.</li>
|
||||||
|
<li>Security/commands: require sender ownership for <code>/config</code> and <code>/debug</code> so authorized non-owner senders can no longer reach owner-only config and runtime debug surfaces. (<code>GHSA-r7vr-gr74-94p8</code>)(#44305) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Security/gateway auth: clear unbound client-declared scopes on shared-token WebSocket connects so device-less shared-token operators cannot self-declare elevated scopes. (<code>GHSA-rqpp-rjj8-7wv8</code>)(#44306) Thanks @LUOYEcode and @vincentkoc.</li>
|
||||||
|
<li>Security/browser.request: block persistent browser profile create/delete routes from write-scoped <code>browser.request</code> so callers can no longer persist admin-only browser profile changes through the browser control surface. (<code>GHSA-vmhq-cqm9-6p7q</code>)(#43800) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external <code>agent</code> callers can no longer override the gateway workspace boundary. (<code>GHSA-2rqg-gjgv-84jm</code>)(#43801) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via <code>session_status</code>. (<code>GHSA-wcxr-59v9-rxr8</code>)(#43754) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Security/agent tools: mark <code>nodes</code> as explicitly owner-only and document/test that <code>canvas</code> remains a shared trusted-operator surface unless a real boundary bypass exists.</li>
|
||||||
|
<li>Security/exec approvals: fail closed for Ruby approval flows that use <code>-r</code>, <code>--require</code>, or <code>-I</code> so approval-backed commands no longer bind only the main script while extra local code-loading flags remain outside the reviewed file snapshot.</li>
|
||||||
|
<li>Security/device pairing: cap issued and verified device-token scopes to each paired device's approved scope baseline so stale or overbroad tokens cannot exceed approved access. (<code>GHSA-2pwv-x786-56f8</code>)(#43686) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Docs/onboarding: align the legacy wizard reference and <code>openclaw onboard</code> command docs with the Ollama onboarding flow so all onboarding reference paths now document <code>--auth-choice ollama</code>, Cloud + Local mode, and non-interactive usage. (#43473) Thanks @BruceMacD.</li>
|
||||||
|
<li>Models/secrets: enforce source-managed SecretRef markers in generated <code>models.json</code> so runtime-resolved provider secrets are not persisted when runtime projection is skipped. (#43759) Thanks @joshavant.</li>
|
||||||
|
<li>Security/WebSocket preauth: shorten unauthenticated handshake retention and reject oversized pre-auth frames before application-layer parsing to reduce pre-pairing exposure on unsupported public deployments. (<code>GHSA-jv4g-m82p-2j93</code>)(#44089) (<code>GHSA-xwx2-ppv2-wx98</code>)(#44089) Thanks @ez-lbz and @vincentkoc.</li>
|
||||||
|
<li>Security/proxy attachments: restore the shared media-store size cap for persisted browser proxy files so oversized payloads are rejected instead of overriding the intended 5 MB limit. (<code>GHSA-6rph-mmhp-h7h9</code>)(#43684) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Security/host env: block inherited <code>GIT_EXEC_PATH</code> from sanitized host exec environments so Git helper resolution cannot be steered by host environment state. (<code>GHSA-jf5v-pqgw-gm5m</code>)(#43685) Thanks @zpbrent and @vincentkoc.</li>
|
||||||
|
<li>Security/Feishu webhook: require <code>encryptKey</code> alongside <code>verificationToken</code> in webhook mode so unsigned forged events are rejected instead of being processed with token-only configuration. (<code>GHSA-g353-mgv3-8pcj</code>)(#44087) Thanks @lintsinghua and @vincentkoc.</li>
|
||||||
|
<li>Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic <code>p2p</code> reactions. (<code>GHSA-m69h-jm2f-2pv8</code>)(#44088) Thanks @zpbrent and @vincentkoc.</li>
|
||||||
|
<li>Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a <code>200</code> response. (<code>GHSA-mhxh-9pjm-w7q5</code>)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.</li>
|
||||||
|
<li>Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth <code>429</code> responses. (<code>GHSA-5m9r-p9g7-679c</code>)(#44173) Thanks @zpbrent and @vincentkoc.</li>
|
||||||
|
<li>Security/Zalouser groups: require stable group IDs for allowlist auth by default and gate mutable group-name matching behind <code>channels.zalouser.dangerouslyAllowNameMatching</code>. Thanks @zpbrent.</li>
|
||||||
|
<li>Security/Slack and Teams routing: require stable channel and team IDs for allowlist routing by default, with mutable name matching only via each channel's <code>dangerouslyAllowNameMatching</code> break-glass flag.</li>
|
||||||
|
<li>Security/exec approvals: fail closed for ambiguous inline loader and shell-payload script execution, bind the real script after POSIX shell value-taking flags, and unwrap <code>pnpm</code>/<code>npm exec</code>/<code>npx</code> script runners before approval binding. (<code>GHSA-57jw-9722-6rf2</code>)(<code>GHSA-jvqh-rfmh-jh27</code>)(<code>GHSA-x7pp-23xv-mmr4</code>)(<code>GHSA-jc5j-vg4r-j5jx</code>)(#44247) Thanks @tdjackey and @vincentkoc.</li>
|
||||||
|
<li>Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.</li>
|
||||||
|
<li>Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.</li>
|
||||||
|
<li>Gateway/session stores: regenerate the Swift push-test protocol models and align Windows native session-store realpath handling so protocol checks and sync session discovery stop drifting on Windows. (#44266) thanks @jalehman.</li>
|
||||||
|
<li>Context engine/session routing: forward optional <code>sessionKey</code> through context-engine lifecycle calls so plugins can see structured routing metadata during bootstrap, assembly, post-turn ingestion, and compaction. (#44157) thanks @jalehman.</li>
|
||||||
|
<li>Agents/failover: classify z.ai <code>network_error</code> stop reasons as retryable timeouts so provider connectivity failures trigger fallback instead of surfacing raw unhandled-stop-reason errors. (#43884) Thanks @hougangdev.</li>
|
||||||
|
<li>Memory/session sync: add mode-aware post-compaction session reindexing with <code>agents.defaults.compaction.postIndexSync</code> plus <code>agents.defaults.memorySearch.sync.sessions.postCompactionForce</code>, so compacted session memory can refresh immediately without forcing every deployment into synchronous reindexing. (#25561) thanks @rodrigouroz.</li>
|
||||||
|
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
|
||||||
|
<li>Telegram/native command sync: suppress expected <code>BOT_COMMANDS_TOO_MUCH</code> retry error noise, add a final fallback summary log, and document the difference between command-menu overflow and real Telegram network failures.</li>
|
||||||
|
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
|
||||||
|
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
|
||||||
|
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
|
||||||
|
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
|
||||||
|
<li>Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when <code>hooks.allowedAgentIds</code> leaves hook routing unrestricted.</li>
|
||||||
|
<li>Agents/compaction: skip the post-compaction <code>cache-ttl</code> marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.</li>
|
||||||
|
<li>Native chat/macOS: add <code>/new</code>, <code>/reset</code>, and <code>/clear</code> reset triggers, keep shared main-session aliases aligned, and ignore stale model-selection completions so native chat state stays in sync across reset and fast model changes. (#10898) Thanks @Nachx639.</li>
|
||||||
|
<li>Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.</li>
|
||||||
|
<li>Cron/doctor: stop flagging canonical <code>agentTurn</code> and <code>systemEvent</code> payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.</li>
|
||||||
|
<li>ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving <code>end_turn</code>, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.</li>
|
||||||
|
<li>Telegram/Discord status reactions: show a temporary compacting reaction during auto-compaction pauses and restore thinking afterward so the bot no longer appears frozen while context is being compacted. (#35474) thanks @Cypherm.</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||||
|
]]></description>
|
||||||
|
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.12/OpenClaw-2026.3.12.zip" length="23628700" type="application/octet-stream" sparkle:edSignature="o6Zdcw36l3I0jUg14H+RBqNwrhuuSsq1WMDi4tBRa1+5TC3VCVdFKZ2hzmH2Xjru9lDEzVMP8v2A6RexSbOCBQ=="/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>2026.3.8-beta.1</title>
|
<title>2026.3.8-beta.1</title>
|
||||||
<pubDate>Mon, 09 Mar 2026 07:19:57 +0000</pubDate>
|
<pubDate>Mon, 09 Mar 2026 07:19:57 +0000</pubDate>
|
||||||
@@ -438,225 +530,5 @@
|
|||||||
]]></description>
|
]]></description>
|
||||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.7/OpenClaw-2026.3.7.zip" length="23263833" type="application/octet-stream" sparkle:edSignature="SO0zedZMzrvSDltLkuaSVQTWFPPPe1iu/enS4TGGb5EGckhqRCmNJWMKNID5lKwFC8vefTbfG9JTlSrZedP4Bg=="/>
|
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.7/OpenClaw-2026.3.7.zip" length="23263833" type="application/octet-stream" sparkle:edSignature="SO0zedZMzrvSDltLkuaSVQTWFPPPe1iu/enS4TGGb5EGckhqRCmNJWMKNID5lKwFC8vefTbfG9JTlSrZedP4Bg=="/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<title>2026.3.2</title>
|
|
||||||
<pubDate>Tue, 03 Mar 2026 04:30:29 +0000</pubDate>
|
|
||||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
|
||||||
<sparkle:version>2026030290</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>2026.3.2</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
|
||||||
<description><![CDATA[<h2>OpenClaw 2026.3.2</h2>
|
|
||||||
<h3>Changes</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Secrets/SecretRef coverage: expand SecretRef support across the full supported user-supplied credential surface (64 targets total), including runtime collectors, <code>openclaw secrets</code> planning/apply/audit flows, onboarding SecretInput UX, and related docs; unresolved refs now fail fast on active surfaces while inactive surfaces report non-blocking diagnostics. (#29580) Thanks @joshavant.</li>
|
|
||||||
<li>Tools/PDF analysis: add a first-class <code>pdf</code> tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (<code>agents.defaults.pdfModel</code>, <code>pdfMaxBytesMb</code>, <code>pdfMaxPages</code>), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.</li>
|
|
||||||
<li>Outbound adapters/plugins: add shared <code>sendPayload</code> support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.</li>
|
|
||||||
<li>Models/MiniMax: add first-class <code>MiniMax-M2.5-highspeed</code> support across built-in provider catalogs, onboarding flows, and MiniMax OAuth plugin defaults, while keeping legacy <code>MiniMax-M2.5-Lightning</code> compatibility for existing configs.</li>
|
|
||||||
<li>Sessions/Attachments: add inline file attachment support for <code>sessions_spawn</code> (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via <code>tools.sessions_spawn.attachments</code>. (#16761) Thanks @napetrov.</li>
|
|
||||||
<li>Telegram/Streaming defaults: default <code>channels.telegram.streaming</code> to <code>partial</code> (from <code>off</code>) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.</li>
|
|
||||||
<li>Telegram/DM streaming: use <code>sendMessageDraft</code> for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.</li>
|
|
||||||
<li>Telegram/voice mention gating: add optional <code>disableAudioPreflight</code> on group/topic config to skip mention-detection preflight transcription for inbound voice notes where operators want text-only mention checks. (#23067) Thanks @yangnim21029.</li>
|
|
||||||
<li>CLI/Config validation: add <code>openclaw config validate</code> (with <code>--json</code>) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.</li>
|
|
||||||
<li>Tools/Diffs: add PDF file output support and rendering quality customization controls (<code>fileQuality</code>, <code>fileScale</code>, <code>fileMaxWidth</code>) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.</li>
|
|
||||||
<li>Memory/Ollama embeddings: add <code>memorySearch.provider = "ollama"</code> and <code>memorySearch.fallback = "ollama"</code> support, honor <code>models.providers.ollama</code> settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.</li>
|
|
||||||
<li>Zalo Personal plugin (<code>@openclaw/zalouser</code>): rebuilt channel runtime to use native <code>zca-js</code> integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.</li>
|
|
||||||
<li>Plugin SDK/channel extensibility: expose <code>channelRuntime</code> on <code>ChannelGatewayContext</code> so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.</li>
|
|
||||||
<li>Plugin runtime/STT: add <code>api.runtime.stt.transcribeAudioFile(...)</code> so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.</li>
|
|
||||||
<li>Plugin hooks/session lifecycle: include <code>sessionKey</code> in <code>session_start</code>/<code>session_end</code> hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.</li>
|
|
||||||
<li>Hooks/message lifecycle: add internal hook events <code>message:transcribed</code> and <code>message:preprocessed</code>, plus richer outbound <code>message:sent</code> context (<code>isGroup</code>, <code>groupId</code>) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.</li>
|
|
||||||
<li>Media understanding/audio echo: add optional <code>tools.media.audio.echoTranscript</code> + <code>echoFormat</code> to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.</li>
|
|
||||||
<li>Plugin runtime/system: expose <code>runtime.system.requestHeartbeatNow(...)</code> so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.</li>
|
|
||||||
<li>Plugin runtime/events: expose <code>runtime.events.onAgentEvent</code> and <code>runtime.events.onSessionTranscriptUpdate</code> for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.</li>
|
|
||||||
<li>CLI/Banner taglines: add <code>cli.banner.taglineMode</code> (<code>random</code> | <code>default</code> | <code>off</code>) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Breaking</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>BREAKING:</strong> Onboarding now defaults <code>tools.profile</code> to <code>messaging</code> for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.</li>
|
|
||||||
<li><strong>BREAKING:</strong> ACP dispatch now defaults to enabled unless explicitly disabled (<code>acp.dispatch.enabled=false</code>). If you need to pause ACP turn routing while keeping <code>/acp</code> controls, set <code>acp.dispatch.enabled=false</code>. Docs: https://docs.openclaw.ai/tools/acp-agents</li>
|
|
||||||
<li><strong>BREAKING:</strong> Plugin SDK removed <code>api.registerHttpHandler(...)</code>. Plugins must register explicit HTTP routes via <code>api.registerHttpRoute({ path, auth, match, handler })</code>, and dynamic webhook lifecycles should use <code>registerPluginHttpRoute(...)</code>.</li>
|
|
||||||
<li><strong>BREAKING:</strong> Zalo Personal plugin (<code>@openclaw/zalouser</code>) no longer depends on external <code>zca</code>-compatible CLI binaries (<code>openzca</code>, <code>zca-cli</code>) for runtime send/listen/login; operators should use <code>openclaw channels login --channel zalouser</code> after upgrade to refresh sessions in the new JS-native path.</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Fixes</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (<code>trim</code> on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing <code>token.trim()</code> crashes during status/start flows. (#31973) Thanks @ningding97.</li>
|
|
||||||
<li>Discord/lifecycle startup status: push an immediate <code>connected</code> status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.</li>
|
|
||||||
<li>Feishu/LINE group system prompts: forward per-group <code>systemPrompt</code> config into inbound context <code>GroupSystemPrompt</code> for Feishu and LINE group/room events so configured group-specific behavior actually applies at dispatch time. (#31713) Thanks @whiskyboy.</li>
|
|
||||||
<li>Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.</li>
|
|
||||||
<li>Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older <code>openclaw/plugin-sdk</code> builds omit webhook default constants. (#31606)</li>
|
|
||||||
<li>Feishu/group broadcast dispatch: add configurable multi-agent group broadcast dispatch with observer-session isolation, cross-account dedupe safeguards, and non-mention history buffering rules that avoid duplicate replay in broadcast/topic workflows. (#29575) Thanks @ohmyskyhigh.</li>
|
|
||||||
<li>Gateway/Subagent TLS pairing: allow authenticated local <code>gateway-client</code> backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring <code>sessions_spawn</code> with <code>gateway.tls.enabled=true</code> in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.</li>
|
|
||||||
<li>Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.</li>
|
|
||||||
<li>Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.</li>
|
|
||||||
<li>Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)</li>
|
|
||||||
<li>Voice-call/runtime lifecycle: prevent <code>EADDRINUSE</code> loops by resetting failed runtime promises, making webhook <code>start()</code> idempotent with the actual bound port, and fully cleaning up webhook/tunnel/tailscale resources after startup failures. (#32395) Thanks @scoootscooob.</li>
|
|
||||||
<li>Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when <code>gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback</code> accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example <code>(a|aa)+</code>), and bound large regex-evaluation inputs for session-filter and log-redaction paths.</li>
|
|
||||||
<li>Gateway/Plugin HTTP hardening: require explicit <code>auth</code> for plugin route registration, add route ownership guards for duplicate <code>path+match</code> registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Browser/Profile defaults: prefer <code>openclaw</code> profile over <code>chrome</code> in headless/no-sandbox environments unless an explicit <code>defaultProfile</code> is configured. (#14944) Thanks @BenediktSchackenberg.</li>
|
|
||||||
<li>Gateway/WS security: keep plaintext <code>ws://</code> loopback-only by default, with explicit break-glass private-network opt-in via <code>OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1</code>; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.</li>
|
|
||||||
<li>OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit <code>doctor --deep</code>) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.</li>
|
|
||||||
<li>Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.</li>
|
|
||||||
<li>CLI/Config validation and routing hardening: dedupe <code>openclaw config validate</code> failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including <code>--json</code> fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed <code>config get/unset</code> with split root options). Thanks @gumadeiras.</li>
|
|
||||||
<li>Browser/Extension relay reconnect tolerance: keep <code>/json/version</code> and <code>/cdp</code> reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.</li>
|
|
||||||
<li>CLI/Browser start timeout: honor <code>openclaw browser --timeout <ms> start</code> and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.</li>
|
|
||||||
<li>Synology Chat/gateway lifecycle: keep <code>startAccount</code> pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.</li>
|
|
||||||
<li>Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like <code>/usr/bin/g++</code> and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.</li>
|
|
||||||
<li>Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with <code>204</code> to avoid persistent <code>Processing...</code> states in Synology Chat clients. (#26635) Thanks @memphislee09-source.</li>
|
|
||||||
<li>Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.</li>
|
|
||||||
<li>Slack/Bolt startup compatibility: remove invalid <code>message.channels</code> and <code>message.groups</code> event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified <code>message</code> handler (<code>channel_type</code>). (#32033) Thanks @mahopan.</li>
|
|
||||||
<li>Slack/socket auth failure handling: fail fast on non-recoverable auth errors (<code>account_inactive</code>, <code>invalid_auth</code>, etc.) during startup and reconnect instead of retry-looping indefinitely, including <code>unable_to_socket_mode_start</code> error payload propagation. (#32377) Thanks @scoootscooob.</li>
|
|
||||||
<li>Gateway/macOS LaunchAgent hardening: write <code>Umask=077</code> in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>macOS/LaunchAgent security defaults: write <code>Umask=63</code> (octal <code>077</code>) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system <code>022</code>. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from <code>HTTPS_PROXY</code>/<code>HTTP_PROXY</code> env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.</li>
|
|
||||||
<li>Sandbox/workspace mount permissions: make primary <code>/workspace</code> bind mounts read-only whenever <code>workspaceAccess</code> is not <code>rw</code> (including <code>none</code>) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.</li>
|
|
||||||
<li>Tools/fsPolicy propagation: honor <code>tools.fs.workspaceOnly</code> for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.</li>
|
|
||||||
<li>Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like <code>node@22</code>) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.</li>
|
|
||||||
<li>Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded <code>/api/channels/*</code> variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Browser/Gateway hardening: preserve env credentials for <code>OPENCLAW_GATEWAY_URL</code> / <code>CLAWDBOT_GATEWAY_URL</code> while treating explicit <code>--url</code> as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.</li>
|
|
||||||
<li>Gateway/Control UI basePath webhook passthrough: let non-read methods under configured <code>controlUiBasePath</code> fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.</li>
|
|
||||||
<li>Control UI/Legacy browser compatibility: replace <code>toSorted</code>-dependent cron suggestion sorting in <code>app-render</code> with a compatibility helper so older browsers without <code>Array.prototype.toSorted</code> no longer white-screen. (#31775) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>macOS/PeekabooBridge: add compatibility socket symlinks for legacy <code>clawdbot</code>, <code>clawdis</code>, and <code>moltbot</code> Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.</li>
|
|
||||||
<li>Gateway/message tool reliability: avoid false <code>Unknown channel</code> failures when <code>message.*</code> actions receive platform-specific channel ids by falling back to <code>toolContext.currentChannelProvider</code>, and prevent health-monitor restart thrash for channels that just (re)started by adding a per-channel startup-connect grace window. (from #32367) Thanks @MunemHashmi.</li>
|
|
||||||
<li>Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for <code>.cmd</code> shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.</li>
|
|
||||||
<li>Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for <code>sessions_spawn</code> with <code>runtime="acp"</code> by rejecting ACP spawns from sandboxed requester sessions and rejecting <code>sandbox="require"</code> for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.</li>
|
|
||||||
<li>Security/Web tools SSRF guard: keep DNS pinning for untrusted <code>web_fetch</code> and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Gemini schema sanitization: coerce malformed JSON Schema <code>properties</code> values (<code>null</code>, arrays, primitives) to <code>{}</code> before provider validation, preventing downstream strict-validator crashes on invalid plugin/tool schemas. (#32332) Thanks @webdevtodayjason.</li>
|
|
||||||
<li>Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.</li>
|
|
||||||
<li>Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.</li>
|
|
||||||
<li>Browser/Extension relay stale tabs: evict stale cached targets from <code>/json/list</code> when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.</li>
|
|
||||||
<li>Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up <code>PortInUseError</code> races after <code>browser start</code>/<code>open</code>. (#29538) Thanks @AaronWander.</li>
|
|
||||||
<li>OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty <code>function_call_output.call_id</code> payloads in the WS conversion path to avoid OpenAI 400 errors (<code>Invalid 'input[n].call_id': empty string</code>), with regression coverage for both inbound stream normalization and outbound payload guards.</li>
|
|
||||||
<li>Security/Nodes camera URL downloads: bind node <code>camera.snap</code>/<code>camera.clip</code> URL payload downloads to the resolved node host, enforce fail-closed behavior when node <code>remoteIp</code> is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Config/backups hardening: enforce owner-only (<code>0600</code>) permissions on rotated config backups and clean orphan <code>.bak.*</code> files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.</li>
|
|
||||||
<li>Telegram/inbound media filenames: preserve original <code>file_name</code> metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.</li>
|
|
||||||
<li>Gateway/OpenAI chat completions: honor <code>x-openclaw-message-channel</code> when building <code>agentCommand</code> input for <code>/v1/chat/completions</code>, preserving caller channel identity instead of forcing <code>webchat</code>. (#30462) Thanks @bmendonca3.</li>
|
|
||||||
<li>Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @Glucksberg.</li>
|
|
||||||
<li>Media/MIME normalization: normalize parameterized/case-variant MIME strings in <code>kindFromMime</code> (for example <code>Audio/Ogg; codecs=opus</code>) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.</li>
|
|
||||||
<li>Discord/audio preflight mentions: detect audio attachments via Discord <code>content_type</code> and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.</li>
|
|
||||||
<li>Feishu/topic session routing: use <code>thread_id</code> as topic session scope fallback when <code>root_id</code> is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.</li>
|
|
||||||
<li>Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of <code>NO_REPLY</code> and keep final-message buffering in sync, preventing partial <code>NO</code> leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.</li>
|
|
||||||
<li>Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.</li>
|
|
||||||
<li>Voice-call/Twilio external outbound: auto-register webhook-first <code>outbound-api</code> calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.</li>
|
|
||||||
<li>Feishu/topic root replies: prefer <code>root_id</code> as outbound <code>replyTargetMessageId</code> when present, and parse millisecond <code>message_create_time</code> values correctly so topic replies anchor to the root message in grouped thread flows. (#29968) Thanks @bmendonca3.</li>
|
|
||||||
<li>Feishu/DM pairing reply target: send pairing challenge replies to <code>chat:<chat_id></code> instead of <code>user:<sender_open_id></code> so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky.</li>
|
|
||||||
<li>Feishu/Lark private DM routing: treat inbound <code>chat_type: "private"</code> as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky.</li>
|
|
||||||
<li>Signal/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.</li>
|
|
||||||
<li>Discord/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram/Signal reaction ergonomics in inbound turns.</li>
|
|
||||||
<li>Synology Chat/reply delivery: resolve webhook usernames to Chat API <code>user_id</code> values for outbound chatbot replies, avoiding mismatches between webhook user IDs and <code>method=chatbot</code> recipient IDs in multi-account setups. (#23709) Thanks @druide67.</li>
|
|
||||||
<li>Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.</li>
|
|
||||||
<li>Slack/session routing: keep top-level channel messages in one shared session when <code>replyToMode=off</code>, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.</li>
|
|
||||||
<li>Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.</li>
|
|
||||||
<li>Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.</li>
|
|
||||||
<li>Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (<code>monitor.account-scope.test.ts</code>) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.</li>
|
|
||||||
<li>Feishu/Send target prefixes: normalize explicit <code>group:</code>/<code>dm:</code> send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Webchat/Feishu session continuation: preserve routable <code>OriginatingChannel</code>/<code>OriginatingTo</code> metadata from session delivery context in <code>chat.send</code>, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)</li>
|
|
||||||
<li>Telegram/implicit mention forum handling: exclude Telegram forum system service messages (<code>forum_topic_*</code>, <code>general_forum_topic_*</code>) from reply-chain implicit mention detection so <code>requireMention</code> does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.</li>
|
|
||||||
<li>Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.</li>
|
|
||||||
<li>Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (<code>provider: "message"</code>) and normalize <code>lark</code>/<code>feishu</code> provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)</li>
|
|
||||||
<li>Webchat/silent token leak: filter assistant <code>NO_REPLY</code>-only transcript entries from <code>chat.history</code> responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.</li>
|
|
||||||
<li>Doctor/local memory provider checks: stop false-positive local-provider warnings when <code>provider=local</code> and no explicit <code>modelPath</code> is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.</li>
|
|
||||||
<li>Media understanding/parakeet CLI output parsing: read <code>parakeet-mlx</code> transcripts from <code>--output-dir/<media-basename>.txt</code> when txt output is requested (or default), with stdout fallback for non-txt formats. (#9177) Thanks @mac-110.</li>
|
|
||||||
<li>Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.</li>
|
|
||||||
<li>Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Gateway/Node browser proxy routing: honor <code>profile</code> from <code>browser.request</code> JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Gateway/Control UI basePath POST handling: return 405 for <code>POST</code> on exact basePath routes (for example <code>/openclaw</code>) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Browser/default profile selection: default <code>browser.defaultProfile</code> behavior now prefers <code>openclaw</code> (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the <code>chrome</code> relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.</li>
|
|
||||||
<li>Models/config env propagation: apply <code>config.env.vars</code> before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.</li>
|
|
||||||
<li>Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so <code>openclaw models status</code> no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.</li>
|
|
||||||
<li>Gateway/Heartbeat model reload: treat <code>models.*</code> and <code>agents.defaults.model</code> config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.</li>
|
|
||||||
<li>Memory/LanceDB embeddings: forward configured <code>embedding.dimensions</code> into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.</li>
|
|
||||||
<li>Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.</li>
|
|
||||||
<li>Browser/CDP status accuracy: require a successful <code>Browser.getVersion</code> response over the CDP websocket (not just socket-open) before reporting <code>cdpReady</code>, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.</li>
|
|
||||||
<li>Daemon/systemd checks in containers: treat missing <code>systemctl</code> invocations (including <code>spawn systemctl ENOENT</code>/<code>EACCES</code>) as unavailable service state during <code>is-enabled</code> checks, preventing container flows from failing with <code>Gateway service check failed</code> before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.</li>
|
|
||||||
<li>Security/Node exec approvals: revalidate approval-bound <code>cwd</code> identity immediately before execution/forwarding and fail closed with an explicit denial when <code>cwd</code> drifts after approval hardening.</li>
|
|
||||||
<li>Security audit/skills workspace hardening: add <code>skills.workspace.symlink_escape</code> warning in <code>openclaw security audit</code> when workspace <code>skills/**/SKILL.md</code> resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.</li>
|
|
||||||
<li>Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example <code>env sh -c ...</code>) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Security/fs-safe write hardening: make <code>writeFileWithinRoot</code> use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.</li>
|
|
||||||
<li>Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.</li>
|
|
||||||
<li>Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like <code>[System Message]</code> and line-leading <code>System:</code> in untrusted message content. (#30448)</li>
|
|
||||||
<li>Sandbox/Docker setup command parsing: accept <code>agents.*.sandbox.docker.setupCommand</code> as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction <code>AGENTS.md</code> context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.</li>
|
|
||||||
<li>Agents/Sandbox workdir mapping: map container workdir paths (for example <code>/workspace</code>) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Docker/Sandbox bootstrap hardening: make <code>OPENCLAW_SANDBOX</code> opt-in parsing explicit (<code>1|true|yes|on</code>), support custom Docker socket paths via <code>OPENCLAW_DOCKER_SOCKET</code>, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to <code>off</code> when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.</li>
|
|
||||||
<li>Hooks/webhook ACK compatibility: return <code>200</code> (instead of <code>202</code>) for successful <code>/hooks/agent</code> requests so providers that require <code>200</code> (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.</li>
|
|
||||||
<li>Feishu/Run channel fallback: prefer <code>Provider</code> over <code>Surface</code> when inferring queued run <code>messageProvider</code> fallback (when <code>OriginatingChannel</code> is missing), preventing Feishu turns from being mislabeled as <code>webchat</code> in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Skills/sherpa-onnx-tts: run the <code>sherpa-onnx-tts</code> bin under ESM (replace CommonJS <code>require</code> imports) and add regression coverage to prevent <code>require is not defined in ES module scope</code> startup crashes. (#31965) Thanks @bmendonca3.</li>
|
|
||||||
<li>Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.</li>
|
|
||||||
<li>Slack/Channel message subscriptions: register explicit <code>message.channels</code> and <code>message.groups</code> monitor handlers (alongside generic <code>message</code>) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.</li>
|
|
||||||
<li>Hooks/session-scoped memory context: expose ephemeral <code>sessionId</code> in embedded plugin tool contexts and <code>before_tool_call</code>/<code>after_tool_call</code> hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across <code>/new</code> and <code>/reset</code>. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.</li>
|
|
||||||
<li>Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.</li>
|
|
||||||
<li>Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.</li>
|
|
||||||
<li>Feishu/File upload filenames: percent-encode non-ASCII/special-character <code>file_name</code> values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.</li>
|
|
||||||
<li>Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized <code>kindFromMime</code> so mixed-case/parameterized MIME values classify consistently across message channels.</li>
|
|
||||||
<li>WhatsApp/inbound self-message context: propagate inbound <code>fromMe</code> through the web inbox pipeline and annotate direct self messages as <code>(self)</code> in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.</li>
|
|
||||||
<li>Webchat/stream finalization: persist streamed assistant text when final events omit <code>message</code>, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)</li>
|
|
||||||
<li>Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)</li>
|
|
||||||
<li>Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)</li>
|
|
||||||
<li>Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663)</li>
|
|
||||||
<li>Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured <code>LarkApiError</code> responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450)</li>
|
|
||||||
<li>Feishu/Missing-scope grant URL fix: rewrite known invalid scope aliases (<code>contact:contact.base:readonly</code>) to valid scope names in permission grant links, so remediation URLs open with correct Feishu consent scopes. (#31943)</li>
|
|
||||||
<li>BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound <code>message_id</code> selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.</li>
|
|
||||||
<li>WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.</li>
|
|
||||||
<li>Feishu/default account resolution: always honor explicit <code>channels.feishu.defaultAccount</code> during outbound account selection (including top-level-credential setups where the preferred id is not present in <code>accounts</code>), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.</li>
|
|
||||||
<li>Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (<code>contact:contact.base:readonly</code>) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)</li>
|
|
||||||
<li>Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)</li>
|
|
||||||
<li>Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.</li>
|
|
||||||
<li>Browser/Extension re-announce reliability: keep relay state in <code>connecting</code> when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.</li>
|
|
||||||
<li>Browser/Act request compatibility: accept legacy flattened <code>action="act"</code> params (<code>kind/ref/text/...</code>) in addition to <code>request={...}</code> so browser act calls no longer fail with <code>request required</code>. (#15120) Thanks @vincentkoc.</li>
|
|
||||||
<li>OpenRouter/x-ai compatibility: skip <code>reasoning.effort</code> injection for <code>x-ai/*</code> models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.</li>
|
|
||||||
<li>Models/openai-completions developer-role compatibility: force <code>supportsDeveloperRole=false</code> for non-native endpoints, treat unparseable <code>baseUrl</code> values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.</li>
|
|
||||||
<li>Browser/Profile attach-only override: support <code>browser.profiles.<name>.attachOnly</code> (fallback to global <code>browser.attachOnly</code>) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.</li>
|
|
||||||
<li>Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file <code>starttime</code> with <code>/proc/<pid>/stat</code> starttime, so stale <code>.jsonl.lock</code> files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.</li>
|
|
||||||
<li>Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel <code>resolveDefaultTo</code> fallback) when <code>delivery.to</code> is omitted. (#32364) Thanks @hclsys.</li>
|
|
||||||
<li>OpenAI media capabilities: include <code>audio</code> in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.</li>
|
|
||||||
<li>Browser/Managed tab cap: limit loopback managed <code>openclaw</code> page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.</li>
|
|
||||||
<li>Docker/Image health checks: add Dockerfile <code>HEALTHCHECK</code> that probes gateway <code>GET /healthz</code> so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.</li>
|
|
||||||
<li>Gateway/Node dangerous-command parity: include <code>sms.send</code> in default onboarding node <code>denyCommands</code>, share onboarding deny defaults with the gateway dangerous-command source of truth, and include <code>sms.send</code> in phone-control <code>/phone arm writes</code> handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.</li>
|
|
||||||
<li>Pairing/AllowFrom account fallback: handle omitted <code>accountId</code> values in <code>readChannelAllowFromStore</code> and <code>readChannelAllowFromStoreSync</code> as <code>default</code>, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.</li>
|
|
||||||
<li>Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.</li>
|
|
||||||
<li>Browser/CDP proxy bypass: force direct loopback agent paths and scoped <code>NO_PROXY</code> expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.</li>
|
|
||||||
<li>Sessions/idle reset correctness: preserve existing <code>updatedAt</code> during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.</li>
|
|
||||||
<li>Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing <code>starttime</code> when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.</li>
|
|
||||||
<li>Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (<code>mtimeMs</code> + <code>sizeBytes</code>), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.</li>
|
|
||||||
<li>Agents/Subagents <code>sessions_spawn</code>: reject malformed <code>agentId</code> inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.</li>
|
|
||||||
<li>CLI/installer Node preflight: enforce Node.js <code>v22.12+</code> consistently in both <code>openclaw.mjs</code> runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.</li>
|
|
||||||
<li>Web UI/config form: support SecretInput string-or-secret-ref unions in map <code>additionalProperties</code>, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.</li>
|
|
||||||
<li>Auto-reply/inline command cleanup: preserve newline structure when stripping inline <code>/status</code> and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.</li>
|
|
||||||
<li>Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like <code>source</code>/<code>provider</code>), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.</li>
|
|
||||||
<li>Hooks/runtime stability: keep the internal hook handler registry on a <code>globalThis</code> singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.</li>
|
|
||||||
<li>Hooks/after_tool_call: include embedded session context (<code>sessionKey</code>, <code>agentId</code>) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.</li>
|
|
||||||
<li>Hooks/tool-call correlation: include <code>runId</code> and <code>toolCallId</code> in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in <code>before_tool_call</code> and <code>after_tool_call</code>. (#32360) Thanks @vincentkoc.</li>
|
|
||||||
<li>Plugins/install diagnostics: reject legacy plugin package shapes without <code>openclaw.extensions</code> and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.</li>
|
|
||||||
<li>Hooks/plugin context parity: ensure <code>llm_input</code> hooks in embedded attempts receive the same <code>trigger</code> and <code>channelId</code>-aware <code>hookCtx</code> used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.</li>
|
|
||||||
<li>Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (<code>pnpm</code>, <code>bun</code>) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.</li>
|
|
||||||
<li>Cron/session reaper reliability: move cron session reaper sweeps into <code>onTimer</code> <code>finally</code> and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.</li>
|
|
||||||
<li>Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so <code>HEARTBEAT_OK</code> noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.</li>
|
|
||||||
<li>Authentication: classify <code>permission_error</code> as <code>auth_permanent</code> for profile fallback. (#31324) Thanks @Sid-Qin.</li>
|
|
||||||
<li>Agents/host edit reliability: treat host edit-tool throws as success only when on-disk post-check confirms replacement likely happened (<code>newText</code> present and <code>oldText</code> absent), preventing false failure reports while avoiding pre-write false positives. (#32383) Thanks @polooooo.</li>
|
|
||||||
<li>Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example <code>diffs</code> -> bundled <code>@openclaw/diffs</code>), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.</li>
|
|
||||||
<li>Web UI/inline code copy fidelity: disable forced mid-token wraps on inline <code><code></code> spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.</li>
|
|
||||||
<li>Restart sentinel formatting: avoid duplicate <code>Reason:</code> lines when restart message text already matches <code>stats.reason</code>, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.</li>
|
|
||||||
<li>Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.</li>
|
|
||||||
<li>Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.</li>
|
|
||||||
<li>Failover/error classification: treat HTTP <code>529</code> (provider overloaded, common with Anthropic-compatible APIs) as <code>rate_limit</code> so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.</li>
|
|
||||||
<li>Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.</li>
|
|
||||||
<li>Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.</li>
|
|
||||||
<li>Secrets/exec resolver timeout defaults: use provider <code>timeoutMs</code> as the default inactivity (<code>noOutputTimeoutMs</code>) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.</li>
|
|
||||||
<li>Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.</li>
|
|
||||||
<li>Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing <code>HEARTBEAT_OK</code> from being delivered to users. (#32131) Thanks @adhishthite.</li>
|
|
||||||
<li>Cron/store migration: normalize legacy cron jobs with string <code>schedule</code> and top-level <code>command</code>/<code>timeout</code> fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.</li>
|
|
||||||
<li>Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.</li>
|
|
||||||
<li>Tests/Subagent announce: set <code>OPENCLAW_TEST_FAST=1</code> before importing <code>subagent-announce</code> format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.</li>
|
|
||||||
</ul>
|
|
||||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
|
||||||
]]></description>
|
|
||||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.2/OpenClaw-2026.3.2.zip" length="23181513" type="application/octet-stream" sparkle:edSignature="THMgkcoMgz2vv5zse3Po3K7l3Or2RhBKurXZIi8iYVXN76yJy1YXAY6kXi6ovD+dbYn68JKYDIKA1Ya78bO7BQ=="/>
|
|
||||||
<!-- pragma: allowlist secret -->
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -382,6 +382,7 @@
|
|||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
"osc-progress": "^0.3.0",
|
"osc-progress": "^0.3.0",
|
||||||
"pdfjs-dist": "^5.5.207",
|
"pdfjs-dist": "^5.5.207",
|
||||||
|
"pg": "^8.20.0",
|
||||||
"playwright-core": "1.58.2",
|
"playwright-core": "1.58.2",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
@@ -400,11 +401,13 @@
|
|||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/pg": "^8.18.0",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||||
"@vitest/coverage-v8": "^4.1.0",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
"jscpd": "4.0.8",
|
"jscpd": "4.0.8",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
"lit": "^3.3.2",
|
"lit": "^3.3.2",
|
||||||
"oxfmt": "0.40.0",
|
"oxfmt": "0.40.0",
|
||||||
"oxlint": "^1.55.0",
|
"oxlint": "^1.55.0",
|
||||||
|
|||||||
368
pnpm-lock.yaml
generated
368
pnpm-lock.yaml
generated
@@ -163,6 +163,9 @@ importers:
|
|||||||
pdfjs-dist:
|
pdfjs-dist:
|
||||||
specifier: ^5.5.207
|
specifier: ^5.5.207
|
||||||
version: 5.5.207
|
version: 5.5.207
|
||||||
|
pg:
|
||||||
|
specifier: ^8.20.0
|
||||||
|
version: 8.20.0
|
||||||
playwright-core:
|
playwright-core:
|
||||||
specifier: 1.58.2
|
specifier: 1.58.2
|
||||||
version: 1.58.2
|
version: 1.58.2
|
||||||
@@ -212,6 +215,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.0
|
specifier: ^25.5.0
|
||||||
version: 25.5.0
|
version: 25.5.0
|
||||||
|
'@types/pg':
|
||||||
|
specifier: ^8.18.0
|
||||||
|
version: 8.18.0
|
||||||
'@types/qrcode-terminal':
|
'@types/qrcode-terminal':
|
||||||
specifier: ^0.12.2
|
specifier: ^0.12.2
|
||||||
version: 0.12.2
|
version: 0.12.2
|
||||||
@@ -227,6 +233,9 @@ importers:
|
|||||||
jscpd:
|
jscpd:
|
||||||
specifier: 4.0.8
|
specifier: 4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
|
jsdom:
|
||||||
|
specifier: ^28.1.0
|
||||||
|
version: 28.1.0(@noble/hashes@2.0.1)
|
||||||
lit:
|
lit:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@@ -648,10 +657,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw==}
|
resolution: {integrity: sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/client-bedrock@3.1007.0':
|
|
||||||
resolution: {integrity: sha512-49hH8o6ALKkCiBUgg20HkwxNamP1yYA/n8Si73Z438EqhZGpCfScP3FfxVhrfD5o+4bV4Whi9BTzPKCa/PfUww==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/client-bedrock@3.1008.0':
|
'@aws-sdk/client-bedrock@3.1008.0':
|
||||||
resolution: {integrity: sha512-mzxO/DplpZZT7AIZUCG7Q78OlaeHeDybYz+ZlWZPaXFjGDJwUv1E3SKskmaaQvTsMeieie0WX7gzueYrCx4YfQ==}
|
resolution: {integrity: sha512-mzxO/DplpZZT7AIZUCG7Q78OlaeHeDybYz+ZlWZPaXFjGDJwUv1E3SKskmaaQvTsMeieie0WX7gzueYrCx4YfQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -708,10 +713,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==}
|
resolution: {integrity: sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.18':
|
|
||||||
resolution: {integrity: sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.19':
|
'@aws-sdk/credential-provider-ini@3.972.19':
|
||||||
resolution: {integrity: sha512-pVJVjWqVrPqjpFq7o0mCmeZu1Y0c94OCHSYgivdCD2wfmYVtBbwQErakruhgOD8pcMcx9SCqRw1pzHKR7OGBcA==}
|
resolution: {integrity: sha512-pVJVjWqVrPqjpFq7o0mCmeZu1Y0c94OCHSYgivdCD2wfmYVtBbwQErakruhgOD8pcMcx9SCqRw1pzHKR7OGBcA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -724,10 +725,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==}
|
resolution: {integrity: sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.18':
|
|
||||||
resolution: {integrity: sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.19':
|
'@aws-sdk/credential-provider-login@3.972.19':
|
||||||
resolution: {integrity: sha512-jOXdZ1o+CywQKr6gyxgxuUmnGwTTnY2Kxs1PM7fI6AYtDWDnmW/yKXayNqkF8KjP1unflqMWKVbVt5VgmE3L0g==}
|
resolution: {integrity: sha512-jOXdZ1o+CywQKr6gyxgxuUmnGwTTnY2Kxs1PM7fI6AYtDWDnmW/yKXayNqkF8KjP1unflqMWKVbVt5VgmE3L0g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -740,10 +737,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==}
|
resolution: {integrity: sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.19':
|
|
||||||
resolution: {integrity: sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.20':
|
'@aws-sdk/credential-provider-node@3.972.20':
|
||||||
resolution: {integrity: sha512-0xHca2BnPY0kzjDYPH7vk8YbfdBPpWVS67rtqQMalYDQUCBYS37cZ55K6TuFxCoIyNZgSCFrVKr9PXC5BVvQQw==}
|
resolution: {integrity: sha512-0xHca2BnPY0kzjDYPH7vk8YbfdBPpWVS67rtqQMalYDQUCBYS37cZ55K6TuFxCoIyNZgSCFrVKr9PXC5BVvQQw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -768,10 +761,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==}
|
resolution: {integrity: sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.18':
|
|
||||||
resolution: {integrity: sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.19':
|
'@aws-sdk/credential-provider-sso@3.972.19':
|
||||||
resolution: {integrity: sha512-kVjQsEU3b///q7EZGrUzol9wzwJFKbEzqJKSq82A9ShrUTEO7FNylTtby3sPV19ndADZh1H3FB3+5ZrvKtEEeg==}
|
resolution: {integrity: sha512-kVjQsEU3b///q7EZGrUzol9wzwJFKbEzqJKSq82A9ShrUTEO7FNylTtby3sPV19ndADZh1H3FB3+5ZrvKtEEeg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -784,10 +773,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==}
|
resolution: {integrity: sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.18':
|
|
||||||
resolution: {integrity: sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.19':
|
'@aws-sdk/credential-provider-web-identity@3.972.19':
|
||||||
resolution: {integrity: sha512-BV1BlTFdG4w4tAihxN7iXDBoNcNewXD4q8uZlNQiUrnqxwGWUhKHODIQVSPlQGxXClEj+63m+cqZskw+ESmeZg==}
|
resolution: {integrity: sha512-BV1BlTFdG4w4tAihxN7iXDBoNcNewXD4q8uZlNQiUrnqxwGWUhKHODIQVSPlQGxXClEj+63m+cqZskw+ESmeZg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -872,10 +857,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==}
|
resolution: {integrity: sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.996.8':
|
|
||||||
resolution: {integrity: sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.996.9':
|
'@aws-sdk/nested-clients@3.996.9':
|
||||||
resolution: {integrity: sha512-+RpVtpmQbbtzFOKhMlsRcXM/3f1Z49qTOHaA8gEpHOYruERmog6f2AUtf/oTRLCWjR9H2b3roqryV/hI7QMW8w==}
|
resolution: {integrity: sha512-+RpVtpmQbbtzFOKhMlsRcXM/3f1Z49qTOHaA8gEpHOYruERmog6f2AUtf/oTRLCWjR9H2b3roqryV/hI7QMW8w==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -900,14 +881,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==}
|
resolution: {integrity: sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1005.0':
|
|
||||||
resolution: {integrity: sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1007.0':
|
|
||||||
resolution: {integrity: sha512-kKvVyr53vvVc5k6RbvI6jhafxufxO2SkEw8QeEzJqwOXH/IMY7Cm0IyhnBGdqj80iiIIiIM2jGe7Fn3TIdwdrw==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1008.0':
|
'@aws-sdk/token-providers@3.1008.0':
|
||||||
resolution: {integrity: sha512-TulwlHQBWcJs668kNUDMZHN51DeLrDsYT59Ux4a/nbvr025gM6HjKJJ3LvnZccam7OS/ZKUVkWomCneRQKJbBg==}
|
resolution: {integrity: sha512-TulwlHQBWcJs668kNUDMZHN51DeLrDsYT59Ux4a/nbvr025gM6HjKJJ3LvnZccam7OS/ZKUVkWomCneRQKJbBg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -976,15 +949,6 @@ packages:
|
|||||||
aws-crt:
|
aws-crt:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.5':
|
|
||||||
resolution: {integrity: sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
aws-crt: '>=1.0.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
aws-crt:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.6':
|
'@aws-sdk/util-user-agent-node@3.973.6':
|
||||||
resolution: {integrity: sha512-iF7G0prk7AvmOK64FcLvc/fW+Ty1H+vttajL7PvJFReU8urMxfYmynTTuFKDTA76Wgpq3FzTPKwabMQIXQHiXQ==}
|
resolution: {integrity: sha512-iF7G0prk7AvmOK64FcLvc/fW+Ty1H+vttajL7PvJFReU8urMxfYmynTTuFKDTA76Wgpq3FzTPKwabMQIXQHiXQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -3583,6 +3547,9 @@ packages:
|
|||||||
'@types/node@25.5.0':
|
'@types/node@25.5.0':
|
||||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||||
|
|
||||||
|
'@types/pg@8.18.0':
|
||||||
|
resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==}
|
||||||
|
|
||||||
'@types/qrcode-terminal@0.12.2':
|
'@types/qrcode-terminal@0.12.2':
|
||||||
resolution: {integrity: sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==}
|
resolution: {integrity: sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==}
|
||||||
|
|
||||||
@@ -5842,6 +5809,40 @@ packages:
|
|||||||
performance-now@2.1.0:
|
performance-now@2.1.0:
|
||||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||||
|
|
||||||
|
pg-connection-string@2.12.0:
|
||||||
|
resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
|
||||||
|
|
||||||
|
pg-int8@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pg-pool@3.13.0:
|
||||||
|
resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
|
||||||
|
peerDependencies:
|
||||||
|
pg: '>=8.0'
|
||||||
|
|
||||||
|
pg-protocol@1.13.0:
|
||||||
|
resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pg@8.20.0:
|
||||||
|
resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
|
||||||
|
engines: {node: '>= 16.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
pg-native: '>=3.0.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
pg-native:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -5889,6 +5890,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postgres-array@2.0.0:
|
||||||
|
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1:
|
||||||
|
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-date@1.0.7:
|
||||||
|
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
postgres@3.4.8:
|
postgres@3.4.8:
|
||||||
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -6664,10 +6681,6 @@ packages:
|
|||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
undici@7.22.0:
|
|
||||||
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
|
|
||||||
engines: {node: '>=20.18.1'}
|
|
||||||
|
|
||||||
undici@7.24.0:
|
undici@7.24.0:
|
||||||
resolution: {integrity: sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==}
|
resolution: {integrity: sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
@@ -6925,6 +6938,10 @@ packages:
|
|||||||
xmlchars@2.2.0:
|
xmlchars@2.2.0:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -7117,51 +7134,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/client-bedrock@3.1007.0':
|
|
||||||
dependencies:
|
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
|
||||||
'@aws-crypto/sha256-js': 5.2.0
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/credential-provider-node': 3.972.19
|
|
||||||
'@aws-sdk/middleware-host-header': 3.972.7
|
|
||||||
'@aws-sdk/middleware-logger': 3.972.7
|
|
||||||
'@aws-sdk/middleware-recursion-detection': 3.972.7
|
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.20
|
|
||||||
'@aws-sdk/region-config-resolver': 3.972.7
|
|
||||||
'@aws-sdk/token-providers': 3.1007.0
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@aws-sdk/util-endpoints': 3.996.4
|
|
||||||
'@aws-sdk/util-user-agent-browser': 3.972.7
|
|
||||||
'@aws-sdk/util-user-agent-node': 3.973.5
|
|
||||||
'@smithy/config-resolver': 4.4.10
|
|
||||||
'@smithy/core': 3.23.9
|
|
||||||
'@smithy/fetch-http-handler': 5.3.13
|
|
||||||
'@smithy/hash-node': 4.2.11
|
|
||||||
'@smithy/invalid-dependency': 4.2.11
|
|
||||||
'@smithy/middleware-content-length': 4.2.11
|
|
||||||
'@smithy/middleware-endpoint': 4.4.23
|
|
||||||
'@smithy/middleware-retry': 4.4.40
|
|
||||||
'@smithy/middleware-serde': 4.2.12
|
|
||||||
'@smithy/middleware-stack': 4.2.11
|
|
||||||
'@smithy/node-config-provider': 4.3.11
|
|
||||||
'@smithy/node-http-handler': 4.4.14
|
|
||||||
'@smithy/protocol-http': 5.3.11
|
|
||||||
'@smithy/smithy-client': 4.12.3
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
'@smithy/url-parser': 4.2.11
|
|
||||||
'@smithy/util-base64': 4.3.2
|
|
||||||
'@smithy/util-body-length-browser': 4.2.2
|
|
||||||
'@smithy/util-body-length-node': 4.2.3
|
|
||||||
'@smithy/util-defaults-mode-browser': 4.3.39
|
|
||||||
'@smithy/util-defaults-mode-node': 4.2.42
|
|
||||||
'@smithy/util-endpoints': 3.3.2
|
|
||||||
'@smithy/util-middleware': 4.2.11
|
|
||||||
'@smithy/util-retry': 4.2.11
|
|
||||||
'@smithy/util-utf8': 4.2.2
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/client-bedrock@3.1008.0':
|
'@aws-sdk/client-bedrock@3.1008.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
'@aws-crypto/sha256-browser': 5.2.0
|
||||||
@@ -7421,25 +7393,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.18':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/credential-provider-env': 3.972.17
|
|
||||||
'@aws-sdk/credential-provider-http': 3.972.19
|
|
||||||
'@aws-sdk/credential-provider-login': 3.972.18
|
|
||||||
'@aws-sdk/credential-provider-process': 3.972.17
|
|
||||||
'@aws-sdk/credential-provider-sso': 3.972.18
|
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.18
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/credential-provider-imds': 4.2.11
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.19':
|
'@aws-sdk/credential-provider-ini@3.972.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.973.19
|
'@aws-sdk/core': 3.973.19
|
||||||
@@ -7485,19 +7438,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.18':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/protocol-http': 5.3.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.19':
|
'@aws-sdk/credential-provider-login@3.972.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.973.19
|
'@aws-sdk/core': 3.973.19
|
||||||
@@ -7545,23 +7485,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.19':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/credential-provider-env': 3.972.17
|
|
||||||
'@aws-sdk/credential-provider-http': 3.972.19
|
|
||||||
'@aws-sdk/credential-provider-ini': 3.972.18
|
|
||||||
'@aws-sdk/credential-provider-process': 3.972.17
|
|
||||||
'@aws-sdk/credential-provider-sso': 3.972.18
|
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.18
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/credential-provider-imds': 4.2.11
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.20':
|
'@aws-sdk/credential-provider-node@3.972.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/credential-provider-env': 3.972.17
|
'@aws-sdk/credential-provider-env': 3.972.17
|
||||||
@@ -7632,19 +7555,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.18':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/token-providers': 3.1005.0
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.19':
|
'@aws-sdk/credential-provider-sso@3.972.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.973.19
|
'@aws-sdk/core': 3.973.19
|
||||||
@@ -7682,18 +7592,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.18':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.19':
|
'@aws-sdk/credential-provider-web-identity@3.972.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.973.19
|
'@aws-sdk/core': 3.973.19
|
||||||
@@ -7958,49 +7856,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.996.8':
|
|
||||||
dependencies:
|
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
|
||||||
'@aws-crypto/sha256-js': 5.2.0
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/middleware-host-header': 3.972.7
|
|
||||||
'@aws-sdk/middleware-logger': 3.972.7
|
|
||||||
'@aws-sdk/middleware-recursion-detection': 3.972.7
|
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.20
|
|
||||||
'@aws-sdk/region-config-resolver': 3.972.7
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@aws-sdk/util-endpoints': 3.996.4
|
|
||||||
'@aws-sdk/util-user-agent-browser': 3.972.7
|
|
||||||
'@aws-sdk/util-user-agent-node': 3.973.5
|
|
||||||
'@smithy/config-resolver': 4.4.10
|
|
||||||
'@smithy/core': 3.23.9
|
|
||||||
'@smithy/fetch-http-handler': 5.3.13
|
|
||||||
'@smithy/hash-node': 4.2.11
|
|
||||||
'@smithy/invalid-dependency': 4.2.11
|
|
||||||
'@smithy/middleware-content-length': 4.2.11
|
|
||||||
'@smithy/middleware-endpoint': 4.4.23
|
|
||||||
'@smithy/middleware-retry': 4.4.40
|
|
||||||
'@smithy/middleware-serde': 4.2.12
|
|
||||||
'@smithy/middleware-stack': 4.2.11
|
|
||||||
'@smithy/node-config-provider': 4.3.11
|
|
||||||
'@smithy/node-http-handler': 4.4.14
|
|
||||||
'@smithy/protocol-http': 5.3.11
|
|
||||||
'@smithy/smithy-client': 4.12.3
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
'@smithy/url-parser': 4.2.11
|
|
||||||
'@smithy/util-base64': 4.3.2
|
|
||||||
'@smithy/util-body-length-browser': 4.2.2
|
|
||||||
'@smithy/util-body-length-node': 4.2.3
|
|
||||||
'@smithy/util-defaults-mode-browser': 4.3.39
|
|
||||||
'@smithy/util-defaults-mode-node': 4.2.42
|
|
||||||
'@smithy/util-endpoints': 3.3.2
|
|
||||||
'@smithy/util-middleware': 4.2.11
|
|
||||||
'@smithy/util-retry': 4.2.11
|
|
||||||
'@smithy/util-utf8': 4.2.2
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.996.9':
|
'@aws-sdk/nested-clients@3.996.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 5.2.0
|
'@aws-crypto/sha256-browser': 5.2.0
|
||||||
@@ -8092,30 +7947,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1005.0':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1007.0':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.973.19
|
|
||||||
'@aws-sdk/nested-clients': 3.996.8
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/property-provider': 4.2.11
|
|
||||||
'@smithy/shared-ini-file-loader': 4.4.6
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- aws-crt
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1008.0':
|
'@aws-sdk/token-providers@3.1008.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.973.19
|
'@aws-sdk/core': 3.973.19
|
||||||
@@ -8222,14 +8053,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.13.0
|
'@smithy/types': 4.13.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.5':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.20
|
|
||||||
'@aws-sdk/types': 3.973.5
|
|
||||||
'@smithy/node-config-provider': 4.3.11
|
|
||||||
'@smithy/types': 4.13.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/util-user-agent-node@3.973.6':
|
'@aws-sdk/util-user-agent-node@3.973.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/middleware-user-agent': 3.972.20
|
'@aws-sdk/middleware-user-agent': 3.972.20
|
||||||
@@ -11164,6 +10987,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
|
'@types/pg@8.18.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.5.0
|
||||||
|
pg-protocol: 1.13.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
|
||||||
'@types/qrcode-terminal@0.12.2': {}
|
'@types/qrcode-terminal@0.12.2': {}
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
@@ -13497,7 +13326,7 @@ snapshots:
|
|||||||
openclaw@2026.3.11(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3)):
|
openclaw@2026.3.11(@discordjs/opus@0.10.0)(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(node-llama-cpp@3.16.2(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@agentclientprotocol/sdk': 0.16.1(zod@4.3.6)
|
'@agentclientprotocol/sdk': 0.16.1(zod@4.3.6)
|
||||||
'@aws-sdk/client-bedrock': 3.1007.0
|
'@aws-sdk/client-bedrock': 3.1008.0
|
||||||
'@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.7)(opusscript@0.1.1)
|
'@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.7)(opusscript@0.1.1)
|
||||||
'@clack/prompts': 1.1.0
|
'@clack/prompts': 1.1.0
|
||||||
'@discordjs/voice': 0.19.1(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
'@discordjs/voice': 0.19.1(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||||
@@ -13548,7 +13377,7 @@ snapshots:
|
|||||||
sqlite-vec: 0.1.7-alpha.2
|
sqlite-vec: 0.1.7-alpha.2
|
||||||
tar: 7.5.11
|
tar: 7.5.11
|
||||||
tslog: 4.10.2
|
tslog: 4.10.2
|
||||||
undici: 7.22.0
|
undici: 7.24.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
@@ -13753,6 +13582,41 @@ snapshots:
|
|||||||
|
|
||||||
performance-now@2.1.0: {}
|
performance-now@2.1.0: {}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-connection-string@2.12.0: {}
|
||||||
|
|
||||||
|
pg-int8@1.0.1: {}
|
||||||
|
|
||||||
|
pg-pool@3.13.0(pg@8.20.0):
|
||||||
|
dependencies:
|
||||||
|
pg: 8.20.0
|
||||||
|
|
||||||
|
pg-protocol@1.13.0: {}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
pg-int8: 1.0.1
|
||||||
|
postgres-array: 2.0.0
|
||||||
|
postgres-bytea: 1.0.1
|
||||||
|
postgres-date: 1.0.7
|
||||||
|
postgres-interval: 1.2.0
|
||||||
|
|
||||||
|
pg@8.20.0:
|
||||||
|
dependencies:
|
||||||
|
pg-connection-string: 2.12.0
|
||||||
|
pg-pool: 3.13.0(pg@8.20.0)
|
||||||
|
pg-protocol: 1.13.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
pgpass: 1.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
pg-cloudflare: 1.3.0
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
dependencies:
|
||||||
|
split2: 4.2.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -13803,6 +13667,16 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
postgres-array@2.0.0: {}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1: {}
|
||||||
|
|
||||||
|
postgres-date@1.0.7: {}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
|
||||||
postgres@3.4.8: {}
|
postgres@3.4.8: {}
|
||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
@@ -14722,8 +14596,6 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
undici@7.22.0: {}
|
|
||||||
|
|
||||||
undici@7.24.0: {}
|
undici@7.24.0: {}
|
||||||
|
|
||||||
unist-util-is@6.0.1:
|
unist-util-is@6.0.1:
|
||||||
@@ -14922,6 +14794,8 @@ snapshots:
|
|||||||
|
|
||||||
xmlchars@2.2.0: {}
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@4.0.0: {}
|
yallist@4.0.0: {}
|
||||||
|
|||||||
11
scripts/claw-broker/.env.example
Normal file
11
scripts/claw-broker/.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CLAW_BROKER_BIND=127.0.0.1
|
||||||
|
CLAW_BROKER_PORT=8787
|
||||||
|
CLAW_BROKER_TOKEN=change-me
|
||||||
|
CLAW_BROKER_CMD_TIMEOUT_MS=120000
|
||||||
|
CLAW_BROKER_MAX_SUMMARY_CHARS=2000
|
||||||
|
|
||||||
|
PGHOST=147.45.189.234
|
||||||
|
PGPORT=5432
|
||||||
|
PGDATABASE=default_db
|
||||||
|
PGUSER=gen_user
|
||||||
|
PGPASSWORD=change-me
|
||||||
43
scripts/claw-broker/README.md
Normal file
43
scripts/claw-broker/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Claw Broker (MVP)
|
||||||
|
|
||||||
|
Minimal privileged broker for claw.approvals.execute.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- POST /v1/execute
|
||||||
|
- Bearer token via CLAW_BROKER_TOKEN
|
||||||
|
|
||||||
|
Request fields:
|
||||||
|
|
||||||
|
- executionId
|
||||||
|
- approvalRequestId
|
||||||
|
- approvalGrantId
|
||||||
|
- exactCommand
|
||||||
|
- targetHost
|
||||||
|
- targetUser
|
||||||
|
- requestedBy
|
||||||
|
- channel
|
||||||
|
- chatId
|
||||||
|
- humanUserId
|
||||||
|
- sessionId
|
||||||
|
|
||||||
|
Response fields:
|
||||||
|
|
||||||
|
- executionId
|
||||||
|
- status
|
||||||
|
- exitCode
|
||||||
|
- stdoutSummary
|
||||||
|
- stderrSummary
|
||||||
|
- startedAt
|
||||||
|
- finishedAt
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Broker re-checks in Postgres before execution:
|
||||||
|
|
||||||
|
- request/grant exist
|
||||||
|
- status allows execution
|
||||||
|
- once grant atomic consume
|
||||||
|
- command exact match
|
||||||
|
- scope match (targetHost, targetUser, channel, chatId, humanUserId, sessionId)
|
||||||
|
- dangerous shell policy
|
||||||
437
scripts/claw-broker/broker.mjs
Normal file
437
scripts/claw-broker/broker.mjs
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import http from "node:http";
|
||||||
|
import pg from "pg";
|
||||||
|
|
||||||
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
const MAX_SUMMARY_CHARS = Number(process.env.CLAW_BROKER_MAX_SUMMARY_CHARS ?? "2000");
|
||||||
|
const CMD_TIMEOUT_MS = Number(process.env.CLAW_BROKER_CMD_TIMEOUT_MS ?? "120000");
|
||||||
|
const BIND_HOST = process.env.CLAW_BROKER_BIND ?? "127.0.0.1";
|
||||||
|
const BIND_PORT = Number(process.env.CLAW_BROKER_PORT ?? "8787");
|
||||||
|
const REQUIRED_TOKEN = (process.env.CLAW_BROKER_TOKEN ?? "").trim();
|
||||||
|
|
||||||
|
function env(name, fallback = undefined) {
|
||||||
|
return process.env[`CLAW_${name}`] ?? process.env[name] ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredEnv(name) {
|
||||||
|
const value = env(name, "");
|
||||||
|
if (!value || !String(value).trim()) {
|
||||||
|
throw new Error(`missing env: ${name} (or CLAW_${name})`);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: requiredEnv("PGHOST"),
|
||||||
|
port: Number(env("PGPORT", "5432")),
|
||||||
|
user: requiredEnv("PGUSER"),
|
||||||
|
password: requiredEnv("PGPASSWORD"),
|
||||||
|
database: requiredEnv("PGDATABASE"),
|
||||||
|
max: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!REQUIRED_TOKEN) {
|
||||||
|
throw new Error("missing CLAW_BROKER_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function json(res, code, body) {
|
||||||
|
const payload = JSON.stringify(body);
|
||||||
|
res.writeHead(code, {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"content-length": Buffer.byteLength(payload),
|
||||||
|
});
|
||||||
|
res.end(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCommand(input) {
|
||||||
|
return String(input).trim().replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDangerousShellConstruct(command) {
|
||||||
|
const source = String(command).toLowerCase();
|
||||||
|
const checks = [
|
||||||
|
/\bbash\s+-c\b/,
|
||||||
|
/\bsh\s+-c\b/,
|
||||||
|
/\bsudo\s+su\b/,
|
||||||
|
/\bsudo\s+-i\b/,
|
||||||
|
/&&/,
|
||||||
|
/\|\|/,
|
||||||
|
/;/,
|
||||||
|
/\|/,
|
||||||
|
/>/,
|
||||||
|
/</,
|
||||||
|
/\$\(/,
|
||||||
|
/`/,
|
||||||
|
/<<[-\w]*/,
|
||||||
|
];
|
||||||
|
return checks.some((r) => r.test(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(text) {
|
||||||
|
const value = String(text ?? "");
|
||||||
|
return value.length <= MAX_SUMMARY_CHARS ? value : `${value.slice(0, MAX_SUMMARY_CHARS)}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertAudit(client, args) {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO claw_audit_events (
|
||||||
|
event_type, request_id, grant_id, execution_id,
|
||||||
|
actor_type, actor_id, target_host, target_user,
|
||||||
|
command_snapshot, status, exit_code, stdout_summary, stderr_summary, metadata
|
||||||
|
) VALUES (
|
||||||
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb
|
||||||
|
)`,
|
||||||
|
[
|
||||||
|
args.eventType,
|
||||||
|
args.requestId ?? null,
|
||||||
|
args.grantId ?? null,
|
||||||
|
args.executionId ?? null,
|
||||||
|
args.actorType,
|
||||||
|
args.actorId,
|
||||||
|
args.targetHost ?? null,
|
||||||
|
args.targetUser ?? null,
|
||||||
|
args.commandSnapshot ?? null,
|
||||||
|
args.status ?? null,
|
||||||
|
args.exitCode ?? null,
|
||||||
|
args.stdoutSummary ?? null,
|
||||||
|
args.stderrSummary ?? null,
|
||||||
|
JSON.stringify(args.metadata ?? {}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireString(body, key) {
|
||||||
|
const value = body?.[key];
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
throw new Error(`${key} is required`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyAndMarkStarted(body) {
|
||||||
|
const executionId = requireString(body, "executionId");
|
||||||
|
const approvalRequestId = requireString(body, "approvalRequestId");
|
||||||
|
const approvalGrantId = requireString(body, "approvalGrantId");
|
||||||
|
const exactCommand = requireString(body, "exactCommand");
|
||||||
|
const targetHost = requireString(body, "targetHost");
|
||||||
|
const targetUser = requireString(body, "targetUser");
|
||||||
|
const requestedBy = requireString(body, "requestedBy");
|
||||||
|
const channel = requireString(body, "channel");
|
||||||
|
const chatId = requireString(body, "chatId");
|
||||||
|
const humanUserId = requireString(body, "humanUserId");
|
||||||
|
const sessionId = requireString(body, "sessionId");
|
||||||
|
|
||||||
|
if (hasDangerousShellConstruct(exactCommand)) {
|
||||||
|
throw new Error("dangerous shell policy violation");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
const reqRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_requests WHERE id = $1 FOR UPDATE`,
|
||||||
|
[approvalRequestId],
|
||||||
|
);
|
||||||
|
if (reqRes.rowCount === 0) {
|
||||||
|
throw new Error("approval request not found");
|
||||||
|
}
|
||||||
|
const request = reqRes.rows[0];
|
||||||
|
|
||||||
|
if (!["approved_once", "approved_always"].includes(String(request.status))) {
|
||||||
|
throw new Error(`request status does not allow execution: ${request.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grantRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_grants WHERE id = $1 AND request_id = $2 FOR UPDATE`,
|
||||||
|
[approvalGrantId, approvalRequestId],
|
||||||
|
);
|
||||||
|
if (grantRes.rowCount === 0) {
|
||||||
|
throw new Error("approval grant not found");
|
||||||
|
}
|
||||||
|
const grant = grantRes.rows[0];
|
||||||
|
|
||||||
|
const dbExact = String(request.exact_command);
|
||||||
|
if (normalizeCommand(dbExact) !== normalizeCommand(exactCommand)) {
|
||||||
|
throw new Error("exact command mismatch");
|
||||||
|
}
|
||||||
|
if (normalizeCommand(String(grant.exact_command)) !== normalizeCommand(exactCommand)) {
|
||||||
|
throw new Error("grant command mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeChecks = [
|
||||||
|
[String(request.target_host), targetHost, "targetHost"],
|
||||||
|
[String(request.target_user), targetUser, "targetUser"],
|
||||||
|
[String(request.channel), channel, "channel"],
|
||||||
|
[String(request.chat_id), chatId, "chatId"],
|
||||||
|
[String(request.human_user_id), humanUserId, "humanUserId"],
|
||||||
|
[String(request.session_id), sessionId, "sessionId"],
|
||||||
|
[String(grant.target_host), targetHost, "grant.targetHost"],
|
||||||
|
[String(grant.target_user), targetUser, "grant.targetUser"],
|
||||||
|
[String(grant.channel), channel, "grant.channel"],
|
||||||
|
[String(grant.chat_id), chatId, "grant.chatId"],
|
||||||
|
[String(grant.human_user_id), humanUserId, "grant.humanUserId"],
|
||||||
|
[String(grant.session_id), sessionId, "grant.sessionId"],
|
||||||
|
];
|
||||||
|
for (const [db, incoming, label] of scopeChecks) {
|
||||||
|
if (db !== incoming) {
|
||||||
|
throw new Error(`scope mismatch: ${label}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDangerousShellConstruct(String(request.exact_command))) {
|
||||||
|
throw new Error("dangerous shell policy violation (request)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(grant.grant_type) === "once") {
|
||||||
|
const consumeRes = await client.query(
|
||||||
|
`UPDATE claw_approval_grants
|
||||||
|
SET used_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
AND grant_type = 'once'
|
||||||
|
AND used_at IS NULL
|
||||||
|
AND revoked_at IS NULL
|
||||||
|
AND expires_at > now()
|
||||||
|
RETURNING id`,
|
||||||
|
[approvalGrantId],
|
||||||
|
);
|
||||||
|
if (consumeRes.rowCount === 0) {
|
||||||
|
throw new Error("once grant expired/revoked/already used");
|
||||||
|
}
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "grant_consumed",
|
||||||
|
actorType: "broker",
|
||||||
|
actorId: requestedBy,
|
||||||
|
requestId: approvalRequestId,
|
||||||
|
grantId: approvalGrantId,
|
||||||
|
executionId,
|
||||||
|
targetHost,
|
||||||
|
targetUser,
|
||||||
|
commandSnapshot: exactCommand,
|
||||||
|
status: "grant_consumed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE claw_approval_requests SET execution_id = $2, updated_at = now() WHERE id = $1`,
|
||||||
|
[approvalRequestId, executionId],
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "execution_started",
|
||||||
|
actorType: "broker",
|
||||||
|
actorId: requestedBy,
|
||||||
|
requestId: approvalRequestId,
|
||||||
|
grantId: approvalGrantId,
|
||||||
|
executionId,
|
||||||
|
targetHost,
|
||||||
|
targetUser,
|
||||||
|
commandSnapshot: exactCommand,
|
||||||
|
status: "execution_started",
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return {
|
||||||
|
executionId,
|
||||||
|
approvalRequestId,
|
||||||
|
approvalGrantId,
|
||||||
|
exactCommand,
|
||||||
|
targetHost,
|
||||||
|
targetUser,
|
||||||
|
requestedBy,
|
||||||
|
cwd: request.cwd ? String(request.cwd) : undefined,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(command, cwd) {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const child = spawn("bash", ["-lc", command], {
|
||||||
|
cwd: cwd || undefined,
|
||||||
|
env: process.env,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let timedOut = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
}, CMD_TIMEOUT_MS);
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({
|
||||||
|
exitCode: timedOut ? 124 : Number(code ?? 1),
|
||||||
|
stdout,
|
||||||
|
stderr: timedOut ? `${stderr}\nCommand timed out.` : stderr,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finalizeExecution({
|
||||||
|
executionId,
|
||||||
|
approvalRequestId,
|
||||||
|
approvalGrantId,
|
||||||
|
exactCommand,
|
||||||
|
targetHost,
|
||||||
|
targetUser,
|
||||||
|
requestedBy,
|
||||||
|
ok,
|
||||||
|
exitCode,
|
||||||
|
stdoutSummary,
|
||||||
|
stderrSummary,
|
||||||
|
}) {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const finalStatus = ok ? "executed" : "execution_failed";
|
||||||
|
const lastError = ok ? null : stderrSummary;
|
||||||
|
await client.query(
|
||||||
|
`UPDATE claw_approval_requests
|
||||||
|
SET status = $2::claw_approval_status,
|
||||||
|
executed_at = now(),
|
||||||
|
updated_at = now(),
|
||||||
|
last_error = $3
|
||||||
|
WHERE id = $1`,
|
||||||
|
[approvalRequestId, finalStatus, lastError],
|
||||||
|
);
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: ok ? "execution_succeeded" : "execution_failed",
|
||||||
|
actorType: "broker",
|
||||||
|
actorId: requestedBy,
|
||||||
|
requestId: approvalRequestId,
|
||||||
|
grantId: approvalGrantId,
|
||||||
|
executionId,
|
||||||
|
targetHost,
|
||||||
|
targetUser,
|
||||||
|
commandSnapshot: exactCommand,
|
||||||
|
status: ok ? "executed" : "execution_failed",
|
||||||
|
exitCode,
|
||||||
|
stdoutSummary,
|
||||||
|
stderrSummary,
|
||||||
|
});
|
||||||
|
await client.query("COMMIT");
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExecute(body) {
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const validated = await verifyAndMarkStarted(body);
|
||||||
|
const run = await runCommand(validated.exactCommand, validated.cwd);
|
||||||
|
const ok = run.exitCode === 0;
|
||||||
|
const stdoutSummary = summarize(run.stdout);
|
||||||
|
const stderrSummary = summarize(run.stderr);
|
||||||
|
|
||||||
|
await finalizeExecution({
|
||||||
|
executionId: validated.executionId,
|
||||||
|
approvalRequestId: validated.approvalRequestId,
|
||||||
|
approvalGrantId: validated.approvalGrantId,
|
||||||
|
exactCommand: validated.exactCommand,
|
||||||
|
targetHost: validated.targetHost,
|
||||||
|
targetUser: validated.targetUser,
|
||||||
|
requestedBy: validated.requestedBy,
|
||||||
|
ok,
|
||||||
|
exitCode: run.exitCode,
|
||||||
|
stdoutSummary,
|
||||||
|
stderrSummary,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishedAt = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
executionId: validated.executionId,
|
||||||
|
status: ok ? "executed" : "execution_failed",
|
||||||
|
exitCode: run.exitCode,
|
||||||
|
stdoutSummary,
|
||||||
|
stderrSummary,
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBearerToken(req) {
|
||||||
|
const raw = String(req.headers.authorization ?? "");
|
||||||
|
if (!raw.toLowerCase().startsWith("bearer ")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return raw.slice(7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
||||||
|
|
||||||
|
if (req.method === "GET" && url.pathname === "/healthz") {
|
||||||
|
json(res, 200, { ok: true, service: "claw-broker" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "POST" || url.pathname !== "/v1/execute") {
|
||||||
|
json(res, 404, { ok: false, error: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = getBearerToken(req);
|
||||||
|
if (!token || token !== REQUIRED_TOKEN) {
|
||||||
|
json(res, 401, { ok: false, error: "unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = "";
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
raw += chunk.toString("utf8");
|
||||||
|
if (raw.length > 1_000_000) {
|
||||||
|
req.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on("end", async () => {
|
||||||
|
const fallbackExecutionId = randomUUID();
|
||||||
|
try {
|
||||||
|
const body = raw.length ? JSON.parse(raw) : {};
|
||||||
|
if (!body.executionId) {
|
||||||
|
body.executionId = fallbackExecutionId;
|
||||||
|
}
|
||||||
|
const result = await handleExecute(body);
|
||||||
|
json(res, 200, result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[claw-broker] execute error:", err);
|
||||||
|
const nowIso = new Date().toISOString();
|
||||||
|
json(res, 400, {
|
||||||
|
ok: false,
|
||||||
|
executionId: fallbackExecutionId,
|
||||||
|
status: "execution_failed",
|
||||||
|
exitCode: 1,
|
||||||
|
stdoutSummary: "",
|
||||||
|
stderrSummary: String(err),
|
||||||
|
startedAt: nowIso,
|
||||||
|
finishedAt: nowIso,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(BIND_PORT, BIND_HOST, () => {
|
||||||
|
process.stdout.write(`claw-broker listening on http://${BIND_HOST}:${BIND_PORT}\n`);
|
||||||
|
});
|
||||||
20
scripts/claw-broker/claw-broker.service
Normal file
20
scripts/claw-broker/claw-broker.service
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OpenClaw Privileged Broker (MVP)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/home/negodiy/claw-broker
|
||||||
|
EnvironmentFile=/home/negodiy/claw-broker/.env
|
||||||
|
ExecStart=/usr/bin/node /home/negodiy/claw-broker/broker.mjs
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=no
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
13
scripts/claw-broker/package.json
Normal file
13
scripts/claw-broker/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "claw-broker",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "broker.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node broker.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "^8.20.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -393,11 +393,15 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
await params.opts?.onToolStart?.({ name, phase });
|
await params.opts?.onToolStart?.({ name, phase });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Track auto-compaction completion
|
// Track auto-compaction completion and notify UI layer
|
||||||
if (evt.stream === "compaction") {
|
if (evt.stream === "compaction") {
|
||||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||||
|
if (phase === "start") {
|
||||||
|
await params.opts?.onCompactionStart?.();
|
||||||
|
}
|
||||||
if (phase === "end") {
|
if (phase === "end") {
|
||||||
autoCompactionCompleted = true;
|
autoCompactionCompleted = true;
|
||||||
|
await params.opts?.onCompactionEnd?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export type GetReplyOptions = {
|
|||||||
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
/** Called when a tool phase starts/updates, before summary payloads are emitted. */
|
/** Called when a tool phase starts/updates, before summary payloads are emitted. */
|
||||||
onToolStart?: (payload: { name?: string; phase?: string }) => Promise<void> | void;
|
onToolStart?: (payload: { name?: string; phase?: string }) => Promise<void> | void;
|
||||||
|
/** Called when context auto-compaction starts (allows UX feedback during the pause). */
|
||||||
|
onCompactionStart?: () => Promise<void> | void;
|
||||||
|
/** Called when context auto-compaction completes. */
|
||||||
|
onCompactionEnd?: () => Promise<void> | void;
|
||||||
/** Called when the actual model is selected (including after fallback).
|
/** Called when the actual model is selected (including after fallback).
|
||||||
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
||||||
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should debounce setCompacting and eventually call adapter", async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
void controller.setCompacting();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
|
|
||||||
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.compacting });
|
||||||
|
});
|
||||||
|
|
||||||
it("should classify tool name and debounce", async () => {
|
it("should classify tool name and debounce", async () => {
|
||||||
const { calls, controller } = createEnabledController();
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
@@ -245,6 +254,19 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls.length).toBe(callsAfterFirst);
|
expect(calls.length).toBe(callsAfterFirst);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should cancel a pending compacting emoji before resuming thinking", async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
void controller.setCompacting();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs - 1);
|
||||||
|
controller.cancelPending();
|
||||||
|
void controller.setThinking();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
|
|
||||||
|
const setEmojis = calls.filter((call) => call.method === "set").map((call) => call.emoji);
|
||||||
|
expect(setEmojis).toEqual([DEFAULT_EMOJIS.thinking]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
||||||
const { calls, controller } = createEnabledController();
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
@@ -446,6 +468,7 @@ describe("constants", () => {
|
|||||||
const emojiKeys = [
|
const emojiKeys = [
|
||||||
"queued",
|
"queued",
|
||||||
"thinking",
|
"thinking",
|
||||||
|
"compacting",
|
||||||
"tool",
|
"tool",
|
||||||
"coding",
|
"coding",
|
||||||
"web",
|
"web",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type StatusReactionEmojis = {
|
|||||||
error?: string; // Default: "❌"
|
error?: string; // Default: "❌"
|
||||||
stallSoft?: string; // Default: "⏳"
|
stallSoft?: string; // Default: "⏳"
|
||||||
stallHard?: string; // Default: "⚠️"
|
stallHard?: string; // Default: "⚠️"
|
||||||
|
compacting?: string; // Default: "✍"
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatusReactionTiming = {
|
export type StatusReactionTiming = {
|
||||||
@@ -38,6 +39,9 @@ export type StatusReactionController = {
|
|||||||
setQueued: () => Promise<void> | void;
|
setQueued: () => Promise<void> | void;
|
||||||
setThinking: () => Promise<void> | void;
|
setThinking: () => Promise<void> | void;
|
||||||
setTool: (toolName?: string) => Promise<void> | void;
|
setTool: (toolName?: string) => Promise<void> | void;
|
||||||
|
setCompacting: () => Promise<void> | void;
|
||||||
|
/** Cancel any pending debounced emoji (useful before forcing a state transition). */
|
||||||
|
cancelPending: () => void;
|
||||||
setDone: () => Promise<void>;
|
setDone: () => Promise<void>;
|
||||||
setError: () => Promise<void>;
|
setError: () => Promise<void>;
|
||||||
clear: () => Promise<void>;
|
clear: () => Promise<void>;
|
||||||
@@ -58,6 +62,7 @@ export const DEFAULT_EMOJIS: Required<StatusReactionEmojis> = {
|
|||||||
error: "😱",
|
error: "😱",
|
||||||
stallSoft: "🥱",
|
stallSoft: "🥱",
|
||||||
stallHard: "😨",
|
stallHard: "😨",
|
||||||
|
compacting: "✍",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_TIMING: Required<StatusReactionTiming> = {
|
export const DEFAULT_TIMING: Required<StatusReactionTiming> = {
|
||||||
@@ -162,6 +167,7 @@ export function createStatusReactionController(params: {
|
|||||||
emojis.error,
|
emojis.error,
|
||||||
emojis.stallSoft,
|
emojis.stallSoft,
|
||||||
emojis.stallHard,
|
emojis.stallHard,
|
||||||
|
emojis.compacting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,6 +312,15 @@ export function createStatusReactionController(params: {
|
|||||||
scheduleEmoji(emoji);
|
scheduleEmoji(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCompacting(): void {
|
||||||
|
scheduleEmoji(emojis.compacting);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPending(): void {
|
||||||
|
clearDebounceTimer();
|
||||||
|
pendingEmoji = "";
|
||||||
|
}
|
||||||
|
|
||||||
function finishWithEmoji(emoji: string): Promise<void> {
|
function finishWithEmoji(emoji: string): Promise<void> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -375,6 +390,8 @@ export function createStatusReactionController(params: {
|
|||||||
setQueued,
|
setQueued,
|
||||||
setThinking,
|
setThinking,
|
||||||
setTool,
|
setTool,
|
||||||
|
setCompacting,
|
||||||
|
cancelPending,
|
||||||
setDone,
|
setDone,
|
||||||
setError,
|
setError,
|
||||||
clear,
|
clear,
|
||||||
|
|||||||
@@ -1481,7 +1481,7 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"messages.statusReactions.enabled":
|
"messages.statusReactions.enabled":
|
||||||
"Enable lifecycle status reactions for Telegram. When enabled, the ack reaction becomes the initial 'queued' state and progresses through thinking, tool, done/error automatically. Default: false.",
|
"Enable lifecycle status reactions for Telegram. When enabled, the ack reaction becomes the initial 'queued' state and progresses through thinking, tool, done/error automatically. Default: false.",
|
||||||
"messages.statusReactions.emojis":
|
"messages.statusReactions.emojis":
|
||||||
"Override default status reaction emojis. Keys: thinking, tool, coding, web, done, error, stallSoft, stallHard. Must be valid Telegram reaction emojis.",
|
"Override default status reaction emojis. Keys: thinking, compacting, tool, coding, web, done, error, stallSoft, stallHard. Must be valid Telegram reaction emojis.",
|
||||||
"messages.statusReactions.timing":
|
"messages.statusReactions.timing":
|
||||||
"Override default timing. Keys: debounceMs (700), stallSoftMs (25000), stallHardMs (60000), doneHoldMs (1500), errorHoldMs (2500).",
|
"Override default timing. Keys: debounceMs (700), stallSoftMs (25000), stallHardMs (60000), doneHoldMs (1500), errorHoldMs (2500).",
|
||||||
"messages.inbound.debounceMs":
|
"messages.inbound.debounceMs":
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export type StatusReactionsEmojiConfig = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
stallSoft?: string;
|
stallSoft?: string;
|
||||||
stallHard?: string;
|
stallHard?: string;
|
||||||
|
compacting?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatusReactionsTimingConfig = {
|
export type StatusReactionsTimingConfig = {
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export const MessagesSchema = z
|
|||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
stallSoft: z.string().optional(),
|
stallSoft: z.string().optional(),
|
||||||
stallHard: z.string().optional(),
|
stallHard: z.string().optional(),
|
||||||
|
compacting: z.string().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -47,15 +47,19 @@ type DispatchInboundParams = {
|
|||||||
onReasoningStream?: () => Promise<void> | void;
|
onReasoningStream?: () => Promise<void> | void;
|
||||||
onReasoningEnd?: () => Promise<void> | void;
|
onReasoningEnd?: () => Promise<void> | void;
|
||||||
onToolStart?: (payload: { name?: string }) => Promise<void> | void;
|
onToolStart?: (payload: { name?: string }) => Promise<void> | void;
|
||||||
|
onCompactionStart?: () => Promise<void> | void;
|
||||||
|
onCompactionEnd?: () => Promise<void> | void;
|
||||||
onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
|
onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
|
||||||
onAssistantMessageStart?: () => Promise<void> | void;
|
onAssistantMessageStart?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const dispatchInboundMessage = vi.fn(async (_params?: DispatchInboundParams) => ({
|
const dispatchInboundMessage = vi.hoisted(() =>
|
||||||
queuedFinal: false,
|
vi.fn(async (_params?: DispatchInboundParams) => ({
|
||||||
counts: { final: 0, tool: 0, block: 0 },
|
queuedFinal: false,
|
||||||
}));
|
counts: { final: 0, tool: 0, block: 0 },
|
||||||
const recordInboundSession = vi.fn(async () => {});
|
})),
|
||||||
|
);
|
||||||
|
const recordInboundSession = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
const configSessionsMocks = vi.hoisted(() => ({
|
const configSessionsMocks = vi.hoisted(() => ({
|
||||||
readSessionUpdatedAt: vi.fn(() => undefined),
|
readSessionUpdatedAt: vi.fn(() => undefined),
|
||||||
resolveStorePath: vi.fn(() => "/tmp/openclaw-discord-process-test-sessions.json"),
|
resolveStorePath: vi.fn(() => "/tmp/openclaw-discord-process-test-sessions.json"),
|
||||||
@@ -346,6 +350,39 @@ describe("processDiscordMessage ack reactions", () => {
|
|||||||
expect(emojis).toContain("🏁");
|
expect(emojis).toContain("🏁");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows compacting reaction during auto-compaction and resumes thinking", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||||
|
await params?.replyOptions?.onCompactionStart?.();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||||
|
await params?.replyOptions?.onCompactionEnd?.();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||||
|
return createNoQueuedDispatchResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = await createBaseContext({
|
||||||
|
cfg: {
|
||||||
|
messages: {
|
||||||
|
ackReaction: "👀",
|
||||||
|
statusReactions: {
|
||||||
|
timing: { debounceMs: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
|
const runPromise = processDiscordMessage(ctx as any);
|
||||||
|
await vi.advanceTimersByTimeAsync(2_500);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
await runPromise;
|
||||||
|
|
||||||
|
const emojis = getReactionEmojis();
|
||||||
|
expect(emojis).toContain(DEFAULT_EMOJIS.compacting);
|
||||||
|
expect(emojis).toContain(DEFAULT_EMOJIS.thinking);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears status reactions when dispatch aborts and removeAckAfterReply is enabled", async () => {
|
it("clears status reactions when dispatch aborts and removeAckAfterReply is enabled", async () => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
dispatchInboundMessage.mockImplementationOnce(async () => {
|
dispatchInboundMessage.mockImplementationOnce(async () => {
|
||||||
|
|||||||
@@ -769,6 +769,19 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
}
|
}
|
||||||
await statusReactions.setTool(payload.name);
|
await statusReactions.setTool(payload.name);
|
||||||
},
|
},
|
||||||
|
onCompactionStart: async () => {
|
||||||
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await statusReactions.setCompacting();
|
||||||
|
},
|
||||||
|
onCompactionEnd: async () => {
|
||||||
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusReactions.cancelPending();
|
||||||
|
await statusReactions.setThinking();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (isProcessAborted(abortSignal)) {
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
|||||||
682
src/gateway/claw-approvals-store.ts
Normal file
682
src/gateway/claw-approvals-store.ts
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { Pool, type PoolClient } from "pg";
|
||||||
|
|
||||||
|
export type ClawApprovalStatus =
|
||||||
|
| "pending"
|
||||||
|
| "approved_once"
|
||||||
|
| "approved_always"
|
||||||
|
| "rejected"
|
||||||
|
| "expired"
|
||||||
|
| "executed"
|
||||||
|
| "execution_failed";
|
||||||
|
|
||||||
|
export type ClawRiskLevel = "low" | "medium" | "high";
|
||||||
|
|
||||||
|
export type ClawApprovalRequestRow = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
requestedByAgent: string;
|
||||||
|
sessionId: string;
|
||||||
|
channel: string;
|
||||||
|
chatId: string;
|
||||||
|
humanUserId: string;
|
||||||
|
targetHost: string;
|
||||||
|
targetUser: string;
|
||||||
|
cwd: string | null;
|
||||||
|
humanSummary: string;
|
||||||
|
reason: string;
|
||||||
|
exactCommand: string;
|
||||||
|
normalizedCommand: string;
|
||||||
|
riskLevel: ClawRiskLevel;
|
||||||
|
rollbackHint: string | null;
|
||||||
|
requiresPrivilege: boolean;
|
||||||
|
dangerousFlags: Record<string, boolean>;
|
||||||
|
status: ClawApprovalStatus;
|
||||||
|
statusReason: string | null;
|
||||||
|
approvedBy: string | null;
|
||||||
|
approvedAt: string | null;
|
||||||
|
rejectedBy: string | null;
|
||||||
|
rejectedAt: string | null;
|
||||||
|
expiredAt: string | null;
|
||||||
|
executedAt: string | null;
|
||||||
|
executionId: string | null;
|
||||||
|
lastError: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateClawApprovalRequestInput = {
|
||||||
|
requestedByAgent: string;
|
||||||
|
sessionId: string;
|
||||||
|
channel: string;
|
||||||
|
chatId: string;
|
||||||
|
humanUserId: string;
|
||||||
|
targetHost: string;
|
||||||
|
targetUser: string;
|
||||||
|
cwd?: string | null;
|
||||||
|
humanSummary: string;
|
||||||
|
reason: string;
|
||||||
|
exactCommand: string;
|
||||||
|
riskLevel: ClawRiskLevel;
|
||||||
|
rollbackHint?: string | null;
|
||||||
|
dangerousFlags?: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClawApproveInput = {
|
||||||
|
id: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClawApproveOnceInput = ClawApproveInput & {
|
||||||
|
ttlSeconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClawRejectInput = ClawApproveInput & {
|
||||||
|
reason?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClawExecuteInput = {
|
||||||
|
id: string;
|
||||||
|
grantId: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerExecutePayload = {
|
||||||
|
executionId: string;
|
||||||
|
approvalRequestId: string;
|
||||||
|
approvalGrantId: string;
|
||||||
|
exactCommand: string;
|
||||||
|
targetHost: string;
|
||||||
|
targetUser: string;
|
||||||
|
requestedBy: string;
|
||||||
|
channel: string;
|
||||||
|
chatId: string;
|
||||||
|
humanUserId: string;
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerExecuteResult = {
|
||||||
|
executionId: string;
|
||||||
|
status: string;
|
||||||
|
ok: boolean;
|
||||||
|
exitCode: number;
|
||||||
|
stdoutSummary?: string;
|
||||||
|
stderrSummary?: string;
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool: Pool | null = null;
|
||||||
|
|
||||||
|
function resolveEnv(name: string): string | undefined {
|
||||||
|
return process.env[`CLAW_${name}`] ?? process.env[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireEnv(name: string): string {
|
||||||
|
const v = resolveEnv(name);
|
||||||
|
if (!v || !v.trim()) {
|
||||||
|
throw new Error(`missing required environment variable: ${name} (or CLAW_${name})`);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPool(): Pool {
|
||||||
|
if (pool) {
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = requireEnv("PGHOST");
|
||||||
|
const portRaw = resolveEnv("PGPORT") ?? "5432";
|
||||||
|
const user = requireEnv("PGUSER");
|
||||||
|
const password = requireEnv("PGPASSWORD");
|
||||||
|
const database = requireEnv("PGDATABASE");
|
||||||
|
|
||||||
|
const port = Number(portRaw);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) {
|
||||||
|
throw new Error(`invalid PGPORT: ${portRaw}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool = new Pool({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database,
|
||||||
|
max: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCommand(input: string): string {
|
||||||
|
return input.trim().replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasDangerousShellConstruct(command: string): boolean {
|
||||||
|
const source = command.toLowerCase();
|
||||||
|
const checks: RegExp[] = [
|
||||||
|
/\bbash\s+-c\b/,
|
||||||
|
/\bsh\s+-c\b/,
|
||||||
|
/\bsudo\s+su\b/,
|
||||||
|
/\bsudo\s+-i\b/,
|
||||||
|
/&&/,
|
||||||
|
/\|\|/,
|
||||||
|
/;/,
|
||||||
|
/\|/,
|
||||||
|
/>/,
|
||||||
|
/</,
|
||||||
|
/\$\(/,
|
||||||
|
/`/,
|
||||||
|
/<<[-\w]*/,
|
||||||
|
];
|
||||||
|
return checks.some((r) => r.test(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRequestRow(row: Record<string, unknown>): ClawApprovalRequestRow {
|
||||||
|
return {
|
||||||
|
id: String(row.id),
|
||||||
|
createdAt: String(row.created_at),
|
||||||
|
updatedAt: String(row.updated_at),
|
||||||
|
requestedByAgent: String(row.requested_by_agent),
|
||||||
|
sessionId: String(row.session_id),
|
||||||
|
channel: String(row.channel),
|
||||||
|
chatId: String(row.chat_id),
|
||||||
|
humanUserId: String(row.human_user_id),
|
||||||
|
targetHost: String(row.target_host),
|
||||||
|
targetUser: String(row.target_user),
|
||||||
|
cwd: (row.cwd as string | null) ?? null,
|
||||||
|
humanSummary: String(row.human_summary),
|
||||||
|
reason: String(row.reason),
|
||||||
|
exactCommand: String(row.exact_command),
|
||||||
|
normalizedCommand: String(row.normalized_command),
|
||||||
|
riskLevel: String(row.risk_level) as ClawRiskLevel,
|
||||||
|
rollbackHint: (row.rollback_hint as string | null) ?? null,
|
||||||
|
requiresPrivilege: Boolean(row.requires_privilege),
|
||||||
|
dangerousFlags: (row.dangerous_flags as Record<string, boolean> | null) ?? {},
|
||||||
|
status: String(row.status) as ClawApprovalStatus,
|
||||||
|
statusReason: (row.status_reason as string | null) ?? null,
|
||||||
|
approvedBy: (row.approved_by as string | null) ?? null,
|
||||||
|
approvedAt: (row.approved_at as string | null) ?? null,
|
||||||
|
rejectedBy: (row.rejected_by as string | null) ?? null,
|
||||||
|
rejectedAt: (row.rejected_at as string | null) ?? null,
|
||||||
|
expiredAt: (row.expired_at as string | null) ?? null,
|
||||||
|
executedAt: (row.executed_at as string | null) ?? null,
|
||||||
|
executionId: (row.execution_id as string | null) ?? null,
|
||||||
|
lastError: (row.last_error as string | null) ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertAudit(
|
||||||
|
client: PoolClient,
|
||||||
|
args: {
|
||||||
|
eventType: string;
|
||||||
|
actorType: "agent" | "human" | "backend" | "broker" | "system";
|
||||||
|
actorId: string;
|
||||||
|
requestId?: string;
|
||||||
|
grantId?: string;
|
||||||
|
executionId?: string;
|
||||||
|
targetHost?: string;
|
||||||
|
targetUser?: string;
|
||||||
|
commandSnapshot?: string;
|
||||||
|
status?: string;
|
||||||
|
exitCode?: number;
|
||||||
|
stdoutSummary?: string;
|
||||||
|
stderrSummary?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO claw_audit_events (
|
||||||
|
event_type,
|
||||||
|
request_id,
|
||||||
|
grant_id,
|
||||||
|
execution_id,
|
||||||
|
actor_type,
|
||||||
|
actor_id,
|
||||||
|
target_host,
|
||||||
|
target_user,
|
||||||
|
command_snapshot,
|
||||||
|
status,
|
||||||
|
exit_code,
|
||||||
|
stdout_summary,
|
||||||
|
stderr_summary,
|
||||||
|
metadata
|
||||||
|
) VALUES (
|
||||||
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb
|
||||||
|
)`,
|
||||||
|
[
|
||||||
|
args.eventType,
|
||||||
|
args.requestId ?? null,
|
||||||
|
args.grantId ?? null,
|
||||||
|
args.executionId ?? null,
|
||||||
|
args.actorType,
|
||||||
|
args.actorId,
|
||||||
|
args.targetHost ?? null,
|
||||||
|
args.targetUser ?? null,
|
||||||
|
args.commandSnapshot ?? null,
|
||||||
|
args.status ?? null,
|
||||||
|
args.exitCode ?? null,
|
||||||
|
args.stdoutSummary ?? null,
|
||||||
|
args.stderrSummary ?? null,
|
||||||
|
JSON.stringify(args.metadata ?? {}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClawApprovalsStore {
|
||||||
|
async createApprovalRequest(
|
||||||
|
input: CreateClawApprovalRequestInput,
|
||||||
|
): Promise<ClawApprovalRequestRow> {
|
||||||
|
const normalizedCommand = normalizeCommand(input.exactCommand);
|
||||||
|
const dangerousFlags = {
|
||||||
|
hasDangerousShell: hasDangerousShellConstruct(input.exactCommand),
|
||||||
|
...input.dangerousFlags,
|
||||||
|
};
|
||||||
|
|
||||||
|
const db = getPool();
|
||||||
|
const client = await db.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const res = await client.query(
|
||||||
|
`INSERT INTO claw_approval_requests (
|
||||||
|
requested_by_agent, session_id, channel, chat_id, human_user_id,
|
||||||
|
target_host, target_user, cwd,
|
||||||
|
human_summary, reason, exact_command, normalized_command,
|
||||||
|
risk_level, rollback_hint, requires_privilege, dangerous_flags, status
|
||||||
|
) VALUES (
|
||||||
|
$1,$2,$3,$4,$5,
|
||||||
|
$6,$7,$8,
|
||||||
|
$9,$10,$11,$12,
|
||||||
|
$13,$14,TRUE,$15::jsonb,'pending'
|
||||||
|
) RETURNING *`,
|
||||||
|
[
|
||||||
|
input.requestedByAgent,
|
||||||
|
input.sessionId,
|
||||||
|
input.channel,
|
||||||
|
input.chatId,
|
||||||
|
input.humanUserId,
|
||||||
|
input.targetHost,
|
||||||
|
input.targetUser,
|
||||||
|
input.cwd ?? null,
|
||||||
|
input.humanSummary,
|
||||||
|
input.reason,
|
||||||
|
input.exactCommand,
|
||||||
|
normalizedCommand,
|
||||||
|
input.riskLevel,
|
||||||
|
input.rollbackHint ?? null,
|
||||||
|
JSON.stringify(dangerousFlags),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = mapRequestRow(res.rows[0]);
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "request_created",
|
||||||
|
actorType: "agent",
|
||||||
|
actorId: input.requestedByAgent,
|
||||||
|
requestId: row.id,
|
||||||
|
targetHost: row.targetHost,
|
||||||
|
targetUser: row.targetUser,
|
||||||
|
commandSnapshot: row.exactCommand,
|
||||||
|
status: row.status,
|
||||||
|
});
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return row;
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listApprovalRequests(status?: string): Promise<ClawApprovalRequestRow[]> {
|
||||||
|
const db = getPool();
|
||||||
|
const res = await db.query(
|
||||||
|
status && status.trim().length > 0
|
||||||
|
? `SELECT * FROM claw_approval_requests WHERE status = $1 ORDER BY created_at DESC LIMIT 200`
|
||||||
|
: `SELECT * FROM claw_approval_requests ORDER BY created_at DESC LIMIT 200`,
|
||||||
|
status && status.trim().length > 0 ? [status.trim()] : [],
|
||||||
|
);
|
||||||
|
return res.rows.map(mapRequestRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getApprovalRequest(id: string): Promise<ClawApprovalRequestRow | null> {
|
||||||
|
const db = getPool();
|
||||||
|
const res = await db.query(`SELECT * FROM claw_approval_requests WHERE id = $1 LIMIT 1`, [id]);
|
||||||
|
if (res.rowCount === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mapRequestRow(res.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveOnce(
|
||||||
|
input: ClawApproveOnceInput,
|
||||||
|
): Promise<{ request: ClawApprovalRequestRow; grantId: string; expiresAt: string }> {
|
||||||
|
const db = getPool();
|
||||||
|
const client = await db.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const reqRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_requests WHERE id = $1 FOR UPDATE`,
|
||||||
|
[input.id],
|
||||||
|
);
|
||||||
|
if (reqRes.rowCount === 0) {
|
||||||
|
throw new Error("approval request not found");
|
||||||
|
}
|
||||||
|
const req = mapRequestRow(reqRes.rows[0]);
|
||||||
|
if (req.status !== "pending") {
|
||||||
|
throw new Error(`cannot approve_once from status=${req.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttl = Math.max(120, Math.min(300, Math.trunc(input.ttlSeconds || 180)));
|
||||||
|
const grantRes = await client.query(
|
||||||
|
`INSERT INTO claw_approval_grants (
|
||||||
|
request_id, grant_type, match_type,
|
||||||
|
target_host, target_user, channel, chat_id, human_user_id, session_id,
|
||||||
|
exact_command, normalized_command, approved_by, expires_at
|
||||||
|
) VALUES (
|
||||||
|
$1,'once','exact',$2,$3,$4,$5,$6,$7,$8,$9,$10, now() + ($11 || ' seconds')::interval
|
||||||
|
) RETURNING id, expires_at`,
|
||||||
|
[
|
||||||
|
req.id,
|
||||||
|
req.targetHost,
|
||||||
|
req.targetUser,
|
||||||
|
req.channel,
|
||||||
|
req.chatId,
|
||||||
|
req.humanUserId,
|
||||||
|
req.sessionId,
|
||||||
|
req.exactCommand,
|
||||||
|
req.normalizedCommand,
|
||||||
|
input.actorId,
|
||||||
|
String(ttl),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE claw_approval_requests
|
||||||
|
SET status='approved_once', approved_by=$2, approved_at=now(), updated_at=now()
|
||||||
|
WHERE id = $1`,
|
||||||
|
[req.id, input.actorId],
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "request_approved_once",
|
||||||
|
actorType: "human",
|
||||||
|
actorId: input.actorId,
|
||||||
|
requestId: req.id,
|
||||||
|
grantId: String(grantRes.rows[0].id),
|
||||||
|
targetHost: req.targetHost,
|
||||||
|
targetUser: req.targetUser,
|
||||||
|
commandSnapshot: req.exactCommand,
|
||||||
|
status: "approved_once",
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
const next = await this.getApprovalRequest(req.id);
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("approval request not found after approve_once");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
request: next,
|
||||||
|
grantId: String(grantRes.rows[0].id),
|
||||||
|
expiresAt: String(grantRes.rows[0].expires_at),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveAlways(
|
||||||
|
input: ClawApproveInput,
|
||||||
|
): Promise<{ request: ClawApprovalRequestRow; grantId: string; allowRuleId: string }> {
|
||||||
|
const db = getPool();
|
||||||
|
const client = await db.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const reqRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_requests WHERE id = $1 FOR UPDATE`,
|
||||||
|
[input.id],
|
||||||
|
);
|
||||||
|
if (reqRes.rowCount === 0) {
|
||||||
|
throw new Error("approval request not found");
|
||||||
|
}
|
||||||
|
const req = mapRequestRow(reqRes.rows[0]);
|
||||||
|
if (req.status !== "pending") {
|
||||||
|
throw new Error(`cannot approve_always from status=${req.status}`);
|
||||||
|
}
|
||||||
|
if (hasDangerousShellConstruct(req.exactCommand)) {
|
||||||
|
throw new Error("always allow is forbidden for dangerous shell constructs");
|
||||||
|
}
|
||||||
|
|
||||||
|
const grantRes = await client.query(
|
||||||
|
`INSERT INTO claw_approval_grants (
|
||||||
|
request_id, grant_type, match_type,
|
||||||
|
target_host, target_user, channel, chat_id, human_user_id, session_id,
|
||||||
|
exact_command, normalized_command, approved_by
|
||||||
|
) VALUES (
|
||||||
|
$1,'always','exact',$2,$3,$4,$5,$6,$7,$8,$9,$10
|
||||||
|
) RETURNING id`,
|
||||||
|
[
|
||||||
|
req.id,
|
||||||
|
req.targetHost,
|
||||||
|
req.targetUser,
|
||||||
|
req.channel,
|
||||||
|
req.chatId,
|
||||||
|
req.humanUserId,
|
||||||
|
req.sessionId,
|
||||||
|
req.exactCommand,
|
||||||
|
req.normalizedCommand,
|
||||||
|
input.actorId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ruleRes = await client.query(
|
||||||
|
`INSERT INTO claw_allow_rules (
|
||||||
|
created_by, source_request_id,
|
||||||
|
target_host, target_user, channel, chat_id, human_user_id,
|
||||||
|
command_pattern_type, command_pattern, normalized_pattern, enabled
|
||||||
|
) VALUES (
|
||||||
|
$1,$2,$3,$4,$5,$6,$7,'exact',$8,$9,TRUE
|
||||||
|
)
|
||||||
|
ON CONFLICT ON CONSTRAINT uq_claw_allow_rules_active_exact
|
||||||
|
DO UPDATE SET updated_at=now(), enabled=TRUE
|
||||||
|
RETURNING id`,
|
||||||
|
[
|
||||||
|
input.actorId,
|
||||||
|
req.id,
|
||||||
|
req.targetHost,
|
||||||
|
req.targetUser,
|
||||||
|
req.channel,
|
||||||
|
req.chatId,
|
||||||
|
req.humanUserId,
|
||||||
|
req.exactCommand,
|
||||||
|
req.normalizedCommand,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE claw_approval_requests
|
||||||
|
SET status='approved_always', approved_by=$2, approved_at=now(), updated_at=now()
|
||||||
|
WHERE id = $1`,
|
||||||
|
[req.id, input.actorId],
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "request_approved_always",
|
||||||
|
actorType: "human",
|
||||||
|
actorId: input.actorId,
|
||||||
|
requestId: req.id,
|
||||||
|
grantId: String(grantRes.rows[0].id),
|
||||||
|
targetHost: req.targetHost,
|
||||||
|
targetUser: req.targetUser,
|
||||||
|
commandSnapshot: req.exactCommand,
|
||||||
|
status: "approved_always",
|
||||||
|
metadata: { allowRuleId: String(ruleRes.rows[0].id) },
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
const next = await this.getApprovalRequest(req.id);
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("approval request not found after approve_always");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
request: next,
|
||||||
|
grantId: String(grantRes.rows[0].id),
|
||||||
|
allowRuleId: String(ruleRes.rows[0].id),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject(input: ClawRejectInput): Promise<ClawApprovalRequestRow> {
|
||||||
|
const db = getPool();
|
||||||
|
const client = await db.connect();
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
const reqRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_requests WHERE id = $1 FOR UPDATE`,
|
||||||
|
[input.id],
|
||||||
|
);
|
||||||
|
if (reqRes.rowCount === 0) {
|
||||||
|
throw new Error("approval request not found");
|
||||||
|
}
|
||||||
|
const req = mapRequestRow(reqRes.rows[0]);
|
||||||
|
if (req.status !== "pending") {
|
||||||
|
throw new Error(`cannot reject from status=${req.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE claw_approval_requests
|
||||||
|
SET status='rejected', rejected_by=$2, rejected_at=now(), status_reason=$3, updated_at=now()
|
||||||
|
WHERE id = $1`,
|
||||||
|
[req.id, input.actorId, input.reason ?? null],
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertAudit(client, {
|
||||||
|
eventType: "request_rejected",
|
||||||
|
actorType: "human",
|
||||||
|
actorId: input.actorId,
|
||||||
|
requestId: req.id,
|
||||||
|
targetHost: req.targetHost,
|
||||||
|
targetUser: req.targetUser,
|
||||||
|
commandSnapshot: req.exactCommand,
|
||||||
|
status: "rejected",
|
||||||
|
metadata: { reason: input.reason ?? null },
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
const next = await this.getApprovalRequest(req.id);
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("approval request not found after reject");
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeApproved(
|
||||||
|
input: ClawExecuteInput,
|
||||||
|
runBroker: (payload: BrokerExecutePayload) => Promise<BrokerExecuteResult>,
|
||||||
|
): Promise<{
|
||||||
|
request: ClawApprovalRequestRow;
|
||||||
|
executionId: string;
|
||||||
|
broker: BrokerExecuteResult;
|
||||||
|
}> {
|
||||||
|
const db = getPool();
|
||||||
|
const client = await db.connect();
|
||||||
|
let request: ClawApprovalRequestRow | null = null;
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
|
const reqRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_requests WHERE id = $1 FOR UPDATE`,
|
||||||
|
[input.id],
|
||||||
|
);
|
||||||
|
if (reqRes.rowCount === 0) {
|
||||||
|
throw new Error("approval request not found");
|
||||||
|
}
|
||||||
|
request = mapRequestRow(reqRes.rows[0]);
|
||||||
|
if (request.status !== "approved_once" && request.status !== "approved_always") {
|
||||||
|
throw new Error(`cannot execute from status=${request.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grantRes = await client.query(
|
||||||
|
`SELECT * FROM claw_approval_grants WHERE id = $1 AND request_id = $2 FOR UPDATE`,
|
||||||
|
[input.grantId, request.id],
|
||||||
|
);
|
||||||
|
if (grantRes.rowCount === 0) {
|
||||||
|
throw new Error("grant not found");
|
||||||
|
}
|
||||||
|
const grant = grantRes.rows[0] as Record<string, unknown>;
|
||||||
|
|
||||||
|
const exactCommand = String(grant.exact_command);
|
||||||
|
if (normalizeCommand(exactCommand) !== request.normalizedCommand) {
|
||||||
|
throw new Error("grant command mismatch with request");
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new Error("request resolution failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const broker = await runBroker({
|
||||||
|
executionId: randomUUID(),
|
||||||
|
approvalRequestId: request.id,
|
||||||
|
approvalGrantId: input.grantId,
|
||||||
|
exactCommand: request.exactCommand,
|
||||||
|
targetHost: request.targetHost,
|
||||||
|
targetUser: request.targetUser,
|
||||||
|
requestedBy: input.actorId,
|
||||||
|
channel: request.channel,
|
||||||
|
chatId: request.chatId,
|
||||||
|
humanUserId: request.humanUserId,
|
||||||
|
sessionId: request.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const latest = await this.getApprovalRequest(request.id);
|
||||||
|
if (!latest) {
|
||||||
|
throw new Error("approval request not found after execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: latest,
|
||||||
|
executionId: broker.executionId,
|
||||||
|
broker,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuditTrail(requestId: string): Promise<Record<string, unknown>[]> {
|
||||||
|
const db = getPool();
|
||||||
|
const res = await db.query(
|
||||||
|
`SELECT * FROM claw_audit_events WHERE request_id = $1 ORDER BY occurred_at ASC, id ASC`,
|
||||||
|
[requestId],
|
||||||
|
);
|
||||||
|
return res.rows as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let singleton: ClawApprovalsStore | null = null;
|
||||||
|
|
||||||
|
export function getClawApprovalsStore(): ClawApprovalsStore {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new ClawApprovalsStore();
|
||||||
|
}
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
@@ -34,6 +34,14 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
|||||||
"exec.approval.request",
|
"exec.approval.request",
|
||||||
"exec.approval.waitDecision",
|
"exec.approval.waitDecision",
|
||||||
"exec.approval.resolve",
|
"exec.approval.resolve",
|
||||||
|
"claw.approvals.create",
|
||||||
|
"claw.approvals.list",
|
||||||
|
"claw.approvals.get",
|
||||||
|
"claw.approvals.approveOnce",
|
||||||
|
"claw.approvals.approveAlways",
|
||||||
|
"claw.approvals.reject",
|
||||||
|
"claw.approvals.execute",
|
||||||
|
"claw.approvals.audit",
|
||||||
],
|
],
|
||||||
[PAIRING_SCOPE]: [
|
[PAIRING_SCOPE]: [
|
||||||
"node.pair.request",
|
"node.pair.request",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { agentsHandlers } from "./server-methods/agents.js";
|
|||||||
import { browserHandlers } from "./server-methods/browser.js";
|
import { browserHandlers } from "./server-methods/browser.js";
|
||||||
import { channelsHandlers } from "./server-methods/channels.js";
|
import { channelsHandlers } from "./server-methods/channels.js";
|
||||||
import { chatHandlers } from "./server-methods/chat.js";
|
import { chatHandlers } from "./server-methods/chat.js";
|
||||||
|
import { clawApprovalsHandlers } from "./server-methods/claw-approvals.js";
|
||||||
import { configHandlers } from "./server-methods/config.js";
|
import { configHandlers } from "./server-methods/config.js";
|
||||||
import { connectHandlers } from "./server-methods/connect.js";
|
import { connectHandlers } from "./server-methods/connect.js";
|
||||||
import { cronHandlers } from "./server-methods/cron.js";
|
import { cronHandlers } from "./server-methods/cron.js";
|
||||||
@@ -76,6 +77,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
|
|||||||
...deviceHandlers,
|
...deviceHandlers,
|
||||||
...doctorHandlers,
|
...doctorHandlers,
|
||||||
...execApprovalsHandlers,
|
...execApprovalsHandlers,
|
||||||
|
...clawApprovalsHandlers,
|
||||||
...webHandlers,
|
...webHandlers,
|
||||||
...modelsHandlers,
|
...modelsHandlers,
|
||||||
...configHandlers,
|
...configHandlers,
|
||||||
|
|||||||
330
src/gateway/server-methods/claw-approvals.ts
Normal file
330
src/gateway/server-methods/claw-approvals.ts
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
import {
|
||||||
|
getClawApprovalsStore,
|
||||||
|
type BrokerExecutePayload,
|
||||||
|
type BrokerExecuteResult,
|
||||||
|
type ClawRiskLevel,
|
||||||
|
} from "../claw-approvals-store.js";
|
||||||
|
import { ErrorCodes, errorShape } from "../protocol/index.js";
|
||||||
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
|
|
||||||
|
type JsonMap = Record<string, unknown>;
|
||||||
|
|
||||||
|
function asObject(value: unknown): JsonMap | null {
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonMap) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredString(params: JsonMap, key: string): string {
|
||||||
|
const value = params[key];
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
throw new Error(`${key} is required`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionalString(params: JsonMap, key: string): string | null {
|
||||||
|
const value = params[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(`${key} must be a string`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRiskLevel(params: JsonMap): ClawRiskLevel {
|
||||||
|
const value = params.riskLevel;
|
||||||
|
if (value === "low" || value === "medium" || value === "high") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
throw new Error("riskLevel must be low|medium|high");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeBroker(payload: BrokerExecutePayload): Promise<BrokerExecuteResult> {
|
||||||
|
const brokerUrl = process.env.CLAW_BROKER_URL ?? "http://127.0.0.1:8787/v1/execute";
|
||||||
|
const brokerToken = process.env.CLAW_BROKER_TOKEN?.trim();
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
if (brokerToken) {
|
||||||
|
headers.authorization = `Bearer ${brokerToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(brokerUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
let body: JsonMap | null = null;
|
||||||
|
try {
|
||||||
|
body = (await res.json()) as JsonMap;
|
||||||
|
} catch {
|
||||||
|
body = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const nowIso = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
executionId: payload.executionId,
|
||||||
|
status: "broker_error",
|
||||||
|
ok: false,
|
||||||
|
exitCode: 1,
|
||||||
|
stderrSummary: `broker error ${res.status}`,
|
||||||
|
startedAt: nowIso,
|
||||||
|
finishedAt: nowIso,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = body?.ok === true;
|
||||||
|
const exitCodeRaw = body?.exitCode;
|
||||||
|
const exitCode = typeof exitCodeRaw === "number" ? exitCodeRaw : ok ? 0 : 1;
|
||||||
|
const startedAt =
|
||||||
|
typeof body?.startedAt === "string" && body.startedAt.length > 0
|
||||||
|
? body.startedAt
|
||||||
|
: new Date().toISOString();
|
||||||
|
const finishedAt =
|
||||||
|
typeof body?.finishedAt === "string" && body.finishedAt.length > 0
|
||||||
|
? body.finishedAt
|
||||||
|
: startedAt;
|
||||||
|
return {
|
||||||
|
executionId:
|
||||||
|
typeof body?.executionId === "string" && body.executionId.length > 0
|
||||||
|
? body.executionId
|
||||||
|
: payload.executionId,
|
||||||
|
status: typeof body?.status === "string" ? body.status : ok ? "executed" : "execution_failed",
|
||||||
|
ok,
|
||||||
|
exitCode,
|
||||||
|
stdoutSummary: typeof body?.stdoutSummary === "string" ? body.stdoutSummary : "",
|
||||||
|
stderrSummary: typeof body?.stderrSummary === "string" ? body.stderrSummary : "",
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clawApprovalsHandlers: GatewayRequestHandlers = {
|
||||||
|
"claw.approvals.create": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const request = await store.createApprovalRequest({
|
||||||
|
requestedByAgent: getRequiredString(p, "requestedByAgent"),
|
||||||
|
sessionId: getRequiredString(p, "sessionId"),
|
||||||
|
channel: getRequiredString(p, "channel"),
|
||||||
|
chatId: getRequiredString(p, "chatId"),
|
||||||
|
humanUserId: getRequiredString(p, "humanUserId"),
|
||||||
|
targetHost: getRequiredString(p, "targetHost"),
|
||||||
|
targetUser: getRequiredString(p, "targetUser"),
|
||||||
|
cwd: getOptionalString(p, "cwd"),
|
||||||
|
humanSummary: getRequiredString(p, "humanSummary"),
|
||||||
|
reason: getRequiredString(p, "reason"),
|
||||||
|
exactCommand: getRequiredString(p, "exactCommand"),
|
||||||
|
riskLevel: getRiskLevel(p),
|
||||||
|
rollbackHint: getOptionalString(p, "rollbackHint"),
|
||||||
|
dangerousFlags: asObject(p.dangerousFlags) as Record<string, boolean> | undefined,
|
||||||
|
});
|
||||||
|
respond(true, { request }, undefined);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.create failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.list": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params) ?? {};
|
||||||
|
const status = getOptionalString(p, "status");
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const requests = await store.listApprovalRequests(status ?? undefined);
|
||||||
|
respond(true, { requests }, undefined);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.list failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.get": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const id = getRequiredString(p, "id");
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const request = await store.getApprovalRequest(id);
|
||||||
|
if (!request) {
|
||||||
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "request not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
respond(true, { request }, undefined);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.get failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.approveOnce": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const ttlRaw = p.ttlSeconds;
|
||||||
|
const ttlSeconds = typeof ttlRaw === "number" && Number.isFinite(ttlRaw) ? ttlRaw : 180;
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const result = await store.approveOnce({
|
||||||
|
id: getRequiredString(p, "id"),
|
||||||
|
actorId: getRequiredString(p, "actorId"),
|
||||||
|
ttlSeconds,
|
||||||
|
});
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
request: result.request,
|
||||||
|
grant: {
|
||||||
|
grantId: result.grantId,
|
||||||
|
grantType: "once",
|
||||||
|
expiresAt: result.expiresAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.approveOnce failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.approveAlways": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const result = await store.approveAlways({
|
||||||
|
id: getRequiredString(p, "id"),
|
||||||
|
actorId: getRequiredString(p, "actorId"),
|
||||||
|
});
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
request: result.request,
|
||||||
|
grant: {
|
||||||
|
grantId: result.grantId,
|
||||||
|
grantType: "always",
|
||||||
|
},
|
||||||
|
allowRuleId: result.allowRuleId,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`claw.approvals.approveAlways failed: ${String(err)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.reject": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const request = await store.reject({
|
||||||
|
id: getRequiredString(p, "id"),
|
||||||
|
actorId: getRequiredString(p, "actorId"),
|
||||||
|
reason: getOptionalString(p, "reason"),
|
||||||
|
});
|
||||||
|
respond(true, { request }, undefined);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.reject failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.execute": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const result = await store.executeApproved(
|
||||||
|
{
|
||||||
|
id: getRequiredString(p, "id"),
|
||||||
|
grantId: getRequiredString(p, "grantId"),
|
||||||
|
actorId: getRequiredString(p, "actorId"),
|
||||||
|
},
|
||||||
|
invokeBroker,
|
||||||
|
);
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
request: result.request,
|
||||||
|
execution: {
|
||||||
|
executionId: result.executionId,
|
||||||
|
ok: result.broker.ok,
|
||||||
|
status: result.broker.status,
|
||||||
|
exitCode: result.broker.exitCode,
|
||||||
|
stdoutSummary: result.broker.stdoutSummary ?? "",
|
||||||
|
stderrSummary: result.broker.stderrSummary ?? "",
|
||||||
|
startedAt: result.broker.startedAt,
|
||||||
|
finishedAt: result.broker.finishedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.UNAVAILABLE, `claw.approvals.execute failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"claw.approvals.audit": async ({ params, respond }) => {
|
||||||
|
try {
|
||||||
|
const p = asObject(params);
|
||||||
|
if (!p) {
|
||||||
|
throw new Error("params object is required");
|
||||||
|
}
|
||||||
|
const id = getRequiredString(p, "id");
|
||||||
|
const store = getClawApprovalsStore();
|
||||||
|
const events = await store.getAuditTrail(id);
|
||||||
|
respond(true, { events }, undefined);
|
||||||
|
} catch (err) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `claw.approvals.audit failed: ${String(err)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -64,20 +64,16 @@ describe("resolveProxyFetchFromEnv", () => {
|
|||||||
afterEach(() => vi.unstubAllEnvs());
|
afterEach(() => vi.unstubAllEnvs());
|
||||||
|
|
||||||
it("returns undefined when no proxy env vars are set", () => {
|
it("returns undefined when no proxy env vars are set", () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "");
|
expect(resolveProxyFetchFromEnv({})).toBeUndefined();
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
|
||||||
vi.stubEnv("https_proxy", "");
|
|
||||||
vi.stubEnv("http_proxy", "");
|
|
||||||
|
|
||||||
expect(resolveProxyFetchFromEnv()).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns proxy fetch using EnvHttpProxyAgent when HTTPS_PROXY is set", async () => {
|
it("returns proxy fetch using EnvHttpProxyAgent when HTTPS_PROXY is set", async () => {
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
|
||||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
|
||||||
undiciFetch.mockResolvedValue({ ok: true });
|
undiciFetch.mockResolvedValue({ ok: true });
|
||||||
|
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
const fetchFn = resolveProxyFetchFromEnv({
|
||||||
|
HTTP_PROXY: "",
|
||||||
|
HTTPS_PROXY: "http://proxy.test:8080",
|
||||||
|
});
|
||||||
expect(fetchFn).toBeDefined();
|
expect(fetchFn).toBeDefined();
|
||||||
expect(envAgentSpy).toHaveBeenCalled();
|
expect(envAgentSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -89,46 +85,47 @@ describe("resolveProxyFetchFromEnv", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns proxy fetch when HTTP_PROXY is set", () => {
|
it("returns proxy fetch when HTTP_PROXY is set", () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "");
|
const fetchFn = resolveProxyFetchFromEnv({
|
||||||
vi.stubEnv("HTTP_PROXY", "http://fallback.test:3128");
|
HTTPS_PROXY: "",
|
||||||
|
HTTP_PROXY: "http://fallback.test:3128",
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
});
|
||||||
expect(fetchFn).toBeDefined();
|
expect(fetchFn).toBeDefined();
|
||||||
expect(envAgentSpy).toHaveBeenCalled();
|
expect(envAgentSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns proxy fetch when lowercase https_proxy is set", () => {
|
it("returns proxy fetch when lowercase https_proxy is set", () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "");
|
const fetchFn = resolveProxyFetchFromEnv({
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
HTTPS_PROXY: "",
|
||||||
vi.stubEnv("http_proxy", "");
|
HTTP_PROXY: "",
|
||||||
vi.stubEnv("https_proxy", "http://lower.test:1080");
|
http_proxy: "",
|
||||||
|
https_proxy: "http://lower.test:1080",
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
});
|
||||||
expect(fetchFn).toBeDefined();
|
expect(fetchFn).toBeDefined();
|
||||||
expect(envAgentSpy).toHaveBeenCalled();
|
expect(envAgentSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns proxy fetch when lowercase http_proxy is set", () => {
|
it("returns proxy fetch when lowercase http_proxy is set", () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "");
|
const fetchFn = resolveProxyFetchFromEnv({
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
HTTPS_PROXY: "",
|
||||||
vi.stubEnv("https_proxy", "");
|
HTTP_PROXY: "",
|
||||||
vi.stubEnv("http_proxy", "http://lower-http.test:1080");
|
https_proxy: "",
|
||||||
|
http_proxy: "http://lower-http.test:1080",
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
});
|
||||||
expect(fetchFn).toBeDefined();
|
expect(fetchFn).toBeDefined();
|
||||||
expect(envAgentSpy).toHaveBeenCalled();
|
expect(envAgentSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns undefined when EnvHttpProxyAgent constructor throws", () => {
|
it("returns undefined when EnvHttpProxyAgent constructor throws", () => {
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
|
||||||
vi.stubEnv("https_proxy", "");
|
|
||||||
vi.stubEnv("http_proxy", "");
|
|
||||||
vi.stubEnv("HTTPS_PROXY", "not-a-valid-url");
|
|
||||||
envAgentSpy.mockImplementationOnce(() => {
|
envAgentSpy.mockImplementationOnce(() => {
|
||||||
throw new Error("Invalid URL");
|
throw new Error("Invalid URL");
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
const fetchFn = resolveProxyFetchFromEnv({
|
||||||
|
HTTP_PROXY: "",
|
||||||
|
https_proxy: "",
|
||||||
|
http_proxy: "",
|
||||||
|
HTTPS_PROXY: "not-a-valid-url",
|
||||||
|
});
|
||||||
expect(fetchFn).toBeUndefined();
|
expect(fetchFn).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ export function getProxyUrlFromFetch(fetchImpl?: typeof fetch): string | undefin
|
|||||||
* Returns undefined when no proxy is configured.
|
* Returns undefined when no proxy is configured.
|
||||||
* Gracefully returns undefined if the proxy URL is malformed.
|
* Gracefully returns undefined if the proxy URL is malformed.
|
||||||
*/
|
*/
|
||||||
export function resolveProxyFetchFromEnv(): typeof fetch | undefined {
|
export function resolveProxyFetchFromEnv(
|
||||||
if (!hasEnvHttpProxyConfigured("https")) {
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): typeof fetch | undefined {
|
||||||
|
if (!hasEnvHttpProxyConfigured("https", env)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -200,6 +200,29 @@ describe("discoverOpenClawPlugins", () => {
|
|||||||
expect(ids).toContain("voice-call");
|
expect(ids).toContain("voice-call");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes bundled provider package ids to canonical plugin ids", async () => {
|
||||||
|
const stateDir = makeTempDir();
|
||||||
|
const globalExt = path.join(stateDir, "extensions", "ollama-provider-pack");
|
||||||
|
mkdirSafe(path.join(globalExt, "src"));
|
||||||
|
|
||||||
|
writePluginPackageManifest({
|
||||||
|
packageDir: globalExt,
|
||||||
|
packageName: "@openclaw/ollama-provider",
|
||||||
|
extensions: ["./src/index.ts"],
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(globalExt, "src", "index.ts"),
|
||||||
|
"export default function () {}",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { candidates } = await discoverWithStateDir(stateDir, {});
|
||||||
|
|
||||||
|
const ids = candidates.map((c) => c.idHint);
|
||||||
|
expect(ids).toContain("ollama");
|
||||||
|
expect(ids).not.toContain("ollama-provider");
|
||||||
|
});
|
||||||
|
|
||||||
it("treats configured directory paths as plugin packages", async () => {
|
it("treats configured directory paths as plugin packages", async () => {
|
||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const packDir = path.join(stateDir, "packs", "demo-plugin-dir");
|
const packDir = path.join(stateDir, "packs", "demo-plugin-dir");
|
||||||
|
|||||||
@@ -333,11 +333,17 @@ function deriveIdHint(params: {
|
|||||||
const unscoped = rawPackageName.includes("/")
|
const unscoped = rawPackageName.includes("/")
|
||||||
? (rawPackageName.split("/").pop() ?? rawPackageName)
|
? (rawPackageName.split("/").pop() ?? rawPackageName)
|
||||||
: rawPackageName;
|
: rawPackageName;
|
||||||
|
const canonicalPackageId =
|
||||||
|
{
|
||||||
|
"ollama-provider": "ollama",
|
||||||
|
"sglang-provider": "sglang",
|
||||||
|
"vllm-provider": "vllm",
|
||||||
|
}[unscoped] ?? unscoped;
|
||||||
|
|
||||||
if (!params.hasMultipleExtensions) {
|
if (!params.hasMultipleExtensions) {
|
||||||
return unscoped;
|
return canonicalPackageId;
|
||||||
}
|
}
|
||||||
return `${unscoped}/${base}`;
|
return `${canonicalPackageId}/${base}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCandidate(params: {
|
function addCandidate(params: {
|
||||||
|
|||||||
@@ -2182,4 +2182,41 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
);
|
);
|
||||||
expect(finalTextSentViaDeliverReplies).toBe(true);
|
expect(finalTextSentViaDeliverReplies).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows compacting reaction during auto-compaction and resumes thinking", async () => {
|
||||||
|
const statusReactionController = {
|
||||||
|
setThinking: vi.fn(async () => {}),
|
||||||
|
setCompacting: vi.fn(async () => {}),
|
||||||
|
setTool: vi.fn(async () => {}),
|
||||||
|
setDone: vi.fn(async () => {}),
|
||||||
|
setError: vi.fn(async () => {}),
|
||||||
|
setQueued: vi.fn(async () => {}),
|
||||||
|
cancelPending: vi.fn(() => {}),
|
||||||
|
clear: vi.fn(async () => {}),
|
||||||
|
restoreInitial: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
|
||||||
|
await replyOptions?.onCompactionStart?.();
|
||||||
|
await replyOptions?.onCompactionEnd?.();
|
||||||
|
return { queuedFinal: true };
|
||||||
|
});
|
||||||
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
|
|
||||||
|
await dispatchWithContext({
|
||||||
|
context: createContext({
|
||||||
|
statusReactionController: statusReactionController as never,
|
||||||
|
}),
|
||||||
|
streamMode: "off",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(statusReactionController.setCompacting).toHaveBeenCalledTimes(1);
|
||||||
|
expect(statusReactionController.cancelPending).toHaveBeenCalledTimes(1);
|
||||||
|
expect(statusReactionController.setThinking).toHaveBeenCalledTimes(2);
|
||||||
|
expect(statusReactionController.setCompacting.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
statusReactionController.cancelPending.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
expect(statusReactionController.cancelPending.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
statusReactionController.setThinking.mock.invocationCallOrder[1],
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -713,6 +713,15 @@ export const dispatchTelegramMessage = async ({
|
|||||||
await statusReactionController.setTool(payload.name);
|
await statusReactionController.setTool(payload.name);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
onCompactionStart: statusReactionController
|
||||||
|
? () => statusReactionController.setCompacting()
|
||||||
|
: undefined,
|
||||||
|
onCompactionEnd: statusReactionController
|
||||||
|
? async () => {
|
||||||
|
statusReactionController.cancelPending();
|
||||||
|
await statusReactionController.setThinking();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
onModelSelected,
|
onModelSelected,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const TELEGRAM_STATUS_REACTION_VARIANTS: Record<StatusReactionEmojiKey, s
|
|||||||
error: ["😱", "😨", "🤯"],
|
error: ["😱", "😨", "🤯"],
|
||||||
stallSoft: ["🥱", "😴", "🤔"],
|
stallSoft: ["🥱", "😴", "🤔"],
|
||||||
stallHard: ["😨", "😱", "⚡"],
|
stallHard: ["😨", "😱", "⚡"],
|
||||||
|
compacting: ["✍", "🤔", "🤯"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
||||||
@@ -102,6 +103,7 @@ const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
|||||||
"error",
|
"error",
|
||||||
"stallSoft",
|
"stallSoft",
|
||||||
"stallHard",
|
"stallHard",
|
||||||
|
"compacting",
|
||||||
];
|
];
|
||||||
|
|
||||||
function normalizeEmoji(value: string | undefined): string | undefined {
|
function normalizeEmoji(value: string | undefined): string | undefined {
|
||||||
@@ -129,6 +131,7 @@ export function resolveTelegramStatusReactionEmojis(params: {
|
|||||||
error: normalizeEmoji(overrides?.error) ?? DEFAULT_EMOJIS.error,
|
error: normalizeEmoji(overrides?.error) ?? DEFAULT_EMOJIS.error,
|
||||||
stallSoft: normalizeEmoji(overrides?.stallSoft) ?? DEFAULT_EMOJIS.stallSoft,
|
stallSoft: normalizeEmoji(overrides?.stallSoft) ?? DEFAULT_EMOJIS.stallSoft,
|
||||||
stallHard: normalizeEmoji(overrides?.stallHard) ?? DEFAULT_EMOJIS.stallHard,
|
stallHard: normalizeEmoji(overrides?.stallHard) ?? DEFAULT_EMOJIS.stallHard,
|
||||||
|
compacting: normalizeEmoji(overrides?.compacting) ?? DEFAULT_EMOJIS.compacting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
ui/src/ui/app-chat.test.ts
Normal file
65
ui/src/ui/app-chat.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { refreshChatAvatar, type ChatHost } from "./app-chat.ts";
|
||||||
|
|
||||||
|
function makeHost(overrides?: Partial<ChatHost>): ChatHost {
|
||||||
|
return {
|
||||||
|
client: null,
|
||||||
|
chatMessages: [],
|
||||||
|
chatStream: null,
|
||||||
|
connected: true,
|
||||||
|
chatMessage: "",
|
||||||
|
chatAttachments: [],
|
||||||
|
chatQueue: [],
|
||||||
|
chatRunId: null,
|
||||||
|
chatSending: false,
|
||||||
|
lastError: null,
|
||||||
|
sessionKey: "agent:main",
|
||||||
|
basePath: "",
|
||||||
|
hello: null,
|
||||||
|
chatAvatarUrl: null,
|
||||||
|
refreshSessionsAfterChat: new Set<string>(),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("refreshChatAvatar", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a route-relative avatar endpoint before basePath bootstrap finishes", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ avatarUrl: "/avatar/main" }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||||
|
|
||||||
|
const host = makeHost({ basePath: "", sessionKey: "agent:main" });
|
||||||
|
await refreshChatAvatar(host);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
"avatar/main?meta=1",
|
||||||
|
expect.objectContaining({ method: "GET" }),
|
||||||
|
);
|
||||||
|
expect(host.chatAvatarUrl).toBe("/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps mounted dashboard avatar endpoints under the normalized base path", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
json: async () => ({}),
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||||
|
|
||||||
|
const host = makeHost({ basePath: "/openclaw/", sessionKey: "agent:ops:main" });
|
||||||
|
await refreshChatAvatar(host);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
"/openclaw/avatar/ops?meta=1",
|
||||||
|
expect.objectContaining({ method: "GET" }),
|
||||||
|
);
|
||||||
|
expect(host.chatAvatarUrl).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -372,7 +372,7 @@ function resolveAgentIdForSession(host: ChatHost): string | null {
|
|||||||
function buildAvatarMetaUrl(basePath: string, agentId: string): string {
|
function buildAvatarMetaUrl(basePath: string, agentId: string): string {
|
||||||
const base = normalizeBasePath(basePath);
|
const base = normalizeBasePath(basePath);
|
||||||
const encoded = encodeURIComponent(agentId);
|
const encoded = encodeURIComponent(agentId);
|
||||||
return base ? `${base}/avatar/${encoded}?meta=1` : `/avatar/${encoded}?meta=1`;
|
return base ? `${base}/avatar/${encoded}?meta=1` : `avatar/${encoded}?meta=1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshChatAvatar(host: ChatHost) {
|
export async function refreshChatAvatar(host: ChatHost) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
normalizeVerboseLevel,
|
normalizeVerboseLevel,
|
||||||
resolveThinkingDefaultForModel,
|
resolveThinkingDefaultForModel,
|
||||||
} from "../../../../src/auto-reply/thinking.js";
|
} from "../../../../src/auto-reply/thinking.js";
|
||||||
import type { HealthSummary } from "../../../../src/commands/health.js";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENT_ID,
|
DEFAULT_AGENT_ID,
|
||||||
DEFAULT_MAIN_KEY,
|
DEFAULT_MAIN_KEY,
|
||||||
@@ -45,8 +44,6 @@ export async function executeSlashCommand(
|
|||||||
switch (commandName) {
|
switch (commandName) {
|
||||||
case "help":
|
case "help":
|
||||||
return executeHelp();
|
return executeHelp();
|
||||||
case "status":
|
|
||||||
return await executeStatus(client);
|
|
||||||
case "new":
|
case "new":
|
||||||
return { content: "Starting new session...", action: "new-session" };
|
return { content: "Starting new session...", action: "new-session" };
|
||||||
case "reset":
|
case "reset":
|
||||||
@@ -101,27 +98,6 @@ function executeHelp(): SlashCommandResult {
|
|||||||
return { content: lines.join("\n") };
|
return { content: lines.join("\n") };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStatus(client: GatewayBrowserClient): Promise<SlashCommandResult> {
|
|
||||||
try {
|
|
||||||
const health = await client.request<HealthSummary>("health", {});
|
|
||||||
const status = health.ok ? "Healthy" : "Degraded";
|
|
||||||
const agentCount = health.agents?.length ?? 0;
|
|
||||||
const sessionCount = health.sessions?.count ?? 0;
|
|
||||||
const lines = [
|
|
||||||
`**System Status:** ${status}`,
|
|
||||||
`**Agents:** ${agentCount}`,
|
|
||||||
`**Sessions:** ${sessionCount}`,
|
|
||||||
`**Default Agent:** ${health.defaultAgentId || "none"}`,
|
|
||||||
];
|
|
||||||
if (health.durationMs) {
|
|
||||||
lines.push(`**Response:** ${health.durationMs}ms`);
|
|
||||||
}
|
|
||||||
return { content: lines.join("\n") };
|
|
||||||
} catch (err) {
|
|
||||||
return { content: `Failed to fetch status: ${String(err)}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCompact(
|
async function executeCompact(
|
||||||
client: GatewayBrowserClient,
|
client: GatewayBrowserClient,
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { parseSlashCommand } from "./slash-commands.ts";
|
import { parseSlashCommand, SLASH_COMMANDS } from "./slash-commands.ts";
|
||||||
|
|
||||||
describe("parseSlashCommand", () => {
|
describe("parseSlashCommand", () => {
|
||||||
it("parses commands with an optional colon separator", () => {
|
it("parses commands with an optional colon separator", () => {
|
||||||
@@ -30,4 +30,13 @@ describe("parseSlashCommand", () => {
|
|||||||
args: "on",
|
args: "on",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps /status on the agent path", () => {
|
||||||
|
const status = SLASH_COMMANDS.find((entry) => entry.name === "status");
|
||||||
|
expect(status?.executeLocal).not.toBe(true);
|
||||||
|
expect(parseSlashCommand("/status")).toMatchObject({
|
||||||
|
command: { name: "status" },
|
||||||
|
args: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,10 +108,9 @@ export const SLASH_COMMANDS: SlashCommandDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status",
|
name: "status",
|
||||||
description: "Show system status",
|
description: "Show session status",
|
||||||
icon: "barChart",
|
icon: "barChart",
|
||||||
category: "tools",
|
category: "tools",
|
||||||
executeLocal: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "export",
|
name: "export",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
agentLogoUrl,
|
agentLogoUrl,
|
||||||
resolveConfiguredCronModelSuggestions,
|
resolveConfiguredCronModelSuggestions,
|
||||||
|
resolveAgentAvatarUrl,
|
||||||
resolveEffectiveModelFallbacks,
|
resolveEffectiveModelFallbacks,
|
||||||
sortLocaleStrings,
|
sortLocaleStrings,
|
||||||
} from "./agents-utils.ts";
|
} from "./agents-utils.ts";
|
||||||
@@ -110,3 +111,23 @@ describe("agentLogoUrl", () => {
|
|||||||
expect(agentLogoUrl("")).toBe("favicon.svg");
|
expect(agentLogoUrl("")).toBe("favicon.svg");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveAgentAvatarUrl", () => {
|
||||||
|
it("prefers a runtime avatar URL over non-URL identity avatars", () => {
|
||||||
|
expect(
|
||||||
|
resolveAgentAvatarUrl(
|
||||||
|
{ identity: { avatar: "A", avatarUrl: "/avatar/main" } },
|
||||||
|
{
|
||||||
|
agentId: "main",
|
||||||
|
avatar: "A",
|
||||||
|
name: "Main",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toBe("/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for initials or emoji avatar values without a URL", () => {
|
||||||
|
expect(resolveAgentAvatarUrl({ identity: { avatar: "A" } })).toBeNull();
|
||||||
|
expect(resolveAgentAvatarUrl({ identity: { avatar: "🦞" } })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -200,15 +200,18 @@ export function resolveAgentAvatarUrl(
|
|||||||
agent: { identity?: { avatar?: string; avatarUrl?: string } },
|
agent: { identity?: { avatar?: string; avatarUrl?: string } },
|
||||||
agentIdentity?: AgentIdentityResult | null,
|
agentIdentity?: AgentIdentityResult | null,
|
||||||
): string | null {
|
): string | null {
|
||||||
const url =
|
const candidates = [
|
||||||
agentIdentity?.avatar?.trim() ??
|
agentIdentity?.avatar?.trim(),
|
||||||
agent.identity?.avatarUrl?.trim() ??
|
agent.identity?.avatarUrl?.trim(),
|
||||||
agent.identity?.avatar?.trim();
|
agent.identity?.avatar?.trim(),
|
||||||
if (!url) {
|
];
|
||||||
return null;
|
for (const candidate of candidates) {
|
||||||
}
|
if (!candidate) {
|
||||||
if (AVATAR_URL_RE.test(url)) {
|
continue;
|
||||||
return url;
|
}
|
||||||
|
if (AVATAR_URL_RE.test(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { SessionsListResult } from "../types.ts";
|
import type { SessionsListResult } from "../types.ts";
|
||||||
@@ -54,6 +56,95 @@ function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("chat view", () => {
|
describe("chat view", () => {
|
||||||
|
it("uses the assistant avatar URL for the welcome state when the identity avatar is only initials", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderChat(
|
||||||
|
createProps({
|
||||||
|
assistantName: "Assistant",
|
||||||
|
assistantAvatar: "A",
|
||||||
|
assistantAvatarUrl: "/avatar/main",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const welcomeImage = container.querySelector<HTMLImageElement>(".agent-chat__welcome > img");
|
||||||
|
expect(welcomeImage).not.toBeNull();
|
||||||
|
expect(welcomeImage?.getAttribute("src")).toBe("/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the bundled logo in the welcome state when the assistant avatar is not a URL", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderChat(
|
||||||
|
createProps({
|
||||||
|
assistantName: "Assistant",
|
||||||
|
assistantAvatar: "A",
|
||||||
|
assistantAvatarUrl: null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const welcomeImage = container.querySelector<HTMLImageElement>(".agent-chat__welcome > img");
|
||||||
|
const logoImage = container.querySelector<HTMLImageElement>(
|
||||||
|
".agent-chat__welcome .agent-chat__avatar--logo img",
|
||||||
|
);
|
||||||
|
expect(welcomeImage).toBeNull();
|
||||||
|
expect(logoImage).not.toBeNull();
|
||||||
|
expect(logoImage?.getAttribute("src")).toBe("favicon.svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the welcome logo fallback under the mounted base path", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderChat(
|
||||||
|
createProps({
|
||||||
|
assistantName: "Assistant",
|
||||||
|
assistantAvatar: "A",
|
||||||
|
assistantAvatarUrl: null,
|
||||||
|
basePath: "/openclaw/",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const logoImage = container.querySelector<HTMLImageElement>(
|
||||||
|
".agent-chat__welcome .agent-chat__avatar--logo img",
|
||||||
|
);
|
||||||
|
expect(logoImage).not.toBeNull();
|
||||||
|
expect(logoImage?.getAttribute("src")).toBe("/openclaw/favicon.svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps grouped assistant avatar fallbacks under the mounted base path", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderChat(
|
||||||
|
createProps({
|
||||||
|
assistantName: "Assistant",
|
||||||
|
assistantAvatar: "A",
|
||||||
|
assistantAvatarUrl: null,
|
||||||
|
basePath: "/openclaw/",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "hello",
|
||||||
|
timestamp: 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedLogo = container.querySelector<HTMLImageElement>(
|
||||||
|
".chat-group.assistant .chat-avatar--logo",
|
||||||
|
);
|
||||||
|
expect(groupedLogo).not.toBeNull();
|
||||||
|
expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders compacting indicator as a badge", () => {
|
it("renders compacting indicator as a badge", () => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { detectTextDirection } from "../text-direction.ts";
|
|||||||
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
|
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
|
||||||
import type { ChatItem, MessageGroup } from "../types/chat-types.ts";
|
import type { ChatItem, MessageGroup } from "../types/chat-types.ts";
|
||||||
import type { ChatAttachment, ChatQueueItem } from "../ui-types.ts";
|
import type { ChatAttachment, ChatQueueItem } from "../ui-types.ts";
|
||||||
import { agentLogoUrl } from "./agents-utils.ts";
|
import { agentLogoUrl, resolveAgentAvatarUrl } from "./agents-utils.ts";
|
||||||
import { renderMarkdownSidebar } from "./markdown-sidebar.ts";
|
import { renderMarkdownSidebar } from "./markdown-sidebar.ts";
|
||||||
import "../components/resizable-divider.ts";
|
import "../components/resizable-divider.ts";
|
||||||
|
|
||||||
@@ -566,7 +566,12 @@ const WELCOME_SUGGESTIONS = [
|
|||||||
|
|
||||||
function renderWelcomeState(props: ChatProps): TemplateResult {
|
function renderWelcomeState(props: ChatProps): TemplateResult {
|
||||||
const name = props.assistantName || "Assistant";
|
const name = props.assistantName || "Assistant";
|
||||||
const avatar = props.assistantAvatar ?? props.assistantAvatarUrl;
|
const avatar = resolveAgentAvatarUrl({
|
||||||
|
identity: {
|
||||||
|
avatar: props.assistantAvatar ?? undefined,
|
||||||
|
avatarUrl: props.assistantAvatarUrl ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
const logoUrl = agentLogoUrl(props.basePath ?? "");
|
const logoUrl = agentLogoUrl(props.basePath ?? "");
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -802,7 +807,13 @@ export function renderChat(props: ChatProps) {
|
|||||||
const showReasoning = props.showThinking && reasoningLevel !== "off";
|
const showReasoning = props.showThinking && reasoningLevel !== "off";
|
||||||
const assistantIdentity = {
|
const assistantIdentity = {
|
||||||
name: props.assistantName,
|
name: props.assistantName,
|
||||||
avatar: props.assistantAvatar ?? props.assistantAvatarUrl ?? null,
|
avatar:
|
||||||
|
resolveAgentAvatarUrl({
|
||||||
|
identity: {
|
||||||
|
avatar: props.assistantAvatar ?? undefined,
|
||||||
|
avatarUrl: props.assistantAvatarUrl ?? undefined,
|
||||||
|
},
|
||||||
|
}) ?? null,
|
||||||
};
|
};
|
||||||
const pinned = getPinnedMessages(props.sessionKey);
|
const pinned = getPinnedMessages(props.sessionKey);
|
||||||
const deleted = getDeletedMessages(props.sessionKey);
|
const deleted = getDeletedMessages(props.sessionKey);
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ export default defineConfig({
|
|||||||
"src/**/*.test.ts",
|
"src/**/*.test.ts",
|
||||||
"extensions/**/*.test.ts",
|
"extensions/**/*.test.ts",
|
||||||
"test/**/*.test.ts",
|
"test/**/*.test.ts",
|
||||||
|
"ui/src/ui/app-chat.test.ts",
|
||||||
"ui/src/ui/views/agents-utils.test.ts",
|
"ui/src/ui/views/agents-utils.test.ts",
|
||||||
|
"ui/src/ui/views/chat.test.ts",
|
||||||
"ui/src/ui/views/usage-render-details.test.ts",
|
"ui/src/ui/views/usage-render-details.test.ts",
|
||||||
"ui/src/ui/controllers/agents.test.ts",
|
"ui/src/ui/controllers/agents.test.ts",
|
||||||
"ui/src/ui/controllers/chat.test.ts",
|
"ui/src/ui/controllers/chat.test.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user