Compare commits

..
Author SHA1 Message Date
Peter Steinberger d7f5818443 fix: tighten model fallback + docs (#1762) (thanks @senoldogann) 2026-01-25 12:54:32 +00:00
senoldogann f552055cef style: fix formatting and improve model selection logic with tests 2026-01-25 12:50:29 +00:00
senoldogann cdd82b4fa4 style: fix formatting in model-selection.ts 2026-01-25 12:50:29 +00:00
senoldogann f3991055fa feat: audit fixes and documentation improvements
- Refactored model selection to drop legacy fallback and add warning
- Improved heartbeat content validation
- Added Skill Creation guide
- Updated CONTRIBUTING.md with roadmap
2026-01-25 12:50:29 +00:00
20 changed files with 86 additions and 269 deletions

No files matched your search

+29 -33
View File
@@ -5,64 +5,60 @@ Docs: https://docs.clawd.bot
## 2026.1.24
### Highlights
- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.clawd.bot/providers/ollama https://docs.clawd.bot/providers/venice
- Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.
- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.clawd.bot/tts
- Exec approvals: approve in-chat via `/approve` across all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.clawd.bot/channels/telegram
- Ollama: provider discovery + docs. (#1606) Thanks @abhaymundhara. https://docs.clawd.bot/providers/ollama
- Venius (Venice AI): highlight provider guide + cross-links + expanded guidance. https://docs.clawd.bot/providers/venice
### Changes
- Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts
- Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal.
- Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.clawd.bot/channels/telegram
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.clawd.bot/tools/web
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts
- Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
- UI: refresh Control UI dashboard design system (typography, colors, spacing). (#1786) Thanks @mousberg.
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.clawd.bot/diagnostics/flags
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
- Docs: add verbose installer troubleshooting guidance.
- Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.
- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.clawd.bot/bedrock
- Docs: update Fly.io guide notes.
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
- Docs: add Bedrock EC2 instance role setup + IAM steps. (#1625) Thanks @sergical. https://docs.clawd.bot/bedrock
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/slash-commands
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
- Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.clawd.bot/channels/telegram
- Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal.
- Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.clawd.bot/diagnostics/flags
### Fixes
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Web UI: hide internal `message_id` hints in chat bubbles.
- Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (`gateway.controlUi.allowInsecureAuth`). (#1679) Thanks @steipete.
- Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
- Web UI: hide internal `message_id` hints in chat bubbles.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Heartbeat: normalize target identifiers for consistent routing.
- TUI: reload history after gateway reconnect to restore session state. (#1663)
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
- Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt.
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.clawd.bot/channels/signal
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
- Telegram: honor per-account proxy for outbound API calls. (#1774) Thanks @radek-paclt.
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
- Voice Call: serialize Twilio TTS playback and cancel on barge-in to prevent overlap. (#1713) Thanks @dguido.
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
- Google Chat: normalize space targets without double `spaces/` prefix.
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
- Agents: use the active auth profile for auto-compaction recovery.
- Agents: let cron isolated runs inherit subagent allowlists from the parent agent. (#1771) Thanks @Noctivoro.
- Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
- Models: default missing custom provider fields so minimal configs are accepted.
- Messaging: keep newline chunking safe for fenced markdown blocks across channels.
- TUI: reload history after gateway reconnect to restore session state. (#1663)
- Heartbeat: normalize target identifiers for consistent routing.
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
- Exec: treat Windows platform labels as Windows for node shell selection. (#1760) Thanks @ymat19.
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
- Models/Heartbeat: normalize provider-less default model fallback and skip empty checklist items. (#1762) Thanks @senoldogann.
- Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
- Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
- Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690)
- Gateway: listen on IPv6 loopback when bound to 127.0.0.1 so localhost webhooks work.
- Gateway: store lock files in the temp directory to avoid stale locks on persistent volumes. (#1676)
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
- Voice Call: serialize Twilio TTS playback and cancel on barge-in to prevent overlap. (#1713) Thanks @dguido.
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
- Google Chat: normalize space targets without double `spaces/` prefix.
- Messaging: keep newline chunking safe for fenced markdown blocks across channels.
- Tests: cap Vitest workers on CI macOS to reduce timeouts. (#1597) Thanks @rohannagpal.
- Tests: avoid fake-timer dependency in embedded runner stream mock to reduce CI flakes. (#1597) Thanks @rohannagpal.
- Tests: increase embedded runner ordering test timeout to reduce CI flakes. (#1597) Thanks @rohannagpal.
+1 -1
View File
@@ -46,7 +46,7 @@ AI PRs are first-class citizens here. We just want transparency so reviewers kno
We are currently prioritizing:
- **Stability**: Fixing edge cases in channel connections (WhatsApp/Telegram).
- **UX**: Improving the onboarding wizard and error messages.
- **Skills**: Expanding the library of bundled skills and improving the Skill Creation developer experience.
- **Skills**: ClawdHub-first (no new core/bundled skills).
- **Performance**: Optimizing token usage and compaction logic.
Check the [GitHub Issues](https://github.com/clawdbot/clawdbot/issues) for "good first issue" labels!
+15 -15
View File
@@ -26,7 +26,7 @@ Minimal config:
```
### Setup
1) Create a Slack app (From scratch) in https://api.slack.com/apps.
1) Create a Slack app (From scratch) in https://api.channels.slack.com/apps.
2) **Socket Mode** → toggle on. Then go to **Basic Information** → **App-Level Tokens** → **Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
3) **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
4) Optional: **OAuth & Permissions** → add **User Token Scopes** (see the read-only list below). Reinstall the app and copy the **User OAuth Token** (`xoxp-...`).
@@ -245,29 +245,29 @@ If you enable native commands, add one `slash_commands` entry per command you wa
## Scopes (current vs optional)
Slack's Conversations API is type-scoped: you only need the scopes for the
conversation types you actually touch (channels, groups, im, mpim). See
https://docs.slack.dev/apis/web-api/using-the-conversations-api/ for the overview.
https://api.channels.slack.com/docs/conversations-api for the overview.
### Bot token scopes (required)
- `chat:write` (send/update/delete messages via `chat.postMessage`)
https://docs.slack.dev/reference/methods/chat.postMessage
https://api.channels.slack.com/methods/chat.postMessage
- `im:write` (open DMs via `conversations.open` for user DMs)
https://docs.slack.dev/reference/methods/conversations.open
https://api.channels.slack.com/methods/conversations.open
- `channels:history`, `groups:history`, `im:history`, `mpim:history`
https://docs.slack.dev/reference/methods/conversations.history
https://api.channels.slack.com/methods/conversations.history
- `channels:read`, `groups:read`, `im:read`, `mpim:read`
https://docs.slack.dev/reference/methods/conversations.info
https://api.channels.slack.com/methods/conversations.info
- `users:read` (user lookup)
https://docs.slack.dev/reference/methods/users.info
https://api.channels.slack.com/methods/users.info
- `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`)
https://docs.slack.dev/reference/methods/reactions.get
https://docs.slack.dev/reference/methods/reactions.add
https://api.channels.slack.com/methods/reactions.get
https://api.channels.slack.com/methods/reactions.add
- `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`)
https://docs.slack.dev/reference/scopes/pins.read
https://docs.slack.dev/reference/scopes/pins.write
https://api.channels.slack.com/scopes/pins:read
https://api.channels.slack.com/scopes/pins:write
- `emoji:read` (`emoji.list`)
https://docs.slack.dev/reference/scopes/emoji.read
https://api.channels.slack.com/scopes/emoji:read
- `files:write` (uploads via `files.uploadV2`)
https://docs.slack.dev/messaging/working-with-files/#upload
https://api.channels.slack.com/messaging/files/uploading
### User token scopes (optional, read-only by default)
Add these under **User Token Scopes** if you configure `channels.slack.userToken`.
@@ -284,9 +284,9 @@ Add these under **User Token Scopes** if you configure `channels.slack.userToken
- `mpim:write` (only if we add group-DM open/DM start via `conversations.open`)
- `groups:write` (only if we add private-channel management: create/rename/invite/archive)
- `chat:write.public` (only if we want to post to channels the bot isn't in)
https://docs.slack.dev/reference/scopes/chat.write.public
https://api.channels.slack.com/scopes/chat:write.public
- `users:read.email` (only if we need email fields from `users.info`)
https://docs.slack.dev/changelog/2017-04-narrowing-email-access
https://api.channels.slack.com/changelog/2017-04-narrowing-email-access
- `files:read` (only if we start listing/reading file metadata)
## Config
+5 -5
View File
@@ -8,7 +8,7 @@ A skill is a directory containing a `SKILL.md` file (which provides instructions
## Step-by-Step: Your First Skill
### 1. Create the Directory
Skills live in your workspace, usually `~/clawd/skills/`. Create a new folder for your skill:
Skills live in your workspace at `<workspace>/skills` (default: `~/clawd/skills`). Create a new folder for your skill:
```bash
mkdir -p ~/clawd/skills/hello-world
```
@@ -23,18 +23,18 @@ description: A simple skill that says hello.
---
# Hello World Skill
When the user asks for a greeting, use the `echo` tool to say "Hello from your custom skill!".
When the user asks for a greeting, use the `exec` tool to run `echo "Hello from your custom skill!"`.
```
### 3. Add Tools (Optional)
You can define custom tools in the frontmatter or instruct the agent to use existing system tools (like `bash` or `browser`).
You can define custom tools in the frontmatter or instruct the agent to use existing system tools (like `exec` or `web`).
### 4. Refresh Clawdbot
Ask your agent to "refresh skills" or restart the gateway. Clawdbot will discover the new directory and index the `SKILL.md`.
If the skills watcher is enabled (default), changes are picked up automatically. Otherwise restart the gateway or start a new session to refresh the skills snapshot.
## Best Practices
- **Be Concise**: Instruct the model on *what* to do, not how to be an AI.
- **Safety First**: If your skill uses `bash`, ensure the prompts don't allow arbitrary command injection from untrusted user input.
- **Safety First**: If your skill uses `exec`, ensure the prompts don't allow arbitrary command injection from untrusted user input.
- **Test Locally**: Use `clawdbot agent --message "use my new skill"` to test.
## Shared Skills
@@ -329,20 +329,16 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
return;
}
const contentInfo =
"info" in content && content.info && typeof content.info === "object"
? (content.info as { mimetype?: string; size?: number })
const contentType =
"info" in content && content.info && "mimetype" in content.info
? (content.info as { mimetype?: string }).mimetype
: undefined;
const contentType = contentInfo?.mimetype;
const contentSize =
typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
if (mediaUrl?.startsWith("mxc://")) {
try {
media = await downloadMatrixMedia({
client,
mxcUrl: mediaUrl,
contentType,
sizeBytes: contentSize,
maxBytes: mediaMaxBytes,
file: contentFile,
});
@@ -25,8 +25,10 @@ describe("downloadMatrixMedia", () => {
it("decrypts encrypted media when file payloads are present", async () => {
const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
const downloadContent = vi.fn().mockResolvedValue(Buffer.from("encrypted"));
const client = {
downloadContent,
crypto: { decryptMedia },
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
} as unknown as import("matrix-bot-sdk").MatrixClient;
@@ -53,8 +55,7 @@ describe("downloadMatrixMedia", () => {
file,
});
// decryptMedia should be called with just the file object (it handles download internally)
expect(decryptMedia).toHaveBeenCalledWith(file);
expect(decryptMedia).toHaveBeenCalled();
expect(saveMediaBuffer).toHaveBeenCalledWith(
Buffer.from("decrypted"),
"image/png",
@@ -63,41 +64,4 @@ describe("downloadMatrixMedia", () => {
);
expect(result?.path).toBe("/tmp/media");
});
it("rejects encrypted media that exceeds maxBytes before decrypting", async () => {
const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
const client = {
crypto: { decryptMedia },
mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
} as unknown as import("matrix-bot-sdk").MatrixClient;
const file = {
url: "mxc://example/file",
key: {
kty: "oct",
key_ops: ["encrypt", "decrypt"],
alg: "A256CTR",
k: "secret",
ext: true,
},
iv: "iv",
hashes: { sha256: "hash" },
v: "v2",
};
await expect(
downloadMatrixMedia({
client,
mxcUrl: "mxc://example/file",
contentType: "image/png",
sizeBytes: 2048,
maxBytes: 1024,
file,
}),
).rejects.toThrow("Matrix media exceeds configured size limit");
expect(decryptMedia).not.toHaveBeenCalled();
expect(saveMediaBuffer).not.toHaveBeenCalled();
});
});
+9 -12
View File
@@ -40,7 +40,6 @@ async function fetchMatrixMediaBuffer(params: {
/**
* Download and decrypt encrypted media from a Matrix room.
* Uses matrix-bot-sdk's decryptMedia which handles both download and decryption.
*/
async function fetchEncryptedMediaBuffer(params: {
client: MatrixClient;
@@ -51,13 +50,18 @@ async function fetchEncryptedMediaBuffer(params: {
throw new Error("Cannot decrypt media: crypto not enabled");
}
// decryptMedia handles downloading and decrypting the encrypted content internally
const decrypted = await params.client.crypto.decryptMedia(params.file);
if (decrypted.byteLength > params.maxBytes) {
// Download the encrypted content
const encryptedBuffer = await params.client.downloadContent(params.file.url);
if (encryptedBuffer.byteLength > params.maxBytes) {
throw new Error("Matrix media exceeds configured size limit");
}
// Decrypt using matrix-bot-sdk crypto
const decrypted = await params.client.crypto.decryptMedia(
Buffer.from(encryptedBuffer),
params.file,
);
return { buffer: decrypted };
}
@@ -65,7 +69,6 @@ export async function downloadMatrixMedia(params: {
client: MatrixClient;
mxcUrl: string;
contentType?: string;
sizeBytes?: number;
maxBytes: number;
file?: EncryptedFile;
}): Promise<{
@@ -74,12 +77,6 @@ export async function downloadMatrixMedia(params: {
placeholder: string;
} | null> {
let fetched: { buffer: Buffer; headerType?: string } | null;
if (
typeof params.sizeBytes === "number" &&
params.sizeBytes > params.maxBytes
) {
throw new Error("Matrix media exceeds configured size limit");
}
if (params.file) {
// Encrypted media
@@ -29,7 +29,6 @@ export type RoomMessageEventContent = MessageEventContent & {
file?: EncryptedFile;
info?: {
mimetype?: string;
size?: number;
};
"m.relates_to"?: {
rel_type?: string;
-40
View File
@@ -157,44 +157,4 @@ describe("agents_list", () => {
const research = agents?.find((agent) => agent.id === "research");
expect(research?.configured).toBe(false);
});
it("uses requesterAgentIdOverride when resolving allowlists", async () => {
configOverride = {
session: {
mainKey: "main",
scope: "per-sender",
},
agents: {
list: [
{
id: "cron-owner",
subagents: {
allowAgents: ["research"],
},
},
{
id: "research",
name: "Research",
},
],
},
};
const tool = createClawdbotTools({
agentSessionKey: "cron:job-1",
requesterAgentIdOverride: "cron-owner",
}).find((candidate) => candidate.name === "agents_list");
if (!tool) throw new Error("missing agents_list tool");
const result = await tool.execute("call5", {});
const agents = (
result.details as {
agents?: Array<{ id: string }>;
}
).agents;
expect(agents?.map((agent) => agent.id)).toEqual(["cron-owner", "research"]);
expect(result.details).toMatchObject({
requester: "cron-owner",
});
});
});
@@ -87,52 +87,6 @@ describe("clawdbot-tools: subagents", () => {
});
expect(childSessionKey?.startsWith("agent:research:subagent:")).toBe(true);
});
it("sessions_spawn honors requesterAgentIdOverride for cron sessions", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
configOverride = {
session: {
mainKey: "main",
scope: "per-sender",
},
agents: {
list: [
{
id: "cron-owner",
subagents: {
allowAgents: ["research"],
},
},
],
},
};
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown };
if (request.method === "agent") {
return { runId: "run-2", status: "accepted", acceptedAt: 5200 };
}
return {};
});
const tool = createClawdbotTools({
agentSessionKey: "cron:job-1",
requesterAgentIdOverride: "cron-owner",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
const result = await tool.execute("call11", {
task: "do thing",
agentId: "research",
});
expect(result.details).toMatchObject({
status: "accepted",
runId: "run-2",
});
});
it("sessions_spawn forbids cross-agent spawning when not allowed", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
+5 -13
View File
@@ -54,8 +54,6 @@ export function createClawdbotTools(options?: {
hasRepliedRef?: { value: boolean };
/** If true, the model has native vision capability */
modelHasVision?: boolean;
/** Explicit agent ID override for cron/hook sessions. */
requesterAgentIdOverride?: string;
}): AnyAgentTool[] {
const imageTool = options?.agentDir?.trim()
? createImageTool({
@@ -107,10 +105,7 @@ export function createClawdbotTools(options?: {
agentSessionKey: options?.agentSessionKey,
config: options?.config,
}),
createAgentsListTool({
agentSessionKey: options?.agentSessionKey,
requesterAgentIdOverride: options?.requesterAgentIdOverride,
}),
createAgentsListTool({ agentSessionKey: options?.agentSessionKey }),
createSessionsListTool({
agentSessionKey: options?.agentSessionKey,
sandboxed: options?.sandboxed,
@@ -134,7 +129,6 @@ export function createClawdbotTools(options?: {
agentGroupChannel: options?.agentGroupChannel,
agentGroupSpace: options?.agentGroupSpace,
sandboxed: options?.sandboxed,
requesterAgentIdOverride: options?.requesterAgentIdOverride,
}),
createSessionStatusTool({
agentSessionKey: options?.agentSessionKey,
@@ -150,12 +144,10 @@ export function createClawdbotTools(options?: {
config: options?.config,
workspaceDir: options?.workspaceDir,
agentDir: options?.agentDir,
agentId:
options?.requesterAgentIdOverride ??
resolveSessionAgentId({
sessionKey: options?.agentSessionKey,
config: options?.config,
}),
agentId: resolveSessionAgentId({
sessionKey: options?.agentSessionKey,
config: options?.config,
}),
sessionKey: options?.agentSessionKey,
messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
+3 -2
View File
@@ -137,10 +137,11 @@ export function resolveConfiguredModelRef(params: {
if (aliasMatch) return aliasMatch.ref;
// Default to anthropic if no provider is specified, but warn as this is deprecated.
const normalized = normalizeProviderModelId("anthropic", trimmed);
console.warn(
`[clawdbot] Model "${trimmed}" specified without provider. Falling back to "anthropic/${trimmed}". Please use "anthropic/${trimmed}" in your config.`,
`[clawdbot] Model "${trimmed}" specified without provider. Falling back to "anthropic/${normalized}". Please use "anthropic/${normalized}" in your config.`,
);
return { provider: "anthropic", model: trimmed };
return { provider: "anthropic", model: normalized };
}
const resolved = resolveModelRefFromString({
-1
View File
@@ -293,7 +293,6 @@ export function createClawdbotCodingTools(options?: {
agentGroupChannel: options?.groupChannel ?? null,
agentGroupSpace: options?.groupSpace ?? null,
agentDir: options?.agentDir,
requesterAgentIdOverride: agentId,
sandboxRoot,
workspaceDir: options?.workspaceDir,
sandboxed: !!sandbox,
+2 -8
View File
@@ -19,11 +19,7 @@ type AgentListEntry = {
configured: boolean;
};
export function createAgentsListTool(opts?: {
agentSessionKey?: string;
/** Explicit agent ID override for cron/hook sessions. */
requesterAgentIdOverride?: string;
}): AnyAgentTool {
export function createAgentsListTool(opts?: { agentSessionKey?: string }): AnyAgentTool {
return {
label: "Agents",
name: "agents_list",
@@ -41,9 +37,7 @@ export function createAgentsListTool(opts?: {
})
: alias;
const requesterAgentId = normalizeAgentId(
opts?.requesterAgentIdOverride ??
parseAgentSessionKey(requesterInternalKey)?.agentId ??
DEFAULT_AGENT_ID,
parseAgentSessionKey(requesterInternalKey)?.agentId ?? DEFAULT_AGENT_ID,
);
const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];
+1 -3
View File
@@ -67,8 +67,6 @@ export function createSessionsSpawnTool(opts?: {
agentGroupChannel?: string | null;
agentGroupSpace?: string | null;
sandboxed?: boolean;
/** Explicit agent ID override for cron/hook sessions where session key parsing may not work. */
requesterAgentIdOverride?: string;
}): AnyAgentTool {
return {
label: "Sessions",
@@ -131,7 +129,7 @@ export function createSessionsSpawnTool(opts?: {
});
const requesterAgentId = normalizeAgentId(
opts?.requesterAgentIdOverride ?? parseAgentSessionKey(requesterInternalKey)?.agentId,
parseAgentSessionKey(requesterInternalKey)?.agentId,
);
const targetAgentId = requestedAgentId
? normalizeAgentId(requestedAgentId)
+9
View File
@@ -133,6 +133,15 @@ describe("isHeartbeatContentEffectivelyEmpty", () => {
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n\n")).toBe(true);
});
it("returns true for empty list items", () => {
expect(isHeartbeatContentEffectivelyEmpty("- ")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("- [ ]")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("- [x]")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("* [ ]")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("+ [ ]")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n- [ ]")).toBe(true);
});
it("returns true for comments only", () => {
expect(isHeartbeatContentEffectivelyEmpty("# Header\n# Another comment")).toBe(true);
expect(isHeartbeatContentEffectivelyEmpty("## Subheader\n### Another")).toBe(true);
@@ -170,7 +170,6 @@ export async function handleInlineActions(params: {
agentAccountId: (ctx as { AccountId?: string }).AccountId,
agentTo: ctx.OriginatingTo ?? ctx.To,
agentThreadId: ctx.MessageThreadId ?? undefined,
requesterAgentIdOverride: agentId,
agentDir,
workspaceDir,
config: cfg,
-1
View File
@@ -148,7 +148,6 @@ export async function handleToolsInvokeHttpRequest(
agentSessionKey: sessionKey,
agentChannel: messageChannel ?? undefined,
agentAccountId: accountId,
requesterAgentIdOverride: agentId,
config: cfg,
pluginToolAllowlist: collectExplicitAllowlist([
profilePolicy,
-40
View File
@@ -1,40 +0,0 @@
import { describe, expect, it } from "vitest";
import { buildNodeShellCommand } from "./node-shell.js";
describe("buildNodeShellCommand", () => {
it("uses cmd.exe for win32", () => {
expect(buildNodeShellCommand("echo hi", "win32")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses cmd.exe for windows labels", () => {
expect(buildNodeShellCommand("echo hi", "windows")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
expect(buildNodeShellCommand("echo hi", "Windows 11")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses /bin/sh for darwin", () => {
expect(buildNodeShellCommand("echo hi", "darwin")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
it("uses /bin/sh when platform missing", () => {
expect(buildNodeShellCommand("echo hi")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ export function buildNodeShellCommand(command: string, platform?: string | null)
const normalized = String(platform ?? "")
.trim()
.toLowerCase();
if (normalized.startsWith("win")) {
if (normalized.includes("win")) {
return ["cmd.exe", "/d", "/s", "/c", command];
}
return ["/bin/sh", "-lc", command];