Register Requesty (https://router.requesty.ai/v1) next to OpenRouter on the
existing OpenAI-compatible transport: config and env vars
(GOCLAW_REQUESTY_API_KEY, GOCLAW_REQUESTY_BASE_URL), secret masking, DB and
in-memory registration, CLI setup, doctor, placeholder provider, OpenAPI enum,
Web/Desktop provider lists and docs.
The models list merges Requesty managed policies (GET /models/managed) with
the key's catalog from GET /models.
* feat(hooks/observe): add structured output validation hook (ObserveHook)
- hooks/types.go: Add ObserveHook interface + BuiltInHookType enum
- hooks/dispatcher.go: HookDispatcher emits ObserveHook with PhaseResult payload
- pipeline/observe_stage.go: ObserveStage emits hook after ObserveResult built
- pipeline/substates.go: ObserveStageResult carries hook results + validation errors
- hooks/config.go: BuiltInHookTypeObserve added to BuiltInHookType enum
PhaseResult carries structured output, token usage, tool calls + validation errors
emitted post-ObserveStage. ObserveHook implementations can validate structured
output against schemas, detect tool-call loops, enforce token budgets, etc.
Hook fires after ObserveStage produces ObserveResult, before results propagate
to next stage. ValidationError returned by hook halts pipeline and propagates
error to caller without further stage execution.
Co-Authored-By: Claude <noreply@anthropic.com>
* ui(hooks): add post_model_response event to web UI
- Add event to Zod schema, filter dropdown, and form dialog
- Implement conditional test panel UI for model response payload
- Add translations (en/zh/vi) for new test panel fields
- Updated beta description to reference the new event
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(mcp): add MCP CRUD server exposing goclaw resources at /api/mcp/ with Bearer token auth
and X-GoClaw-Tenant-Id header, default to master tenant
* feat(mcp): add goclaw_skills_write_file tool to edit skill files on disk
The CRUD MCP server's goclaw_skills_update only touched skill DB metadata,
with no way to edit a skill's SKILL.md/file content on the filesystem. Extract
the versioned write logic from the web UI's skill file editor
(SkillsHandler.handleWriteFile) into skills.WriteVersionedFile so both
surfaces share identical validation and versioning, and expose it as a new
MCP tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Introduces an explicit backend selector for the browser automation
tool ("chrome" or "lightpanda"). Empty means auto-detect, which will
be wired in a later commit. Chrome remains the default behavior.
Server-side webhook agent runs (sync, async, and admin test) used Stream:false,
so OpenAI-compatible routers that only cache streaming requests never populated
or served their prompt cache. Webhook runs paid full input-token price on every
turn even with a stable session and an identical multi-turn prefix, while WS chat
(Stream:true) got cache hits from the 3rd turn.
Add gateway.webhook_stream (default true, env GOCLAW_WEBHOOK_STREAM) and apply it
to all three run sites. ChatStream returns the fully assembled response, so the
payload returned to the caller is unchanged. Set to false to restore non-streaming.
Replace the hardcoded 30s webhook agent-run deadline with a configurable
timeout (default 600s, cap 3600s) for both the async worker and the
sync/test HTTP handler. Legitimate multi-step runs were dying at 30s
mid-tool-call; the same request over WebSocket completed fine.
- internal/webhooks/timeout.go: ResolveTimeoutSec helper (<=0 → 600s, cap 3600s)
- WorkerConfig.AsyncAgentTimeout (async worker) + WebhookLLMHandler.syncTimeout
(sync + admin test), wired from config
- config keys gateway.webhook_{async,sync}_timeout_sec, also settable via
GOCLAW_WEBHOOK_{ASYNC,SYNC}_TIMEOUT_SEC env (env overrides config)
- tests: timeout bounds + config file/env override
Allow overriding the cron job execution timeout via env var, matching
the existing GOCLAW_* env var pattern used throughout the codebase.
Falls back to the existing default (10m) when unset or invalid, since
CronConfig.JobTimeoutDuration() already handles parse errors with a
slog.Warn and the DefaultJobTimeout constant.
Co-authored-by: will.nguyen <will.nguyen@gemcommerce.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registering a remote MCP server (sse/streamable-http) whose hostname
resolves to a private IP is rejected at config-validation time by the
SSRF guard, with no production escape hatch -- the only bypass is a
test-only loopback flag. Self-hosted MCP servers on a private network
are therefore unregisterable, even though the runtime MCP client
connects to them fine (Test Connection succeeds and lists tools; only
create/update input validation blocks).
Add an opt-in, operator-configured allowlist (GOCLAW_MCP_ALLOWED_HOSTS,
empty by default) of trusted hostnames exempt from the private/loopback
IP block during MCP server URL validation only:
- security.ValidateAllowingHosts(url, allowedHosts): like Validate but
skips the private/loopback block for allowlisted hostnames. The
cloud-metadata/link-local (169.254/fe80), multicast and unspecified
ranges are never exempted, even for allowlisted hosts.
- mcp.SetAllowedHosts wires the operator allowlist into ValidateURL /
ValidateServerConfig; default empty => no behavior change.
- web_fetch / webhook / redirect SSRF paths are unchanged (those stay
agent-influenced and fully guarded).
Matching is case-insensitive on the pre-resolution hostname.
* feat(providers): add Google Cloud Vertex AI provider (#576)
Add `vertex` built-in provider type that routes Gemini calls through Google
Cloud Vertex AI's OpenAI-compatible endpoint. Enterprises on GCP can now use
regional endpoints for data residency, consolidate AI spend under existing GCP
billing, enforce IAM/VPC-SC controls, and use committed-use discounts instead
of standalone Google AI Studio API keys.
Implementation reuses OpenAIProvider via the OpenAI-compat path; the only
provider-specific logic is OAuth2 auth wiring:
- New factory NewVertexProvider in internal/providers/vertex.go builds an
*http.Client with oauth2.Transport, which auto-refreshes GCP access tokens
(1-hour lifetime) transparently. Credentials precedence:
inline SA JSON > credentials_file path > Application Default Credentials
(works on GKE/Cloud Run/Compute Engine via metadata server).
- OpenAIProvider gets WithHTTPClient() + WithoutAuthHeader() options so the
oauth2 transport injects Authorization rather than doRequest() setting a
static Bearer header.
- Endpoint URL computed at registration time from project_id + region:
https://{region}-aiplatform.googleapis.com/v1/projects/{p}/locations/{r}/endpoints/openapi
- Store: api_key column holds AES-256-GCM-encrypted SA JSON (same as other
providers); settings JSONB holds {project_id, region, model}.
- Env vars: GOCLAW_VERTEX_{API_KEY,CREDENTIALS_FILE,PROJECT_ID,REGION,MODEL}.
Registration wired through all three paths: config-driven startup, DB-driven
startup, and HTTP CRUD in-memory registration. Vertex handled before the
generic "api_key empty" guard so ADC deployments register correctly.
Code-review fixes applied:
- H1 (correctness): Gemini thought_signature detection in openai.go now
recognizes providerType="vertex" and apiBase suffix "aiplatform". Previously
only worked because the default model string coincidentally contained
"gemini"; custom model IDs or fine-tuned endpoint numeric IDs would drop the
signature on passback and trigger HTTP 400 mid-tool-loop. Regression test
added (TestVertexProviderForwardsThoughtSignatureOnToolCalls).
- M1 (hardening): region and project_id are regex-validated before URL
concatenation to prevent hostname injection (e.g. region="evil.com/a?").
- M2 (hardening): APIBaseOverride must be https + *.googleapis.com host to
prevent data exfiltration via crafted DB rows.
- M3 (documentation): CredentialsFile marked operator-only in the struct
comment — never expose via admin UI or DB settings without path allow-list.
Tests: 17 Vertex-related unit tests. go build ./... + go build -tags sqliteonly
./... + go vet ./... all clean. Pre-existing TestSignMediaPath failure on
Windows (file_token.go uses path/filepath) is unrelated to this change.
* chore: trigger CI on digitopvn/goclaw fork
* ci: ping
* ci: retrigger workflows
Remove Tools.Web and related WebToolsConfig, DuckDuckGoConfig from config
struct and Default(). Web search providers now resolved dynamically from
config_secrets (tenant-scoped) and builtin_tool_tenant_configs instead of
hardcoded config.json5.
Update config_load_test to match removed config paths. Separate config_secrets
and config_channels loading to clarify schema evolution.
Add env var support for gateway.allowed_origins so container deployments
can set CORS origins reliably even if config file is overwritten by UI
save cycles. Follows same pattern as GOCLAW_OWNER_IDS.
Root cause: compaction threshold compared total prompt tokens (including
system prompt, tool definitions, context files) against budget. Sessions
with only 50-60k of conversation could trigger compaction when overhead
pushed total to 120k+.
Changes:
- Enable context pruning by default for all providers (was Anthropic-only)
- Add EstimateHistoryTokens() that excludes system messages from count
- Fix EstimateTokens() to include ToolCall argument sizes
- Calibrate overhead on first LLM response, subtract from threshold checks
- Mid-loop: prune old tool results first (at 70% budget), compact only if
still over budget after pruning
- maybeSummarize: subtract overhead from calibration base
- Remove applyContextPruningDefaults() (Anthropic-only auto-enable)
- Simplify context pruning UI: show only Keep Last Assistants by default,
hide advanced params behind collapsible, remove Mode selector
* feat(auth): support named chatgpt oauth providers
- add provider-scoped ChatGPT OAuth routes and CLI support
- persist refresh tokens per provider and reject provider-type collisions
- wire provider OAuth setup flows in the dashboard and setup UI
Refs #448
* feat(agent): add chatgpt oauth account routing
- add agent other_config routing for manual and round-robin selection
- reuse routed provider resolution across resolver and pending loaders
- add router, parser, and agent advanced dialog coverage for multi-account use
Refs #448
* docs(api): describe chatgpt oauth routing
- document named-provider ChatGPT OAuth auth routes
- describe agent-side account routing and round-robin behavior
- update OpenAPI agent config schema and provider type enum
Refs #448
* fix(store): add missing agent key context helpers
* feat(ui): clarify chatgpt oauth account setup and routing
* docs(providers): align chatgpt oauth alias examples
* feat(agent): add codex pool activity dashboard
* fix(providers): harden codex oauth alias setup
* feat(codex-pool): improve routing dashboard UX
- redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence
- add clearer selection, attention, and recent-proof states for pool members
- make the lower panels fill the remaining desktop viewport while staying responsive
* fix(store): resolve context helper merge duplication
* feat(oauth): add codex pool quota and observation APIs
- add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers
- teach codex routing to surface pool activity, observation metadata, and quota-aware readiness
- extend tests and HTTP docs/OpenAPI for the new pool monitoring flows
* feat(web): add codex pool quota monitor and controls
- add provider quota fetching, readiness badges, and live routing evidence on the account pool page
- redesign pool setup and activity panels for multi-account management with localized copy updates
- keep the live monitor internally scrollable and compact the account cards for better viewport fit
* fix(web): clarify pool routing labels
- rename the recent request badge from Direct to Selected
- restore compact quota bars in the live pool cards
* feat(codex-pool): add runtime health dashboard
- derive per-provider success and failure health from routed Codex traces
- surface routing, quota, and recent request evidence in the pool UI
- align provider alias guidance and owner access with the dashboard role model
* docs(auth): document tenant scoping and key roles
* fix(auth): harden tenant and codex pool access control
* fix(providers): align codex pool runtime defaults
* feat(ui): tighten codex pool responsive layout
* feat(chatgpt-oauth): refine codex pool management UX
* feat(chatgpt-oauth): surface quota bars on provider pages
- add compact quota bars to Codex provider rows and provider detail
- fetch quota only for ready visible provider rows and ready detail aliases
- fix managed-member detail visibility and tighten provider locale copy
1. providers/anthropic_stream.go — check scanner.Err() after SSE loop;
truncated responses returned silently on network errors
2. config/config_load.go — trim whitespace in GOCLAW_OWNER_IDS;
trailing spaces caused owner exact-match failures
3. cron/service.go — handle saveUnsafe() errors at 5 call sites;
disk I/O failures lost cron state silently
4. tasks/task_ticker.go — separate timeouts for followups (15s) and
recovery (30s); shared context risked followups starving recovery
5. hooks/use-ws-event.ts — useRef pattern for handler to prevent
subscribe/unsubscribe churn with inline functions
6. markdown-renderer.tsx — AbortController for file fetch with abort
on new fetch and on unmount
7. mcp-form-dialog.tsx — auto-split Command field when user pastes
full command; fix splitShellTokens to preserve commas inside quotes
Replace all hardcoded ~/.goclaw path constructions with configurable
sources (cfg.ResolvedDataDir() for service dirs, cfg.Agents.Defaults.Workspace
for agent workspaces). This fixes data persistence issues in Docker
deployments where paths differ from local dev.
- Add DataDir field to Config with ResolvedDataDir() resolver
- Add ResolvedDataDirFromEnv() package-level helper for packages without Config
- Populate StoreConfig.SkillsStorageDir (was never set, caused hardcoded fallback)
- Agent workspaces now use subdirectory format (workspace/{key}) for volume compatibility
- Remove dead GOCLAW_SESSIONS_STORAGE env/config (sessions moved to PostgreSQL)
- Fix deploy-stg.sh trailing space after backslash + remove deprecated GOCLAW_MODE
- Add GOCLAW_SKILLS_DIR override in docker-compose for volume persistence
* feat(providers): add Ollama local and Ollama Cloud provider support
Adds two new provider variants:
ollama — local/self-hosted Ollama instance
- Gated on providers.ollama.host in config (or GOCLAW_OLLAMA_HOST env)
- No API key required; Ollama's OpenAI-compat endpoint accepts any Bearer value
- Defaults to http://localhost:11434/v1 (configurable for LAN/remote hosts)
- Default model: llama3.3
ollama-cloud — Ollama Cloud (managed remote inference)
- Gated on providers.ollama_cloud.api_key (or GOCLAW_OLLAMA_CLOUD_API_KEY env)
- Bearer token from ollama.com/settings/keys
- Default base URL: https://ollama.com/v1 (overridable via api_base)
- Default model: llama3.3
Both variants use the existing NewOpenAIProvider (OpenAI-compat) — no new
provider struct needed. Both are registered from config file and DB (via
llm_providers table with ProviderOllama / ProviderOllamaCloud types).
OllamaCloud.APIKey follows all existing secret handling patterns:
MaskedCopy, StripSecrets, StripMaskedSecrets.
* feat(providers): wire Ollama into web UI and fix DB registration
- Add ollama + ollama_cloud to PROVIDER_TYPES constants (dropdowns)
- Fix setup wizard: skip API key requirement for Ollama local (isOllama)
- Fix bootstrap status: recognize Ollama local as no-API-key provider
- Add ollama_cloud to config-page KNOWN_PROVIDERS list
- Fix gateway_providers.go: move ProviderOllama before APIKey=='' guard
so DB-registered local Ollama providers actually register at startup
(same pattern as ClaudeCLI, which also needs no API key)
The onboard wizard sets GOCLAW_PROVIDER and GOCLAW_MODEL in .env for
initial bootstrap. Previously these env vars always overrode the config
file value via envStr(), making it impossible to change the default
provider/model through the Dashboard — every save was silently reverted
by ApplyEnvOverrides().
Change envStr to envFallback for these two fields: the env var is only
applied when the config file has no value (empty string). Once the user
saves a provider/model via the Dashboard, the config-file value wins.
Also:
- Stabilize ProviderModelSelect auto-select effect (useRef + useMemo)
- Add toast feedback on config save success/failure
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* feat: add Z.ai provider support (general API + coding plan)
Add Z.ai (GLM) as a new LLM provider with two variants:
- `zai`: general API (api.z.ai/api/paas/v4)
- `zai_coding`: coding plan (api.z.ai/api/coding/paas/v4)
Reuses OpenAIProvider — Z.ai API is OpenAI-compatible with Bearer
token auth, SSE streaming, and reasoning_content support.
Includes: store constants, config struct fields, env var loading
(GOCLAW_ZAI_API_KEY, GOCLAW_ZAI_CODING_API_KEY), secret masking,
config + DB registration, onboard wizard, and UI provider types.
Default model: glm-5
Closes#100
* docs: add Z.ai provider entries to providers documentation
Add optional Redis cache support via `go build -tags redis`, following
the same paired-stub pattern as OTel and Tailscale. The Cache[V] interface
is unchanged; Redis and in-memory implementations are injected at startup
without altering usage logic.
- Add RedisCache[V] implementation with JSON serialization, fail-open on errors
- Add gateway_redis.go / gateway_redis_noop.go paired wiring files
- Refactor GroupWriterCache and ContextFileInterceptor to accept injected caches
- Add GOCLAW_REDIS_DSN env var, docker-compose.redis.yml overlay
- Update Dockerfile and GitHub Actions with ENABLE_REDIS build arg
- Add Redis variant to CI matrix (5 variants: latest, otel, tsnet, redis, full)
Add Claude CLI as an LLM provider (subscription-based, no API key needed).
The CLI manages session history, tool execution, and context while GoClaw
forwards messages and streams responses.
Key features:
- Claude CLI provider with session persistence (--resume)
- MCP bridge server exposing GoClaw tools to CLI via streamable-http
- Security hooks (shell deny patterns, workspace path restrictions)
- Per-session mutex preventing concurrent CLI calls
- Onboard wizard for Claude CLI setup and auth verification
- Web UI for adding/managing Claude CLI provider with auth status
- Provider registry Close() for proper shutdown cleanup
Security:
- CLI path validation (only "claude" or absolute paths from DB)
- Token auth middleware for MCP bridge endpoint
- Shell injection prevention in hook scripts (single-quoted paths)
- Relative path resolution before workspace boundary checks
- Resource leak prevention on provider replace/unregister
Co-authored-by: nhokboo <nhokboo@users.noreply.github.com>
* feat(browser): add remote Chrome sidecar support for Docker deployments
When running in Docker, Chrome is not installed in the runtime image.
This adds support for connecting to a remote Chrome via CDP (Chrome
DevTools Protocol) using a Docker Compose sidecar overlay, following
the existing pattern used by sandbox, OTel, and Tailscale overlays.
Changes:
- Add RemoteURL field to BrowserToolConfig
- Add GOCLAW_BROWSER_REMOTE_URL env var (auto-enables browser tool)
- Browser Manager: remote CDP connection with hostname-to-IP resolution
(required by Chrome M113+ DNS rebinding protection), auto-reconnect
on dead connections, disconnect-only on Stop (sidecar stays alive)
- Auto-start browser on first tool action (no explicit "start" needed)
- Add docker-compose.browser.yml overlay (zenika/alpine-chrome:124)
- Add unit tests for CDP resolution and Manager lifecycle
Usage:
docker compose -f docker-compose.yml -f docker-compose.managed.yml \
-f docker-compose.browser.yml up -d --build
Closes#56
* feat(browser): fix onboard summary and config serialization for remote mode
- onboard.go: show "remote: ws://..." instead of "headless" when RemoteURL is set
- onboard_auto.go: serialize remote_url field in generated config
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Part A — Channel quota limiter (managed mode):
- DB-backed per-user/group request quotas with in-memory 60s TTL cache
- Config merge priority: Groups > Channels > Providers > Default
- Per-group quota override via channels.telegram.groups[chatID].quota
- Migration 000009: index on channel_requests for quota queries
- Hot-reload quota config via pub/sub (TopicConfigChanged)
Part B — Per-run tool call budget:
- Soft stop at configurable limit (default 25, per-agent override)
- MaxToolCalls field on AgentDefaults + AgentSpec + LoopConfig
- LLM gets one final call to summarize when budget exceeded
Part C — Web UI + config page refactor:
- QuotaSection with provider/channel dropdowns (useProviders, useChannelInstances)
- Config page refactored to vertical sidebar tabs layout
- Categories: General, Quota, Agents, Tools, Connections, Advanced, Raw Editor
- Fixed config.patch RPC to serialize raw JSON + baseHash correctly
- Config change pub/sub broadcast from handleApply/handlePatch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SSE: handle "data:" without space (Kimi and other providers)
- Add scanner.Err() check to detect stream read failures
- Echo reasoning_content for thinking models (Kimi, DeepSeek)
- Add Thinking field to Message struct for reasoning passback
- Add GOCLAW_OPENAI_BASE_URL env var override (parity with Anthropic)
- Add sendMessageDraft transport (disabled pending Telegram client fix for
"reply to deleted message" artifact — tdesktop#10315, bugs.telegram.org/c/561)
- Split stream_mode into dm_stream/group_stream boolean flags (both default false)
- DM messages no longer set reply_to_message_id (cleaner UX, matching TS)
- Progressive placeholder editing for DMs: "Thinking..." → stream chunks → final
- Update web UI with separate DM/Group streaming toggles
fix(agent): prevent false MEDIA: detection in tool output
parseMediaResult() used strings.Index to find "MEDIA:" anywhere in tool output,
causing false positives when external content (e.g. GitHub releases page)
contained commit messages like "return MEDIA: path from screenshot".
Changed to strings.HasPrefix to only match at start of output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix resolvePath for nested non-existent dirs (use resolveThroughExistingAncestors)
- Channel-isolated workspace: user_agent_profiles.workspace stores channel prefix,
used as source of truth with backward compat for existing users
- Loop caches workspace per-user with CacheKindUserWorkspace invalidation via pubsub
- ContractHome/ExpandHome for portable ~-based paths in DB
- create_image saves to workspace/generated/YYYY-MM-DD/ instead of OS temp dir
- SOUL.md template: add ## Expertise section for domain knowledge
- Summoner buildEditPrompt: section guide, complete file output, frontmatter update
- Bus: Topic* constants for Subscribe/Broadcast keys, CacheKind* for payload kinds
- Teams, delegates, sessions, agent links: various enhancements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add DashScope (Qwen) native provider with tools+streaming fallback
- Add Bailian Coding provider with hardcoded model list (no /v1/models API)
- Parse reasoning_content in OpenAI-compat streaming/non-streaming responses
- Emit ChatEventThinking events in agent loop for thinking models
- Add vision support for DashScope (qwen3-vl)
- Fix provider form dialog not updating API base URL when switching types
- Update README provider count from 11+ to 13+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow overriding the Anthropic API base URL via GOCLAW_ANTHROPIC_BASE_URL
env var, config JSON, or DB provider record. Enables use of Anthropic-
compatible proxies and custom endpoints.
Also adds Makefile shortcuts for docker compose (up/down/logs).
- Fix seedManagedData() hardcoding openai_compat for all non-Anthropic providers;
now uses resolveProviderType() mapping to correct store constants (gemini_native,
minimax_native, etc.) — fixes model listing in web UI for Gemini
- Rename GOCLAW_FEISHU_* env vars to GOCLAW_LARK_* (Lark is the global brand)
- Add WhatsApp env override (GOCLAW_WHATSAPP_BRIDGE_URL) and auto-enable
- Add missing env vars to docker-compose.yml (Cohere, Perplexity, Lark, Zalo, WhatsApp)
- Update .env.example with all providers/channels, remove unnecessary GOCLAW_PROVIDER
- Add Cohere and Perplexity to prepare-env.sh provider detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add env var overlay and auto-enable for Discord channel, matching the
existing Telegram pattern. Update .env.example and docker-compose.yml.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Check Docker availability before enabling sandbox and gracefully disable with warning if unavailable. Add resolveAgentUUID helper in Telegram to support both agent keys and direct UUIDs. Expose sandbox config via environment variables for docker-compose overlay. Add compact redirect cards in config UI for Providers and Channels sections when in managed mode, directing users to dedicated management pages.
Register edit tool (sandboxed + non-sandboxed variants) with context file and memory interceptors in managed mode. Add cron tool for agent-facing job management. Register session tools (list, status, history, send) and message tool with proper SessionStoreAware, BusAware, and ChannelSenderAware wiring. Harden exec approval system to always be active with deny patterns + safe bins, defaulting to full