* feat(discord): add exec approval forwarding to DMs
Add support for forwarding exec approval requests to Discord DMs,
allowing users to approve/deny command execution via interactive buttons.
Features:
- New DiscordExecApprovalHandler that connects to gateway and listens
for exec.approval.requested/resolved events
- Sends DMs with embeds showing command details and 3 buttons:
Allow once, Always allow, Deny
- Configurable via channels.discord.execApprovals with:
- enabled: boolean
- approvers: Discord user IDs to notify
- agentFilter: only forward for specific agents
- sessionFilter: only forward for matching session patterns
- Updates message embed when approval is resolved or expires
Also fixes exec completion routing: when async exec completes after
approval, the heartbeat now uses a specialized prompt to ensure the
model relays the result to the user instead of responding HEARTBEAT_OK.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: generic exec approvals forwarding (#1621) (thanks @czekaj)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: reduce log noise for node disconnect/late invoke errors
- Handle both 'node not connected' and 'node disconnected' errors at info level
- Return success with late:true for unknown invoke IDs instead of error
- Add 30-second throttle to skills change listener to prevent rapid-fire probes
- Add tests for isNodeUnavailableError and late invoke handling
* fix: clean up skills refresh timer and listener on shutdown
Store the return value from registerSkillsChangeListener() and call it
on gateway shutdown. Also clear any pending refresh timer. This follows
the same pattern used for agentUnsub and heartbeatUnsub.
* refactor: simplify KISS/YAGNI - inline checks, remove unit tests for internal utilities
* fix: reduce gateway log noise (#1607) (thanks @petter-b)
* test: align agent id casing expectations (#1607)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
In containers, PIDs can be recycled quickly after restarts. When a container
restarts, a different process might get the same PID as the previous gateway,
causing the lock check to incorrectly think the old gateway is still running.
This fix adds isGatewayProcess() which verifies on Linux that the PID actually
belongs to a clawdbot gateway by checking /proc/PID/cmdline. If the cmdline
doesn't contain 'clawdbot' or 'gateway', we assume the lock is stale.
Fixes gateway boot-loop in Docker/Fly.io deployments.
* feat: skip heartbeat API calls when HEARTBEAT.md is effectively empty
- Added isHeartbeatContentEffectivelyEmpty() to detect files with only headers/comments
- Modified runHeartbeatOnce() to check HEARTBEAT.md content before polling the LLM
- Returns early with 'empty-heartbeat-file' reason when no actionable tasks exist
- Preserves existing behavior when file is missing (lets LLM decide)
- Added comprehensive test coverage for empty file detection
- Saves API calls/costs when heartbeat file has no meaningful content
* chore: update HEARTBEAT.md template to be effectively empty by default
Changed instruction text to comment format so new workspaces benefit from
heartbeat optimization immediately. Users still get clear guidance on usage.
* fix: only treat markdown headers (# followed by space) as comments, not #TODO etc
* refactor: simplify regex per code review suggestion
* docs: clarify heartbeat empty file behavior (#1535) (thanks @JustYannicc)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now attempt the command directly first. If it fails, we catch the error and retry
with `sudo -n`. This preserves existing behavior for users where it works, but
attempts to escalate privileges (non-interactively) if needed.
- Added `execWithSudoFallback` helper in `src/infra/tailscale.ts`.
- Updated `ensureFunnel`, `enableTailscaleServe`, `disableTailscaleServe`,
`enableTailscaleFunnel`, and `disableTailscaleFunnel` to use the fallback helper.
- Added tests in `src/infra/tailscale.test.ts` to verify fallback behavior.
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now prepend `sudo -n` to these commands. This ensures that if the user has appropriate
sudo privileges (specifically passwordless for these commands or generally), the operation
succeeds. If sudo fails (e.g. requires password non-interactively), it will throw an error
which is caught and logged as a warning, preserving existing behavior but attempting to escalate privileges first.
- Updated `ensureFunnel` to use `sudo -n` for the enabling step.
- Updated `enableTailscaleServe`, `disableTailscaleServe`, `enableTailscaleFunnel`, `disableTailscaleFunnel` to use `sudo -n`.
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now prepend `sudo -n` to these commands. This ensures that if the user has appropriate
sudo privileges (specifically passwordless for these commands or generally), the operation
succeeds. If sudo fails (e.g. requires password non-interactively), it will throw an error
which is caught and logged as a warning, preserving existing behavior but attempting to escalate privileges first.
- Updated `ensureFunnel` to use `sudo -n` for the enabling step.
- Updated `enableTailscaleServe`, `disableTailscaleServe`, `enableTailscaleFunnel`, `disableTailscaleFunnel` to use `sudo -n`.
- Added tests in `src/infra/tailscale.test.ts` to verify `sudo` usage.
Move mattermost channel implementation from core to extensions/mattermost plugin. Extract config schema, group mentions, normalize utilities, and all mattermost-specific logic (accounts, client, monitor, probe, send) into the extension. Update imports to use plugin SDK and local modules. Add channel metadata directly in plugin definition instead of using getChatChannelMeta. Update package.json with channel and install configuration.
The message tool accepts path and filePath parameters in its schema,
but these were never converted to mediaUrl, causing local files to
be ignored when sending messages.
Changes:
- src/agents/tools/message-tool.ts: Convert path/filePath to media with file:// URL
- src/infra/outbound/message-action-runner.ts: Allow hydrateSendAttachmentParams for "send" action
Fixes issue where local audio files (and other media) couldn't be sent
via the message tool with the path parameter.
Users can now use:
message({ path: "/tmp/file.ogg" })
message({ filePath: "/tmp/file.ogg" })