feat: wire multi-agent config and routing
Co-authored-by: Mark Pors <1078320+pors@users.noreply.github.com>main
parent
81beda0772
commit
7b81d97ec2
|
|
@ -15,6 +15,7 @@
|
||||||
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj
|
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj
|
||||||
- Security: per-agent mention patterns and group elevated directives now require explicit mention to avoid cross-agent toggles.
|
- Security: per-agent mention patterns and group elevated directives now require explicit mention to avoid cross-agent toggles.
|
||||||
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
||||||
|
- Config: migrate routing/agent config into agents.list/agents.defaults and messages/tools/audio with default agent selection and per-agent identity config.
|
||||||
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
||||||
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
||||||
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
||||||
|
|
@ -109,8 +110,8 @@
|
||||||
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
||||||
- To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`).
|
- To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`).
|
||||||
- Approve requests via `clawdbot pairing list --provider <provider>` + `clawdbot pairing approve --provider <provider> <code>`.
|
- Approve requests via `clawdbot pairing list --provider <provider>` + `clawdbot pairing approve --provider <provider> <code>`.
|
||||||
- Sandbox: default `agent.sandbox.scope` to `"agent"` (one container/workspace per agent). Use `"session"` for per-session isolation; `"shared"` disables cross-session isolation.
|
- Sandbox: default `agents.defaults.sandbox.scope` to `"agent"` (one container/workspace per agent). Use `"session"` for per-session isolation; `"shared"` disables cross-session isolation.
|
||||||
- Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agent.userTimezone` to tell the model the user’s local time (system prompt only).
|
- Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agents.defaults.userTimezone` to tell the model the user’s local time (system prompt only).
|
||||||
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
|
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
|
||||||
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
|
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
|
||||||
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
||||||
|
|
@ -136,7 +137,7 @@
|
||||||
## 2026.1.5
|
## 2026.1.5
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
|
- Models: add image-specific model config (`agents.defaults.imageModel` + fallbacks) and scan support.
|
||||||
- Agent tools: new `image` tool routed to the image model (when configured).
|
- Agent tools: new `image` tool routed to the image model (when configured).
|
||||||
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
|
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
|
||||||
- Docs: document built-in model shorthands + precedence (user config wins).
|
- Docs: document built-in model shorthands + precedence (user config wins).
|
||||||
|
|
@ -161,7 +162,7 @@
|
||||||
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
|
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
|
||||||
- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`).
|
- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`).
|
||||||
- Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).
|
- Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).
|
||||||
- Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off.
|
- Agent tools: honor `tools.allow` / `tools.deny` policy even when sandbox is off.
|
||||||
- Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events.
|
- Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events.
|
||||||
- Commands: unify /status (inline) and command auth across providers; group bypass for authorized control commands; remove Discord /clawd slash handler.
|
- Commands: unify /status (inline) and command auth across providers; group bypass for authorized control commands; remove Discord /clawd slash handler.
|
||||||
- CLI: run `clawdbot agent` via the Gateway by default; use `--local` to force embedded mode.
|
- CLI: run `clawdbot agent` via the Gateway by default; use `--local` to force embedded mode.
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ Runbook: [iOS connect](https://docs.clawd.bot/ios).
|
||||||
|
|
||||||
## Agent workspace + skills
|
## Agent workspace + skills
|
||||||
|
|
||||||
- Workspace root: `~/clawd` (configurable via `agent.workspace`).
|
- Workspace root: `~/clawd` (configurable via `agents.defaults.workspace`).
|
||||||
- Injected prompt files: `AGENTS.md`, `SOUL.md`, `TOOLS.md`.
|
- Injected prompt files: `AGENTS.md`, `SOUL.md`, `TOOLS.md`.
|
||||||
- Skills: `~/clawd/skills/<skill>/SKILL.md`.
|
- Skills: `~/clawd/skills/<skill>/SKILL.md`.
|
||||||
|
|
||||||
|
|
@ -305,7 +305,7 @@ Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
|
||||||
## Security model (important)
|
## Security model (important)
|
||||||
|
|
||||||
- **Default:** tools run on the host for the **main** session, so the agent has full access when it’s just you.
|
- **Default:** tools run on the host for the **main** session, so the agent has full access when it’s just you.
|
||||||
- **Group/channel safety:** set `agent.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions.
|
- **Group/channel safety:** set `agents.defaults.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions.
|
||||||
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
|
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
|
||||||
|
|
||||||
Details: [Security guide](https://docs.clawd.bot/security) · [Docker + sandboxing](https://docs.clawd.bot/docker) · [Sandbox config](https://docs.clawd.bot/configuration)
|
Details: [Security guide](https://docs.clawd.bot/security) · [Docker + sandboxing](https://docs.clawd.bot/docker) · [Sandbox config](https://docs.clawd.bot/configuration)
|
||||||
|
|
|
||||||
|
|
@ -81,22 +81,33 @@ enum ClawdbotConfigFile {
|
||||||
|
|
||||||
static func agentWorkspace() -> String? {
|
static func agentWorkspace() -> String? {
|
||||||
let root = self.loadDict()
|
let root = self.loadDict()
|
||||||
let agent = root["agent"] as? [String: Any]
|
let agents = root["agents"] as? [String: Any]
|
||||||
return agent?["workspace"] as? String
|
let defaults = agents?["defaults"] as? [String: Any]
|
||||||
|
return defaults?["workspace"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
static func setAgentWorkspace(_ workspace: String?) {
|
static func setAgentWorkspace(_ workspace: String?) {
|
||||||
var root = self.loadDict()
|
var root = self.loadDict()
|
||||||
var agent = root["agent"] as? [String: Any] ?? [:]
|
var agents = root["agents"] as? [String: Any] ?? [:]
|
||||||
|
var defaults = agents["defaults"] as? [String: Any] ?? [:]
|
||||||
let trimmed = workspace?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let trimmed = workspace?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if trimmed.isEmpty {
|
if trimmed.isEmpty {
|
||||||
agent.removeValue(forKey: "workspace")
|
defaults.removeValue(forKey: "workspace")
|
||||||
} else {
|
} else {
|
||||||
agent["workspace"] = trimmed
|
defaults["workspace"] = trimmed
|
||||||
|
}
|
||||||
|
if defaults.isEmpty {
|
||||||
|
agents.removeValue(forKey: "defaults")
|
||||||
|
} else {
|
||||||
|
agents["defaults"] = defaults
|
||||||
|
}
|
||||||
|
if agents.isEmpty {
|
||||||
|
root.removeValue(forKey: "agents")
|
||||||
|
} else {
|
||||||
|
root["agents"] = agents
|
||||||
}
|
}
|
||||||
root["agent"] = agent
|
|
||||||
self.saveDict(root)
|
self.saveDict(root)
|
||||||
self.logger.debug("agent workspace updated set=\(!trimmed.isEmpty)")
|
self.logger.debug("agents.defaults.workspace updated set=\(!trimmed.isEmpty)")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func gatewayPassword() -> String? {
|
static func gatewayPassword() -> String? {
|
||||||
|
|
|
||||||
|
|
@ -387,13 +387,20 @@ struct ConfigSettings: View {
|
||||||
|
|
||||||
private func loadConfig() async {
|
private func loadConfig() async {
|
||||||
let parsed = await ConfigStore.load()
|
let parsed = await ConfigStore.load()
|
||||||
let agent = parsed["agent"] as? [String: Any]
|
let agents = parsed["agents"] as? [String: Any]
|
||||||
let heartbeatMinutes = agent?["heartbeatMinutes"] as? Int
|
let defaults = agents?["defaults"] as? [String: Any]
|
||||||
let heartbeatBody = agent?["heartbeatBody"] as? String
|
let heartbeat = defaults?["heartbeat"] as? [String: Any]
|
||||||
|
let heartbeatEvery = heartbeat?["every"] as? String
|
||||||
|
let heartbeatBody = heartbeat?["prompt"] as? String
|
||||||
let browser = parsed["browser"] as? [String: Any]
|
let browser = parsed["browser"] as? [String: Any]
|
||||||
let talk = parsed["talk"] as? [String: Any]
|
let talk = parsed["talk"] as? [String: Any]
|
||||||
|
|
||||||
let loadedModel = (agent?["model"] as? String) ?? ""
|
let loadedModel: String = {
|
||||||
|
if let raw = defaults?["model"] as? String { return raw }
|
||||||
|
if let modelDict = defaults?["model"] as? [String: Any],
|
||||||
|
let primary = modelDict["primary"] as? String { return primary }
|
||||||
|
return ""
|
||||||
|
}()
|
||||||
if !loadedModel.isEmpty {
|
if !loadedModel.isEmpty {
|
||||||
self.configModel = loadedModel
|
self.configModel = loadedModel
|
||||||
self.customModel = loadedModel
|
self.customModel = loadedModel
|
||||||
|
|
@ -402,7 +409,13 @@ struct ConfigSettings: View {
|
||||||
self.customModel = SessionLoader.fallbackModel
|
self.customModel = SessionLoader.fallbackModel
|
||||||
}
|
}
|
||||||
|
|
||||||
if let heartbeatMinutes { self.heartbeatMinutes = heartbeatMinutes }
|
if let heartbeatEvery {
|
||||||
|
let digits = heartbeatEvery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.prefix { $0.isNumber }
|
||||||
|
if let minutes = Int(digits) {
|
||||||
|
self.heartbeatMinutes = minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
if let heartbeatBody, !heartbeatBody.isEmpty { self.heartbeatBody = heartbeatBody }
|
if let heartbeatBody, !heartbeatBody.isEmpty { self.heartbeatBody = heartbeatBody }
|
||||||
|
|
||||||
if let browser {
|
if let browser {
|
||||||
|
|
@ -480,25 +493,49 @@ struct ConfigSettings: View {
|
||||||
@MainActor
|
@MainActor
|
||||||
private static func buildAndSaveConfig(_ draft: ConfigDraft) async -> String? {
|
private static func buildAndSaveConfig(_ draft: ConfigDraft) async -> String? {
|
||||||
var root = await ConfigStore.load()
|
var root = await ConfigStore.load()
|
||||||
var agent = root["agent"] as? [String: Any] ?? [:]
|
var agents = root["agents"] as? [String: Any] ?? [:]
|
||||||
|
var defaults = agents["defaults"] as? [String: Any] ?? [:]
|
||||||
var browser = root["browser"] as? [String: Any] ?? [:]
|
var browser = root["browser"] as? [String: Any] ?? [:]
|
||||||
var talk = root["talk"] as? [String: Any] ?? [:]
|
var talk = root["talk"] as? [String: Any] ?? [:]
|
||||||
|
|
||||||
let chosenModel = (draft.configModel == "__custom__" ? draft.customModel : draft.configModel)
|
let chosenModel = (draft.configModel == "__custom__" ? draft.customModel : draft.configModel)
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedModel = chosenModel
|
let trimmedModel = chosenModel
|
||||||
if !trimmedModel.isEmpty { agent["model"] = trimmedModel }
|
if !trimmedModel.isEmpty {
|
||||||
|
var model = defaults["model"] as? [String: Any] ?? [:]
|
||||||
|
model["primary"] = trimmedModel
|
||||||
|
defaults["model"] = model
|
||||||
|
|
||||||
|
var models = defaults["models"] as? [String: Any] ?? [:]
|
||||||
|
if models[trimmedModel] == nil {
|
||||||
|
models[trimmedModel] = [:]
|
||||||
|
}
|
||||||
|
defaults["models"] = models
|
||||||
|
}
|
||||||
|
|
||||||
if let heartbeatMinutes = draft.heartbeatMinutes {
|
if let heartbeatMinutes = draft.heartbeatMinutes {
|
||||||
agent["heartbeatMinutes"] = heartbeatMinutes
|
var heartbeat = defaults["heartbeat"] as? [String: Any] ?? [:]
|
||||||
|
heartbeat["every"] = "\(heartbeatMinutes)m"
|
||||||
|
defaults["heartbeat"] = heartbeat
|
||||||
}
|
}
|
||||||
|
|
||||||
let trimmedBody = draft.heartbeatBody.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedBody = draft.heartbeatBody.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmedBody.isEmpty {
|
if !trimmedBody.isEmpty {
|
||||||
agent["heartbeatBody"] = trimmedBody
|
var heartbeat = defaults["heartbeat"] as? [String: Any] ?? [:]
|
||||||
|
heartbeat["prompt"] = trimmedBody
|
||||||
|
defaults["heartbeat"] = heartbeat
|
||||||
}
|
}
|
||||||
|
|
||||||
root["agent"] = agent
|
if defaults.isEmpty {
|
||||||
|
agents.removeValue(forKey: "defaults")
|
||||||
|
} else {
|
||||||
|
agents["defaults"] = defaults
|
||||||
|
}
|
||||||
|
if agents.isEmpty {
|
||||||
|
root.removeValue(forKey: "agents")
|
||||||
|
} else {
|
||||||
|
root["agents"] = agents
|
||||||
|
}
|
||||||
|
|
||||||
browser["enabled"] = draft.browserEnabled
|
browser["enabled"] = draft.browserEnabled
|
||||||
let trimmedUrl = draft.browserControlUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedUrl = draft.browserControlUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
|
||||||
|
|
@ -607,7 +607,7 @@ extension OnboardingView {
|
||||||
let saved = await self.saveAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
let saved = await self.saveAgentWorkspace(AgentWorkspace.displayPath(for: url))
|
||||||
if saved {
|
if saved {
|
||||||
self.workspaceStatus =
|
self.workspaceStatus =
|
||||||
"Saved to ~/.clawdbot/clawdbot.json (agent.workspace)"
|
"Saved to ~/.clawdbot/clawdbot.json (agents.defaults.workspace)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,9 @@ extension OnboardingView {
|
||||||
|
|
||||||
private func loadAgentWorkspace() async -> String? {
|
private func loadAgentWorkspace() async -> String? {
|
||||||
let root = await ConfigStore.load()
|
let root = await ConfigStore.load()
|
||||||
let agent = root["agent"] as? [String: Any]
|
let agents = root["agents"] as? [String: Any]
|
||||||
return agent?["workspace"] as? String
|
let defaults = agents?["defaults"] as? [String: Any]
|
||||||
|
return defaults?["workspace"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
|
@ -86,17 +87,23 @@ extension OnboardingView {
|
||||||
@MainActor
|
@MainActor
|
||||||
private static func buildAndSaveWorkspace(_ workspace: String?) async -> (Bool, String?) {
|
private static func buildAndSaveWorkspace(_ workspace: String?) async -> (Bool, String?) {
|
||||||
var root = await ConfigStore.load()
|
var root = await ConfigStore.load()
|
||||||
var agent = root["agent"] as? [String: Any] ?? [:]
|
var agents = root["agents"] as? [String: Any] ?? [:]
|
||||||
|
var defaults = agents["defaults"] as? [String: Any] ?? [:]
|
||||||
let trimmed = workspace?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let trimmed = workspace?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if trimmed.isEmpty {
|
if trimmed.isEmpty {
|
||||||
agent.removeValue(forKey: "workspace")
|
defaults.removeValue(forKey: "workspace")
|
||||||
} else {
|
} else {
|
||||||
agent["workspace"] = trimmed
|
defaults["workspace"] = trimmed
|
||||||
}
|
}
|
||||||
if agent.isEmpty {
|
if defaults.isEmpty {
|
||||||
root.removeValue(forKey: "agent")
|
agents.removeValue(forKey: "defaults")
|
||||||
} else {
|
} else {
|
||||||
root["agent"] = agent
|
agents["defaults"] = defaults
|
||||||
|
}
|
||||||
|
if agents.isEmpty {
|
||||||
|
root.removeValue(forKey: "agents")
|
||||||
|
} else {
|
||||||
|
root["agents"] = agents
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try await ConfigStore.save(root)
|
try await ConfigStore.save(root)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ If you want a fixed channel, set `provider` + `to`. Otherwise `provider: "last"`
|
||||||
uses the last delivery route (falls back to WhatsApp).
|
uses the last delivery route (falls back to WhatsApp).
|
||||||
|
|
||||||
To force a cheaper model for Gmail runs, set `model` in the mapping
|
To force a cheaper model for Gmail runs, set `model` in the mapping
|
||||||
(`provider/model` or alias). If you enforce `agent.models`, include it there.
|
(`provider/model` or alias). If you enforce `agents.defaults.models`, include it there.
|
||||||
|
|
||||||
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
|
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
|
||||||
under `hooks.transformsDir` (see [`docs/webhook.md`](https://docs.clawd.bot/automation/webhook)).
|
under `hooks.transformsDir` (see [`docs/webhook.md`](https://docs.clawd.bot/automation/webhook)).
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ curl -X POST http://127.0.0.1:18789/hooks/agent \
|
||||||
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
|
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
If you enforce `agent.models`, make sure the override model is included there.
|
If you enforce `agents.defaults.models`, make sure the override model is included there.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
||||||
|
|
|
||||||
|
|
@ -488,10 +488,10 @@ Options:
|
||||||
Always includes the auth overview and OAuth expiry status for profiles in the auth store.
|
Always includes the auth overview and OAuth expiry status for profiles in the auth store.
|
||||||
|
|
||||||
### `models set <model>`
|
### `models set <model>`
|
||||||
Set `agent.model.primary`.
|
Set `agents.defaults.model.primary`.
|
||||||
|
|
||||||
### `models set-image <model>`
|
### `models set-image <model>`
|
||||||
Set `agent.imageModel.primary`.
|
Set `agents.defaults.imageModel.primary`.
|
||||||
|
|
||||||
### `models aliases list|add|remove`
|
### `models aliases list|add|remove`
|
||||||
Options:
|
Options:
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ Short, exact flow of one agent run.
|
||||||
|
|
||||||
## Timeouts
|
## Timeouts
|
||||||
- `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides.
|
- `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides.
|
||||||
- Agent runtime: `agent.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer.
|
- Agent runtime: `agents.defaults.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer.
|
||||||
|
|
||||||
## Where things can end early
|
## Where things can end early
|
||||||
- Agent timeout (abort)
|
- Agent timeout (abort)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ sessions.
|
||||||
**Important:** the workspace is the **default cwd**, not a hard sandbox. Tools
|
**Important:** the workspace is the **default cwd**, not a hard sandbox. Tools
|
||||||
resolve relative paths against the workspace, but absolute paths can still reach
|
resolve relative paths against the workspace, but absolute paths can still reach
|
||||||
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
|
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
|
||||||
[`agent.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config).
|
[`agents.defaults.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config).
|
||||||
When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate
|
When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate
|
||||||
inside a sandbox workspace under `~/.clawdbot/sandboxes`, not your host workspace.
|
inside a sandbox workspace under `~/.clawdbot/sandboxes`, not your host workspace.
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ only one workspace is active at a time.
|
||||||
**Recommendation:** keep a single active workspace. If you no longer use the
|
**Recommendation:** keep a single active workspace. If you no longer use the
|
||||||
legacy folders, archive or move them to Trash (for example `trash ~/clawdis`).
|
legacy folders, archive or move them to Trash (for example `trash ~/clawdis`).
|
||||||
If you intentionally keep multiple workspaces, make sure
|
If you intentionally keep multiple workspaces, make sure
|
||||||
`agent.workspace` points to the active one.
|
`agents.defaults.workspace` points to the active one.
|
||||||
|
|
||||||
`clawdbot doctor` warns when it detects legacy workspace directories.
|
`clawdbot doctor` warns when it detects legacy workspace directories.
|
||||||
|
|
||||||
|
|
@ -207,7 +207,7 @@ Suggested `.gitignore` starter:
|
||||||
## Moving the workspace to a new machine
|
## Moving the workspace to a new machine
|
||||||
|
|
||||||
1. Clone the repo to the desired path (default `~/clawd`).
|
1. Clone the repo to the desired path (default `~/clawd`).
|
||||||
2. Set `agent.workspace` to that path in `~/.clawdbot/clawdbot.json`.
|
2. Set `agents.defaults.workspace` to that path in `~/.clawdbot/clawdbot.json`.
|
||||||
3. Run `clawdbot setup --workspace <path>` to seed any missing files.
|
3. Run `clawdbot setup --workspace <path>` to seed any missing files.
|
||||||
4. If you need sessions, copy `~/.clawdbot/agents/<agentId>/sessions/` from the
|
4. If you need sessions, copy `~/.clawdbot/agents/<agentId>/sessions/` from the
|
||||||
old machine separately.
|
old machine separately.
|
||||||
|
|
@ -216,5 +216,5 @@ Suggested `.gitignore` starter:
|
||||||
|
|
||||||
- Multi-agent routing can use different workspaces per agent. See
|
- Multi-agent routing can use different workspaces per agent. See
|
||||||
`docs/provider-routing.md` for routing configuration.
|
`docs/provider-routing.md` for routing configuration.
|
||||||
- If `agent.sandbox` is enabled, non-main sessions can use per-session sandbox
|
- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox
|
||||||
workspaces under `agent.sandbox.workspaceRoot`.
|
workspaces under `agents.defaults.sandbox.workspaceRoot`.
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,19 @@ CLAWDBOT runs a single embedded agent runtime derived from **p-mono**.
|
||||||
|
|
||||||
## Workspace (required)
|
## Workspace (required)
|
||||||
|
|
||||||
CLAWDBOT uses a single agent workspace directory (`agent.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context.
|
CLAWDBOT uses a single agent workspace directory (`agents.defaults.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context.
|
||||||
|
|
||||||
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
|
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
|
||||||
|
|
||||||
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
|
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
|
||||||
|
|
||||||
If `agent.sandbox` is enabled, non-main sessions can override this with
|
If `agents.defaults.sandbox` is enabled, non-main sessions can override this with
|
||||||
per-session workspaces under `agent.sandbox.workspaceRoot` (see
|
per-session workspaces under `agents.defaults.sandbox.workspaceRoot` (see
|
||||||
[`docs/configuration.md`](/gateway/configuration)).
|
[`docs/configuration.md`](/gateway/configuration)).
|
||||||
|
|
||||||
## Bootstrap files (injected)
|
## Bootstrap files (injected)
|
||||||
|
|
||||||
Inside `agent.workspace`, CLAWDBOT expects these user-editable files:
|
Inside `agents.defaults.workspace`, CLAWDBOT expects these user-editable files:
|
||||||
- `AGENTS.md` — operating instructions + “memory”
|
- `AGENTS.md` — operating instructions + “memory”
|
||||||
- `SOUL.md` — persona, boundaries, tone
|
- `SOUL.md` — persona, boundaries, tone
|
||||||
- `TOOLS.md` — user-maintained tool notes (e.g. `imsg`, `sag`, conventions)
|
- `TOOLS.md` — user-maintained tool notes (e.g. `imsg`, `sag`, conventions)
|
||||||
|
|
@ -84,9 +84,9 @@ current turn ends, then a new agent turn starts with the queued payloads. See
|
||||||
[`docs/queue.md`](/concepts/queue) for mode + debounce/cap behavior.
|
[`docs/queue.md`](/concepts/queue) for mode + debounce/cap behavior.
|
||||||
|
|
||||||
Block streaming sends completed assistant blocks as soon as they finish; disable
|
Block streaming sends completed assistant blocks as soon as they finish; disable
|
||||||
via `agent.blockStreamingDefault: "off"` if you only want the final response.
|
via `agents.defaults.blockStreamingDefault: "off"` if you only want the final response.
|
||||||
Tune the boundary via `agent.blockStreamingBreak` (`text_end` vs `message_end`; defaults to text_end).
|
Tune the boundary via `agents.defaults.blockStreamingBreak` (`text_end` vs `message_end`; defaults to text_end).
|
||||||
Control soft block chunking with `agent.blockStreamingChunk` (defaults to
|
Control soft block chunking with `agents.defaults.blockStreamingChunk` (defaults to
|
||||||
800–1200 chars; prefers paragraph breaks, then newlines; sentences last).
|
800–1200 chars; prefers paragraph breaks, then newlines; sentences last).
|
||||||
Verbose tool summaries are emitted at tool start (no debounce); Control UI
|
Verbose tool summaries are emitted at tool start (no debounce); Control UI
|
||||||
streams tool output via agent events when available.
|
streams tool output via agent events when available.
|
||||||
|
|
@ -95,7 +95,7 @@ More details: [Streaming + chunking](/concepts/streaming).
|
||||||
## Configuration (minimal)
|
## Configuration (minimal)
|
||||||
|
|
||||||
At minimum, set:
|
At minimum, set:
|
||||||
- `agent.workspace`
|
- `agents.defaults.workspace`
|
||||||
- `whatsapp.allowFrom` (strongly recommended)
|
- `whatsapp.allowFrom` (strongly recommended)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ read_when:
|
||||||
|
|
||||||
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
|
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
|
||||||
|
|
||||||
Note: `routing.groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, you can override per agent with `routing.agents.<agentId>.mentionPatterns`.
|
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
|
||||||
|
|
||||||
## What’s implemented (2025-12-03)
|
## What’s implemented (2025-12-03)
|
||||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||||
|
|
@ -28,16 +28,21 @@ Add a `groupChat` block to `~/.clawdbot/clawdbot.json` so display-name pings wor
|
||||||
"*": { "requireMention": true }
|
"*": { "requireMention": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"agents": {
|
||||||
"groupChat": {
|
"list": [
|
||||||
"historyLimit": 50,
|
{
|
||||||
"mentionPatterns": [
|
"id": "main",
|
||||||
"@?clawd",
|
"groupChat": {
|
||||||
"@?clawd\\s*uk",
|
"historyLimit": 50,
|
||||||
"@?clawdbot",
|
"mentionPatterns": [
|
||||||
"\\+?447700900123"
|
"@?clawd",
|
||||||
]
|
"@?clawd\\s*uk",
|
||||||
}
|
"@?clawdbot",
|
||||||
|
"\\+?447700900123"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -70,4 +75,4 @@ Only the owner number (from `whatsapp.allowFrom`, or the bot’s own E.164 when
|
||||||
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
|
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
|
||||||
- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
|
- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
|
||||||
- Session store entries will appear as `agent:<agentId>:whatsapp:group:<jid>` in the session store (`~/.clawdbot/agents/<agentId>/sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet.
|
- Session store entries will appear as `agent:<agentId>:whatsapp:group:<jid>` in the session store (`~/.clawdbot/agents/<agentId>/sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet.
|
||||||
- Typing indicators in groups follow `agent.typingMode` (default: `message` when unmentioned).
|
- Typing indicators in groups follow `agents.defaults.typingMode` (default: `message` when unmentioned).
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,16 @@ Group messages require a mention unless overridden per group. Defaults live per
|
||||||
"123": { requireMention: false }
|
"123": { requireMention: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
groupChat: {
|
list: [
|
||||||
mentionPatterns: ["@clawd", "clawdbot", "\\+15555550123"],
|
{
|
||||||
historyLimit: 50
|
id: "main",
|
||||||
}
|
groupChat: {
|
||||||
|
mentionPatterns: ["@clawd", "clawdbot", "\\+15555550123"],
|
||||||
|
historyLimit: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -100,7 +105,7 @@ Group messages require a mention unless overridden per group. Defaults live per
|
||||||
Notes:
|
Notes:
|
||||||
- `mentionPatterns` are case-insensitive regexes.
|
- `mentionPatterns` are case-insensitive regexes.
|
||||||
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
|
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
|
||||||
- Per-agent override: `routing.agents.<agentId>.mentionPatterns` (useful when multiple agents share a group).
|
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
||||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||||
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
|
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ read_when:
|
||||||
|
|
||||||
Clawdbot handles failures in two stages:
|
Clawdbot handles failures in two stages:
|
||||||
1) **Auth profile rotation** within the current provider.
|
1) **Auth profile rotation** within the current provider.
|
||||||
2) **Model fallback** to the next model in `agent.model.fallbacks`.
|
2) **Model fallback** to the next model in `agents.defaults.model.fallbacks`.
|
||||||
|
|
||||||
This doc explains the runtime rules and the data that backs them.
|
This doc explains the runtime rules and the data that backs them.
|
||||||
|
|
||||||
|
|
@ -82,14 +82,14 @@ State is stored in `auth-profiles.json` under `usageStats`:
|
||||||
## Model fallback
|
## Model fallback
|
||||||
|
|
||||||
If all profiles for a provider fail, Clawdbot moves to the next model in
|
If all profiles for a provider fail, Clawdbot moves to the next model in
|
||||||
`agent.model.fallbacks`. This applies to auth failures, rate limits, and
|
`agents.defaults.model.fallbacks`. This applies to auth failures, rate limits, and
|
||||||
timeouts that exhausted profile rotation.
|
timeouts that exhausted profile rotation.
|
||||||
|
|
||||||
## Related config
|
## Related config
|
||||||
|
|
||||||
See [`docs/configuration.md`](/gateway/configuration) for:
|
See [`docs/configuration.md`](/gateway/configuration) for:
|
||||||
- `auth.profiles` / `auth.order`
|
- `auth.profiles` / `auth.order`
|
||||||
- `agent.model.primary` / `agent.model.fallbacks`
|
- `agents.defaults.model.primary` / `agents.defaults.model.fallbacks`
|
||||||
- `agent.imageModel` routing
|
- `agents.defaults.imageModel` routing
|
||||||
|
|
||||||
See [`docs/models.md`](/concepts/models) for the broader model selection and fallback overview.
|
See [`docs/models.md`](/concepts/models) for the broader model selection and fallback overview.
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,20 @@ rotation, cooldowns, and how that interacts with fallbacks.
|
||||||
|
|
||||||
Clawdbot selects models in this order:
|
Clawdbot selects models in this order:
|
||||||
|
|
||||||
1) **Primary** model (`agent.model.primary` or `agent.model`).
|
1) **Primary** model (`agents.defaults.model.primary` or `agents.defaults.model`).
|
||||||
2) **Fallbacks** in `agent.model.fallbacks` (in order).
|
2) **Fallbacks** in `agents.defaults.model.fallbacks` (in order).
|
||||||
3) **Provider auth failover** happens inside a provider before moving to the
|
3) **Provider auth failover** happens inside a provider before moving to the
|
||||||
next model.
|
next model.
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
- `agent.models` is the allowlist/catalog of models Clawdbot can use (plus aliases).
|
- `agents.defaults.models` is the allowlist/catalog of models Clawdbot can use (plus aliases).
|
||||||
- `agent.imageModel` is used **only when** the primary model can’t accept images.
|
- `agents.defaults.imageModel` is used **only when** the primary model can’t accept images.
|
||||||
|
|
||||||
## Config keys (overview)
|
## Config keys (overview)
|
||||||
|
|
||||||
- `agent.model.primary` and `agent.model.fallbacks`
|
- `agents.defaults.model.primary` and `agents.defaults.model.fallbacks`
|
||||||
- `agent.imageModel.primary` and `agent.imageModel.fallbacks`
|
- `agents.defaults.imageModel.primary` and `agents.defaults.imageModel.fallbacks`
|
||||||
- `agent.models` (allowlist + aliases + provider params)
|
- `agents.defaults.models` (allowlist + aliases + provider params)
|
||||||
- `models.providers` (custom providers written into `models.json`)
|
- `models.providers` (custom providers written into `models.json`)
|
||||||
|
|
||||||
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
|
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
|
||||||
|
|
@ -35,7 +35,7 @@ to `zai/*`.
|
||||||
|
|
||||||
## “Model is not allowed” (and why replies stop)
|
## “Model is not allowed” (and why replies stop)
|
||||||
|
|
||||||
If `agent.models` is set, it becomes the **allowlist** for `/model` and for
|
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for
|
||||||
session overrides. When a user selects a model that isn’t in that allowlist,
|
session overrides. When a user selects a model that isn’t in that allowlist,
|
||||||
Clawdbot returns:
|
Clawdbot returns:
|
||||||
|
|
||||||
|
|
@ -46,8 +46,8 @@ Model "provider/model" is not allowed. Use /model to list available models.
|
||||||
This happens **before** a normal reply is generated, so the message can feel
|
This happens **before** a normal reply is generated, so the message can feel
|
||||||
like it “didn’t respond.” The fix is to either:
|
like it “didn’t respond.” The fix is to either:
|
||||||
|
|
||||||
- Add the model to `agent.models`, or
|
- Add the model to `agents.defaults.models`, or
|
||||||
- Clear the allowlist (remove `agent.models`), or
|
- Clear the allowlist (remove `agents.defaults.models`), or
|
||||||
- Pick a model from `/model list`.
|
- Pick a model from `/model list`.
|
||||||
|
|
||||||
Example allowlist config:
|
Example allowlist config:
|
||||||
|
|
@ -123,8 +123,8 @@ Key flags:
|
||||||
- `--max-age-days <days>`: skip older models
|
- `--max-age-days <days>`: skip older models
|
||||||
- `--provider <name>`: provider prefix filter
|
- `--provider <name>`: provider prefix filter
|
||||||
- `--max-candidates <n>`: fallback list size
|
- `--max-candidates <n>`: fallback list size
|
||||||
- `--set-default`: set `agent.model.primary` to the first selection
|
- `--set-default`: set `agents.defaults.model.primary` to the first selection
|
||||||
- `--set-image`: set `agent.imageModel.primary` to the first image selection
|
- `--set-image`: set `agents.defaults.imageModel.primary` to the first image selection
|
||||||
|
|
||||||
Probing requires an OpenRouter API key (from auth profiles or
|
Probing requires an OpenRouter API key (from auth profiles or
|
||||||
`OPENROUTER_API_KEY`). Without a key, use `--no-probe` to list candidates only.
|
`OPENROUTER_API_KEY`). Without a key, use `--no-probe` to list candidates only.
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ reach other host locations unless sandboxing is enabled. See
|
||||||
- Config: `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`)
|
- Config: `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`)
|
||||||
- State dir: `~/.clawdbot` (or `CLAWDBOT_STATE_DIR`)
|
- State dir: `~/.clawdbot` (or `CLAWDBOT_STATE_DIR`)
|
||||||
- Workspace: `~/clawd` (or `~/clawd-<agentId>`)
|
- Workspace: `~/clawd` (or `~/clawd-<agentId>`)
|
||||||
- Agent dir: `~/.clawdbot/agents/<agentId>/agent` (or `routing.agents.<agentId>.agentDir`)
|
- Agent dir: `~/.clawdbot/agents/<agentId>/agent` (or `agents.list[].agentDir`)
|
||||||
- Sessions: `~/.clawdbot/agents/<agentId>/sessions`
|
- Sessions: `~/.clawdbot/agents/<agentId>/sessions`
|
||||||
|
|
||||||
### Single-agent mode (default)
|
### Single-agent mode (default)
|
||||||
|
|
@ -52,7 +52,7 @@ Use the agent wizard to add a new isolated agent:
|
||||||
clawdbot agents add work
|
clawdbot agents add work
|
||||||
```
|
```
|
||||||
|
|
||||||
Then add `routing.bindings` (or let the wizard do it) to route inbound messages.
|
Then add `bindings` (or let the wizard do it) to route inbound messages.
|
||||||
|
|
||||||
Verify with:
|
Verify with:
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ Bindings are **deterministic** and **most-specific wins**:
|
||||||
3. `teamId` (Slack)
|
3. `teamId` (Slack)
|
||||||
4. `accountId` match for a provider
|
4. `accountId` match for a provider
|
||||||
5. provider-level match (`accountId: "*"`)
|
5. provider-level match (`accountId: "*"`)
|
||||||
6. fallback to `routing.defaultAgentId` (default: `main`)
|
6. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`)
|
||||||
|
|
||||||
## Multiple accounts / phone numbers
|
## Multiple accounts / phone numbers
|
||||||
|
|
||||||
|
|
@ -100,39 +100,42 @@ multiple phone numbers without mixing sessions.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
defaultAgentId: "home",
|
list: [
|
||||||
|
{
|
||||||
agents: {
|
id: "home",
|
||||||
home: {
|
default: true,
|
||||||
name: "Home",
|
name: "Home",
|
||||||
workspace: "~/clawd-home",
|
workspace: "~/clawd-home",
|
||||||
agentDir: "~/.clawdbot/agents/home/agent",
|
agentDir: "~/.clawdbot/agents/home/agent",
|
||||||
},
|
},
|
||||||
work: {
|
{
|
||||||
|
id: "work",
|
||||||
name: "Work",
|
name: "Work",
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
agentDir: "~/.clawdbot/agents/work/agent",
|
agentDir: "~/.clawdbot/agents/work/agent",
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
// Deterministic routing: first match wins (most-specific first).
|
|
||||||
bindings: [
|
|
||||||
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
|
||||||
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
|
|
||||||
|
|
||||||
// Optional per-peer override (example: send a specific group to work agent).
|
|
||||||
{
|
|
||||||
agentId: "work",
|
|
||||||
match: {
|
|
||||||
provider: "whatsapp",
|
|
||||||
accountId: "personal",
|
|
||||||
peer: { kind: "group", id: "1203630...@g.us" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
|
// Deterministic routing: first match wins (most-specific first).
|
||||||
|
bindings: [
|
||||||
|
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
||||||
|
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
|
||||||
|
|
||||||
|
// Optional per-peer override (example: send a specific group to work agent).
|
||||||
|
{
|
||||||
|
agentId: "work",
|
||||||
|
match: {
|
||||||
|
provider: "whatsapp",
|
||||||
|
accountId: "personal",
|
||||||
|
peer: { kind: "group", id: "1203630...@g.us" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
|
||||||
|
tools: {
|
||||||
agentToAgent: {
|
agentToAgent: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allow: ["home", "work"],
|
allow: ["home", "work"],
|
||||||
|
|
@ -160,16 +163,18 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
personal: {
|
{
|
||||||
|
id: "personal",
|
||||||
workspace: "~/clawd-personal",
|
workspace: "~/clawd-personal",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "off", // No sandbox for personal agent
|
mode: "off", // No sandbox for personal agent
|
||||||
},
|
},
|
||||||
// No tool restrictions - all tools available
|
// No tool restrictions - all tools available
|
||||||
},
|
},
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all", // Always sandboxed
|
mode: "all", // Always sandboxed
|
||||||
|
|
@ -184,7 +189,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
|
||||||
deny: ["bash", "write", "edit"], // Deny others
|
deny: ["bash", "write", "edit"], // Deny others
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -194,8 +199,8 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
|
||||||
- **Resource control**: Sandbox specific agents while keeping others on host
|
- **Resource control**: Sandbox specific agents while keeping others on host
|
||||||
- **Flexible policies**: Different permissions per agent
|
- **Flexible policies**: Different permissions per agent
|
||||||
|
|
||||||
Note: `agent.elevated` is **global** and sender-based; it is not configurable per agent.
|
Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent.
|
||||||
If you need per-agent boundaries, use `routing.agents[id].tools` to deny `bash`.
|
If you need per-agent boundaries, use `agents.list[].tools` to deny `bash`.
|
||||||
For group targeting, you can set `routing.agents[id].mentionPatterns` so @mentions map cleanly to the intended agent.
|
For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
|
||||||
|
|
||||||
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples.
|
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples.
|
||||||
|
|
|
||||||
|
|
@ -42,35 +42,33 @@ Examples:
|
||||||
|
|
||||||
Routing picks **one agent** for each inbound message:
|
Routing picks **one agent** for each inbound message:
|
||||||
|
|
||||||
1. **Exact peer match** (`routing.bindings` with `peer.kind` + `peer.id`).
|
1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`).
|
||||||
2. **Guild match** (Discord) via `guildId`.
|
2. **Guild match** (Discord) via `guildId`.
|
||||||
3. **Team match** (Slack) via `teamId`.
|
3. **Team match** (Slack) via `teamId`.
|
||||||
4. **Account match** (`accountId` on the provider).
|
4. **Account match** (`accountId` on the provider).
|
||||||
5. **Provider match** (any account on that provider).
|
5. **Provider match** (any account on that provider).
|
||||||
6. **Default agent** (`routing.defaultAgentId`, fallback to `main`).
|
6. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`).
|
||||||
|
|
||||||
The matched agent determines which workspace and session store are used.
|
The matched agent determines which workspace and session store are used.
|
||||||
|
|
||||||
## Config overview
|
## Config overview
|
||||||
|
|
||||||
- `routing.defaultAgentId`: default agent when no binding matches.
|
- `agents.list`: named agent definitions (workspace, model, etc.).
|
||||||
- `routing.agents`: named agent definitions (workspace, model, etc.).
|
- `bindings`: map inbound providers/accounts/peers to agents.
|
||||||
- `routing.bindings`: map inbound providers/accounts/peers to agents.
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
defaultAgentId: "main",
|
list: [
|
||||||
agents: {
|
{ id: "support", name: "Support", workspace: "~/clawd-support" }
|
||||||
support: { name: "Support", workspace: "~/clawd-support" }
|
|
||||||
},
|
|
||||||
bindings: [
|
|
||||||
{ match: { provider: "slack", teamId: "T123" }, agentId: "support" },
|
|
||||||
{ match: { provider: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
bindings: [
|
||||||
|
{ match: { provider: "slack", teamId: "T123" }, agentId: "support" },
|
||||||
|
{ match: { provider: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ We now serialize command-based auto-replies (WhatsApp Web listener) through a ti
|
||||||
## How it works
|
## How it works
|
||||||
- A lane-aware FIFO queue drains each lane synchronously.
|
- A lane-aware FIFO queue drains each lane synchronously.
|
||||||
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
||||||
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agent.maxConcurrent`.
|
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
|
||||||
- When verbose logging is enabled, queued commands emit a short notice if they waited more than ~2s before starting.
|
- When verbose logging is enabled, queued commands emit a short notice if they waited more than ~2s before starting.
|
||||||
- Typing indicators (`onReplyStart`) still fire immediately on enqueue so user experience is unchanged while we wait our turn.
|
- Typing indicators (`onReplyStart`) still fire immediately on enqueue so user experience is unchanged while we wait our turn.
|
||||||
|
|
||||||
|
|
@ -30,16 +30,16 @@ Inbound messages can steer the current run, wait for a followup turn, or do both
|
||||||
Steer-backlog means you can get a followup response after the steered run, so
|
Steer-backlog means you can get a followup response after the steered run, so
|
||||||
streaming surfaces can look like duplicates. Prefer `collect`/`steer` if you want
|
streaming surfaces can look like duplicates. Prefer `collect`/`steer` if you want
|
||||||
one response per inbound message.
|
one response per inbound message.
|
||||||
Send `/queue collect` as a standalone command (per-session) or set `routing.queue.byProvider.discord: "collect"`.
|
Send `/queue collect` as a standalone command (per-session) or set `messages.queue.byProvider.discord: "collect"`.
|
||||||
|
|
||||||
Defaults (when unset in config):
|
Defaults (when unset in config):
|
||||||
- All surfaces → `collect`
|
- All surfaces → `collect`
|
||||||
|
|
||||||
Configure globally or per provider via `routing.queue`:
|
Configure globally or per provider via `messages.queue`:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
messages: {
|
||||||
queue: {
|
queue: {
|
||||||
mode: "collect",
|
mode: "collect",
|
||||||
debounceMs: 1000,
|
debounceMs: 1000,
|
||||||
|
|
@ -67,7 +67,7 @@ Defaults: `debounceMs: 1000`, `cap: 20`, `drop: summarize`.
|
||||||
|
|
||||||
## Scope and guarantees
|
## Scope and guarantees
|
||||||
- Applies only to config-driven command replies; plain text replies are unaffected.
|
- Applies only to config-driven command replies; plain text replies are unaffected.
|
||||||
- Default lane (`main`) is process-wide for inbound + main heartbeats; set `agent.maxConcurrent` to allow multiple sessions in parallel.
|
- Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel.
|
||||||
- Additional lanes may exist (e.g. `cron`) so background jobs can run in parallel without blocking inbound replies.
|
- Additional lanes may exist (e.g. `cron`) so background jobs can run in parallel without blocking inbound replies.
|
||||||
- Per-session lanes guarantee that only one agent run touches a given session at a time.
|
- Per-session lanes guarantee that only one agent run touches a given session at a time.
|
||||||
- No external dependencies or background worker threads; pure TypeScript + promises.
|
- No external dependencies or background worker threads; pure TypeScript + promises.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
summary: "Session pruning: tool-result trimming to reduce context bloat"
|
summary: "Session pruning: tool-result trimming to reduce context bloat"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to reduce LLM context growth from tool outputs
|
- You want to reduce LLM context growth from tool outputs
|
||||||
- You are tuning agent.contextPruning
|
- You are tuning agents.defaults.contextPruning
|
||||||
---
|
---
|
||||||
# Session Pruning
|
# Session Pruning
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ Session pruning trims **old tool results** from the in-memory context right befo
|
||||||
Pruning uses an estimated context window (chars ≈ tokens × 4). The window size is resolved in this order:
|
Pruning uses an estimated context window (chars ≈ tokens × 4). The window size is resolved in this order:
|
||||||
1) Model definition `contextWindow` (from the model registry).
|
1) Model definition `contextWindow` (from the model registry).
|
||||||
2) `models.providers.*.models[].contextWindow` override.
|
2) `models.providers.*.models[].contextWindow` override.
|
||||||
3) `agent.contextTokens`.
|
3) `agents.defaults.contextTokens`.
|
||||||
4) Default `200000` tokens.
|
4) Default `200000` tokens.
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
|
|
|
||||||
|
|
@ -132,19 +132,19 @@ Parameters:
|
||||||
- `cleanup?` (`delete|keep`, default `keep`)
|
- `cleanup?` (`delete|keep`, default `keep`)
|
||||||
|
|
||||||
Allowlist:
|
Allowlist:
|
||||||
- `routing.agents.<agentId>.subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
- `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
|
||||||
Discovery:
|
Discovery:
|
||||||
- Use `agents_list` to discover which agent ids are allowed for `sessions_spawn`.
|
- Use `agents_list` to discover which agent ids are allowed for `sessions_spawn`.
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- Starts a new `agent:<agentId>:subagent:<uuid>` session with `deliver: false`.
|
- Starts a new `agent:<agentId>:subagent:<uuid>` session with `deliver: false`.
|
||||||
- Sub-agents default to the full tool set **minus session tools** (configurable via `agent.subagents.tools`).
|
- Sub-agents default to the full tool set **minus session tools** (configurable via `tools.subagents.tools`).
|
||||||
- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
|
- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
|
||||||
- Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }` immediately.
|
- Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }` immediately.
|
||||||
- After completion, Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat provider.
|
- After completion, Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat provider.
|
||||||
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
|
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
|
||||||
- Sub-agent sessions are auto-archived after `agent.subagents.archiveAfterMinutes` (default: 60).
|
- Sub-agent sessions are auto-archived after `agents.defaults.subagents.archiveAfterMinutes` (default: 60).
|
||||||
- Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
|
- Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
|
||||||
|
|
||||||
## Sandbox Session Visibility
|
## Sandbox Session Visibility
|
||||||
|
|
@ -155,10 +155,12 @@ Config:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
// default: "spawned"
|
sandbox: {
|
||||||
sessionToolsVisibility: "spawned" // or "all"
|
// default: "spawned"
|
||||||
|
sessionToolsVisibility: "spawned" // or "all"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ Legend:
|
||||||
- `provider send`: actual outbound messages (block replies).
|
- `provider send`: actual outbound messages (block replies).
|
||||||
|
|
||||||
**Controls:**
|
**Controls:**
|
||||||
- `agent.blockStreamingDefault`: `"on"`/`"off"` (default on).
|
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default on).
|
||||||
- `agent.blockStreamingBreak`: `"text_end"` or `"message_end"`.
|
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`.
|
||||||
- `agent.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
|
- `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`.
|
||||||
- Provider hard cap: `*.textChunkLimit` (e.g., `whatsapp.textChunkLimit`).
|
- Provider hard cap: `*.textChunkLimit` (e.g., `whatsapp.textChunkLimit`).
|
||||||
- Discord soft cap: `discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
|
- Discord soft cap: `discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ The prompt is intentionally compact and uses fixed sections:
|
||||||
- **Tooling**: current tool list + short descriptions.
|
- **Tooling**: current tool list + short descriptions.
|
||||||
- **Skills**: tells the model how to load skill instructions on demand.
|
- **Skills**: tells the model how to load skill instructions on demand.
|
||||||
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
|
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
|
||||||
- **Workspace**: working directory (`agent.workspace`).
|
- **Workspace**: working directory (`agents.defaults.workspace`).
|
||||||
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
||||||
- **Time**: UTC default + the user’s local time (already converted).
|
- **Time**: UTC default + the user’s local time (already converted).
|
||||||
- **Reply Tags**: optional reply tag syntax for supported providers.
|
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||||
|
|
@ -43,9 +43,9 @@ Large files are truncated with a marker. Missing files inject a short missing-fi
|
||||||
The Time line is compact and explicit:
|
The Time line is compact and explicit:
|
||||||
|
|
||||||
- Assume timestamps are **UTC** unless stated.
|
- Assume timestamps are **UTC** unless stated.
|
||||||
- The listed **user time** is already converted to `agent.userTimezone` (if set).
|
- The listed **user time** is already converted to `agents.defaults.userTimezone` (if set).
|
||||||
|
|
||||||
Use `agent.userTimezone` in `~/.clawdbot/clawdbot.json` to change the user time zone.
|
Use `agents.defaults.userTimezone` in `~/.clawdbot/clawdbot.json` to change the user time zone.
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We d
|
||||||
|
|
||||||
## User timezone for the system prompt
|
## User timezone for the system prompt
|
||||||
|
|
||||||
Set `agent.userTimezone` to tell the model the user's local time zone. If it is
|
Set `agents.defaults.userTimezone` to tell the model the user's local time zone. If it is
|
||||||
unset, Clawdbot resolves the **host timezone at runtime** (no config write).
|
unset, Clawdbot resolves the **host timezone at runtime** (no config write).
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,18 @@ read_when:
|
||||||
# Typing indicators
|
# Typing indicators
|
||||||
|
|
||||||
Typing indicators are sent to the chat provider while a run is active. Use
|
Typing indicators are sent to the chat provider while a run is active. Use
|
||||||
`agent.typingMode` to control **when** typing starts and `typingIntervalSeconds`
|
`agents.defaults.typingMode` to control **when** typing starts and `typingIntervalSeconds`
|
||||||
to control **how often** it refreshes.
|
to control **how often** it refreshes.
|
||||||
|
|
||||||
## Defaults
|
## Defaults
|
||||||
When `agent.typingMode` is **unset**, Clawdbot keeps the legacy behavior:
|
When `agents.defaults.typingMode` is **unset**, Clawdbot keeps the legacy behavior:
|
||||||
- **Direct chats**: typing starts immediately once the model loop begins.
|
- **Direct chats**: typing starts immediately once the model loop begins.
|
||||||
- **Group chats with a mention**: typing starts immediately.
|
- **Group chats with a mention**: typing starts immediately.
|
||||||
- **Group chats without a mention**: typing starts only when message text begins streaming.
|
- **Group chats without a mention**: typing starts only when message text begins streaming.
|
||||||
- **Heartbeat runs**: typing is disabled.
|
- **Heartbeat runs**: typing is disabled.
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
Set `agent.typingMode` to one of:
|
Set `agents.defaults.typingMode` to one of:
|
||||||
- `never` — no typing indicator, ever.
|
- `never` — no typing indicator, ever.
|
||||||
- `instant` — start typing **as soon as the model loop begins**, even if the run
|
- `instant` — start typing **as soon as the model loop begins**, even if the run
|
||||||
later returns only the silent reply token.
|
later returns only the silent reply token.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ read_when:
|
||||||
|
|
||||||
# Workspace Memory v2 (offline): research notes
|
# Workspace Memory v2 (offline): research notes
|
||||||
|
|
||||||
Target: Clawd-style workspace (`agent.workspace`, default `~/clawd`) where “memory” is stored as one Markdown file per day (`memory/YYYY-MM-DD.md`) plus a small set of stable files (e.g. `memory.md`, `SOUL.md`).
|
Target: Clawd-style workspace (`agents.defaults.workspace`, default `~/clawd`) where “memory” is stored as one Markdown file per day (`memory/YYYY-MM-DD.md`) plus a small set of stable files (e.g. `memory.md`, `SOUL.md`).
|
||||||
|
|
||||||
This doc proposes an **offline-first** memory architecture that keeps Markdown as the canonical, reviewable source of truth, but adds **structured recall** (search, entity summaries, confidence updates) via a derived index.
|
This doc proposes an **offline-first** memory architecture that keeps Markdown as the canonical, reviewable source of truth, but adds **structured recall** (search, entity summaries, confidence updates) via a derived index.
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ Recommendation: **deep integration in Clawdbot**, but keep a separable core libr
|
||||||
|
|
||||||
### Why integrate into Clawdbot?
|
### Why integrate into Clawdbot?
|
||||||
- Clawdbot already knows:
|
- Clawdbot already knows:
|
||||||
- the workspace path (`agent.workspace`)
|
- the workspace path (`agents.defaults.workspace`)
|
||||||
- the session model + heartbeats
|
- the session model + heartbeats
|
||||||
- logging + troubleshooting patterns
|
- logging + troubleshooting patterns
|
||||||
- You want the agent itself to call the tools:
|
- You want the agent itself to call the tools:
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ Environment overrides:
|
||||||
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
||||||
|
|
||||||
Config (preferred):
|
Config (preferred):
|
||||||
- `agent.bash.backgroundMs` (default 10000)
|
- `tools.bash.backgroundMs` (default 10000)
|
||||||
- `agent.bash.timeoutSec` (default 1800)
|
- `tools.bash.timeoutSec` (default 1800)
|
||||||
- `agent.bash.cleanupMs` (default 1800000)
|
- `tools.bash.cleanupMs` (default 1800000)
|
||||||
|
|
||||||
## process tool
|
## process tool
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,52 +189,71 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||||
},
|
},
|
||||||
|
|
||||||
// Agent runtime
|
// Agent runtime
|
||||||
agent: {
|
agents: {
|
||||||
workspace: "~/clawd",
|
defaults: {
|
||||||
userTimezone: "America/Chicago",
|
workspace: "~/clawd",
|
||||||
model: {
|
userTimezone: "America/Chicago",
|
||||||
primary: "anthropic/claude-sonnet-4-5",
|
model: {
|
||||||
fallbacks: ["anthropic/claude-opus-4-5", "openai/gpt-5.2"]
|
primary: "anthropic/claude-sonnet-4-5",
|
||||||
},
|
fallbacks: ["anthropic/claude-opus-4-5", "openai/gpt-5.2"]
|
||||||
imageModel: {
|
},
|
||||||
primary: "openrouter/anthropic/claude-sonnet-4-5"
|
imageModel: {
|
||||||
},
|
primary: "openrouter/anthropic/claude-sonnet-4-5"
|
||||||
models: {
|
},
|
||||||
"anthropic/claude-opus-4-5": { alias: "opus" },
|
models: {
|
||||||
"anthropic/claude-sonnet-4-5": { alias: "sonnet" },
|
"anthropic/claude-opus-4-5": { alias: "opus" },
|
||||||
"openai/gpt-5.2": { alias: "gpt" }
|
"anthropic/claude-sonnet-4-5": { alias: "sonnet" },
|
||||||
},
|
"openai/gpt-5.2": { alias: "gpt" }
|
||||||
thinkingDefault: "low",
|
},
|
||||||
verboseDefault: "off",
|
thinkingDefault: "low",
|
||||||
elevatedDefault: "on",
|
verboseDefault: "off",
|
||||||
blockStreamingDefault: "on",
|
elevatedDefault: "on",
|
||||||
blockStreamingBreak: "text_end",
|
blockStreamingDefault: "on",
|
||||||
blockStreamingChunk: {
|
blockStreamingBreak: "text_end",
|
||||||
minChars: 800,
|
blockStreamingChunk: {
|
||||||
maxChars: 1200,
|
minChars: 800,
|
||||||
breakPreference: "paragraph"
|
maxChars: 1200,
|
||||||
},
|
breakPreference: "paragraph"
|
||||||
timeoutSeconds: 600,
|
},
|
||||||
mediaMaxMb: 5,
|
timeoutSeconds: 600,
|
||||||
typingIntervalSeconds: 5,
|
mediaMaxMb: 5,
|
||||||
maxConcurrent: 3,
|
typingIntervalSeconds: 5,
|
||||||
tools: {
|
maxConcurrent: 3,
|
||||||
allow: ["bash", "process", "read", "write", "edit"],
|
heartbeat: {
|
||||||
deny: ["browser", "canvas"]
|
every: "30m",
|
||||||
},
|
model: "anthropic/claude-sonnet-4-5",
|
||||||
|
target: "last",
|
||||||
|
to: "+15555550123",
|
||||||
|
prompt: "HEARTBEAT",
|
||||||
|
ackMaxChars: 30
|
||||||
|
},
|
||||||
|
sandbox: {
|
||||||
|
mode: "non-main",
|
||||||
|
perSession: true,
|
||||||
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
|
docker: {
|
||||||
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
|
workdir: "/workspace",
|
||||||
|
readOnlyRoot: true,
|
||||||
|
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
||||||
|
network: "none",
|
||||||
|
user: "1000:1000"
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tools: {
|
||||||
|
allow: ["bash", "process", "read", "write", "edit"],
|
||||||
|
deny: ["browser", "canvas"],
|
||||||
bash: {
|
bash: {
|
||||||
backgroundMs: 10000,
|
backgroundMs: 10000,
|
||||||
timeoutSec: 1800,
|
timeoutSec: 1800,
|
||||||
cleanupMs: 1800000
|
cleanupMs: 1800000
|
||||||
},
|
},
|
||||||
heartbeat: {
|
|
||||||
every: "30m",
|
|
||||||
model: "anthropic/claude-sonnet-4-5",
|
|
||||||
target: "last",
|
|
||||||
to: "+15555550123",
|
|
||||||
prompt: "HEARTBEAT",
|
|
||||||
ackMaxChars: 30
|
|
||||||
},
|
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowFrom: {
|
allowFrom: {
|
||||||
|
|
@ -246,22 +265,6 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||||
imessage: ["user@example.com"],
|
imessage: ["user@example.com"],
|
||||||
webchat: ["session:demo"]
|
webchat: ["session:demo"]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
sandbox: {
|
|
||||||
mode: "non-main",
|
|
||||||
perSession: true,
|
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
|
||||||
docker: {
|
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
|
||||||
workdir: "/workspace",
|
|
||||||
readOnlyRoot: true,
|
|
||||||
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
|
||||||
network: "none",
|
|
||||||
user: "1000:1000"
|
|
||||||
},
|
|
||||||
browser: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ CLAWDBOT reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (co
|
||||||
|
|
||||||
If the file is missing, CLAWDBOT uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to:
|
If the file is missing, CLAWDBOT uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to:
|
||||||
- restrict who can trigger the bot (`whatsapp.allowFrom`, `telegram.allowFrom`, etc.)
|
- restrict who can trigger the bot (`whatsapp.allowFrom`, `telegram.allowFrom`, etc.)
|
||||||
- control group allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `routing.groupChat`)
|
- control group allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `agents.list[].groupChat`)
|
||||||
- customize message prefixes (`messages`)
|
- customize message prefixes (`messages`)
|
||||||
- set the agent's workspace (`agent.workspace`)
|
- set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`)
|
||||||
- tune the embedded agent (`agent`) and session behavior (`session`)
|
- tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`)
|
||||||
- set the agent's identity (`identity`)
|
- set per-agent identity (`agents.list[].identity`)
|
||||||
|
|
||||||
> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
|
> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ Example (via `gateway call`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot gateway call config.apply --params '{
|
clawdbot gateway call config.apply --params '{
|
||||||
"raw": "{\\n agent: { workspace: \\"~/clawd\\" }\\n}\\n",
|
"raw": "{\\n agents: { defaults: { workspace: \\"~/clawd\\" } }\\n}\\n",
|
||||||
"sessionKey": "agent:main:whatsapp:dm:+15555550123",
|
"sessionKey": "agent:main:whatsapp:dm:+15555550123",
|
||||||
"restartDelayMs": 1000
|
"restartDelayMs": 1000
|
||||||
}'
|
}'
|
||||||
|
|
@ -49,7 +49,7 @@ clawdbot gateway call config.apply --params '{
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { workspace: "~/clawd" },
|
agents: { defaults: { workspace: "~/clawd" } },
|
||||||
whatsapp: { allowFrom: ["+15555550123"] }
|
whatsapp: { allowFrom: ["+15555550123"] }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -65,16 +65,19 @@ To prevent the bot from responding to WhatsApp @-mentions in groups (only respon
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { workspace: "~/clawd" },
|
agents: {
|
||||||
|
defaults: { workspace: "~/clawd" },
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "main",
|
||||||
|
groupChat: { mentionPatterns: ["@clawd", "reisponde"] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
// Allowlist is DMs only; including your own number enables self-chat mode.
|
// Allowlist is DMs only; including your own number enables self-chat mode.
|
||||||
allowFrom: ["+15555550123"],
|
allowFrom: ["+15555550123"],
|
||||||
groups: { "*": { requireMention: true } }
|
groups: { "*": { requireMention: true } }
|
||||||
},
|
|
||||||
routing: {
|
|
||||||
groupChat: {
|
|
||||||
mentionPatterns: ["@clawd", "reisponde"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -175,17 +178,21 @@ rotation order used for failover.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `identity`
|
### `agents.list[].identity`
|
||||||
|
|
||||||
Optional agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
||||||
|
|
||||||
If set, CLAWDBOT derives defaults (only when you haven’t set them explicitly):
|
If set, CLAWDBOT derives defaults (only when you haven’t set them explicitly):
|
||||||
- `messages.ackReaction` from `identity.emoji` (falls back to 👀)
|
- `messages.ackReaction` from the **active agent**’s `identity.emoji` (falls back to 👀)
|
||||||
- `routing.groupChat.mentionPatterns` from `identity.name` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
|
- `agents.list[].groupChat.mentionPatterns` from the agent’s `identity.name`/`identity.emoji` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }
|
agents: {
|
||||||
|
list: [
|
||||||
|
{ id: "main", identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -311,25 +318,26 @@ Notes:
|
||||||
- `default` is used when `accountId` is omitted (CLI + routing).
|
- `default` is used when `accountId` is omitted (CLI + routing).
|
||||||
- Env tokens only apply to the **default** account.
|
- Env tokens only apply to the **default** account.
|
||||||
- Base provider settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
- Base provider settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
||||||
- Use `routing.bindings[].match.accountId` to route each account to a different agent.
|
- Use `bindings[].match.accountId` to route each account to a different agents.defaults.
|
||||||
|
|
||||||
### `routing.groupChat`
|
### Group chat mention gating (`agents.list[].groupChat` + `messages.groupChat`)
|
||||||
|
|
||||||
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
|
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
|
||||||
|
|
||||||
**Mention types:**
|
**Mention types:**
|
||||||
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `whatsapp.allowFrom`).
|
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `whatsapp.allowFrom`).
|
||||||
- **Text patterns**: Regex patterns defined in `mentionPatterns`. Always checked regardless of self-chat mode.
|
- **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode.
|
||||||
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
|
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
|
||||||
- Per-agent override: `routing.agents.<agentId>.mentionPatterns` (useful when multiple agents share a group).
|
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
messages: {
|
||||||
groupChat: {
|
groupChat: { historyLimit: 50 }
|
||||||
mentionPatterns: ["@clawd", "clawdbot", "clawd"],
|
},
|
||||||
historyLimit: 50
|
agents: {
|
||||||
}
|
list: [
|
||||||
|
{ id: "main", groupChat: { mentionPatterns: ["@clawd", "clawdbot", "clawd"] } }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -337,11 +345,11 @@ Group messages default to **require mention** (either metadata mention or regex
|
||||||
Per-agent override (takes precedence when set, even `[]`):
|
Per-agent override (takes precedence when set, even `[]`):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
work: { mentionPatterns: ["@workbot", "\\+15555550123"] },
|
{ id: "work", groupChat: { mentionPatterns: ["@workbot", "\\+15555550123"] } },
|
||||||
personal: { mentionPatterns: ["@homebot", "\\+15555550999"] }
|
{ id: "personal", groupChat: { mentionPatterns: ["@homebot", "\\+15555550999"] } }
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -356,11 +364,16 @@ To respond **only** to specific text triggers (ignoring native @-mentions):
|
||||||
allowFrom: ["+15555550123"],
|
allowFrom: ["+15555550123"],
|
||||||
groups: { "*": { requireMention: true } }
|
groups: { "*": { requireMention: true } }
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
groupChat: {
|
list: [
|
||||||
// Only these text patterns will trigger responses
|
{
|
||||||
mentionPatterns: ["reisponde", "@clawd"]
|
id: "main",
|
||||||
}
|
groupChat: {
|
||||||
|
// Only these text patterns will trigger responses
|
||||||
|
mentionPatterns: ["reisponde", "@clawd"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -410,17 +423,22 @@ Notes:
|
||||||
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`).
|
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`).
|
||||||
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
|
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
|
||||||
|
|
||||||
### Multi-agent routing (`routing.agents` + `routing.bindings`)
|
### Multi-agent routing (`agents.list` + `bindings`)
|
||||||
|
|
||||||
Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside one Gateway. Inbound messages are routed to an agent via bindings.
|
Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside one Gateway.
|
||||||
|
Inbound messages are routed to an agent via bindings.
|
||||||
|
|
||||||
- `routing.defaultAgentId`: fallback when no binding matches (default: `main`).
|
- `agents.list[]`: per-agent overrides.
|
||||||
- `routing.agents.<agentId>`: per-agent overrides.
|
- `id`: stable agent id (required).
|
||||||
|
- `default`: optional; when multiple are set, the first wins and a warning is logged.
|
||||||
|
If none are set, the **first entry** in the list is the default agent.
|
||||||
- `name`: display name for the agent.
|
- `name`: display name for the agent.
|
||||||
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to legacy `agent.workspace`).
|
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
|
||||||
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
||||||
- `model`: per-agent default model (provider/model), overrides `agent.model` for that agent.
|
- `model`: per-agent default model (provider/model), overrides `agents.defaults.model` for that agent.
|
||||||
- `sandbox`: per-agent sandbox config (overrides `agent.sandbox`).
|
- `identity`: per-agent name/theme/emoji (used for mention patterns + ack reactions).
|
||||||
|
- `groupChat`: per-agent mention-gating (`mentionPatterns`).
|
||||||
|
- `sandbox`: per-agent sandbox config (overrides `agents.defaults.sandbox`).
|
||||||
- `mode`: `"off"` | `"non-main"` | `"all"`
|
- `mode`: `"off"` | `"non-main"` | `"all"`
|
||||||
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
|
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
|
||||||
- `scope`: `"session"` | `"agent"` | `"shared"`
|
- `scope`: `"session"` | `"agent"` | `"shared"`
|
||||||
|
|
@ -428,13 +446,13 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
|
||||||
- `docker`: per-agent docker overrides (e.g. `image`, `network`, `env`, `setupCommand`, limits; ignored when `scope: "shared"`)
|
- `docker`: per-agent docker overrides (e.g. `image`, `network`, `env`, `setupCommand`, limits; ignored when `scope: "shared"`)
|
||||||
- `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
|
- `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
|
||||||
- `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
|
- `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
|
||||||
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
|
||||||
- `subagents`: per-agent sub-agent defaults.
|
- `subagents`: per-agent sub-agent defaults.
|
||||||
- `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
|
- `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
|
||||||
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
- `tools`: per-agent tool restrictions (applied before sandbox tool policy).
|
||||||
- `allow`: array of allowed tool names
|
- `allow`: array of allowed tool names
|
||||||
- `deny`: array of denied tool names (deny wins)
|
- `deny`: array of denied tool names (deny wins)
|
||||||
- `routing.bindings[]`: routes inbound messages to an `agentId`.
|
- `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.).
|
||||||
|
- `bindings[]`: routes inbound messages to an `agentId`.
|
||||||
- `match.provider` (required)
|
- `match.provider` (required)
|
||||||
- `match.accountId` (optional; `*` = any account; omitted = default account)
|
- `match.accountId` (optional; `*` = any account; omitted = default account)
|
||||||
- `match.peer` (optional; `{ kind: dm|group|channel, id }`)
|
- `match.peer` (optional; `{ kind: dm|group|channel, id }`)
|
||||||
|
|
@ -446,9 +464,9 @@ Deterministic match order:
|
||||||
3) `match.teamId`
|
3) `match.teamId`
|
||||||
4) `match.accountId` (exact, no peer/guild/team)
|
4) `match.accountId` (exact, no peer/guild/team)
|
||||||
5) `match.accountId: "*"` (provider-wide, no peer/guild/team)
|
5) `match.accountId: "*"` (provider-wide, no peer/guild/team)
|
||||||
6) `routing.defaultAgentId`
|
6) default agent (`agents.list[].default`, else first list entry, else `"main"`)
|
||||||
|
|
||||||
Within each match tier, the first matching entry in `routing.bindings` wins.
|
Within each match tier, the first matching entry in `bindings` wins.
|
||||||
|
|
||||||
#### Per-agent access profiles (multi-agent)
|
#### Per-agent access profiles (multi-agent)
|
||||||
|
|
||||||
|
|
@ -464,13 +482,14 @@ additional examples.
|
||||||
Full access (no sandbox):
|
Full access (no sandbox):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
personal: {
|
{
|
||||||
|
id: "personal",
|
||||||
workspace: "~/clawd-personal",
|
workspace: "~/clawd-personal",
|
||||||
sandbox: { mode: "off" }
|
sandbox: { mode: "off" }
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -478,9 +497,10 @@ Full access (no sandbox):
|
||||||
Read-only tools + read-only workspace:
|
Read-only tools + read-only workspace:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -492,7 +512,7 @@ Read-only tools + read-only workspace:
|
||||||
deny: ["write", "edit", "bash", "process", "browser"]
|
deny: ["write", "edit", "bash", "process", "browser"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -500,9 +520,10 @@ Read-only tools + read-only workspace:
|
||||||
No filesystem access (messaging/session tools enabled):
|
No filesystem access (messaging/session tools enabled):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
public: {
|
{
|
||||||
|
id: "public",
|
||||||
workspace: "~/clawd-public",
|
workspace: "~/clawd-public",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -514,7 +535,7 @@ No filesystem access (messaging/session tools enabled):
|
||||||
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -523,17 +544,16 @@ Example: two WhatsApp accounts → two agents:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
defaultAgentId: "home",
|
list: [
|
||||||
agents: {
|
{ id: "home", default: true, workspace: "~/clawd-home" },
|
||||||
home: { workspace: "~/clawd-home" },
|
{ id: "work", workspace: "~/clawd-work" }
|
||||||
work: { workspace: "~/clawd-work" },
|
]
|
||||||
},
|
|
||||||
bindings: [
|
|
||||||
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
|
||||||
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
bindings: [
|
||||||
|
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
||||||
|
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } }
|
||||||
|
],
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
accounts: {
|
accounts: {
|
||||||
personal: {},
|
personal: {},
|
||||||
|
|
@ -543,13 +563,13 @@ Example: two WhatsApp accounts → two agents:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `routing.agentToAgent` (optional)
|
### `tools.agentToAgent` (optional)
|
||||||
|
|
||||||
Agent-to-agent messaging is opt-in:
|
Agent-to-agent messaging is opt-in:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
tools: {
|
||||||
agentToAgent: {
|
agentToAgent: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allow: ["home", "work"]
|
allow: ["home", "work"]
|
||||||
|
|
@ -558,13 +578,13 @@ Agent-to-agent messaging is opt-in:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `routing.queue`
|
### `messages.queue`
|
||||||
|
|
||||||
Controls how inbound messages behave when an agent run is already active.
|
Controls how inbound messages behave when an agent run is already active.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
messages: {
|
||||||
queue: {
|
queue: {
|
||||||
mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
|
mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
|
||||||
debounceMs: 1000,
|
debounceMs: 1000,
|
||||||
|
|
@ -859,7 +879,7 @@ Example wrapper:
|
||||||
exec ssh -T mac-mini "imsg rpc"
|
exec ssh -T mac-mini "imsg rpc"
|
||||||
```
|
```
|
||||||
|
|
||||||
### `agent.workspace`
|
### `agents.defaults.workspace`
|
||||||
|
|
||||||
Sets the **single global workspace directory** used by the agent for file operations.
|
Sets the **single global workspace directory** used by the agent for file operations.
|
||||||
|
|
||||||
|
|
@ -867,14 +887,14 @@ Default: `~/clawd`.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { workspace: "~/clawd" }
|
agents: { defaults: { workspace: "~/clawd" } }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If `agent.sandbox` is enabled, non-main sessions can override this with their
|
If `agents.defaults.sandbox` is enabled, non-main sessions can override this with their
|
||||||
own per-scope workspaces under `agent.sandbox.workspaceRoot`.
|
own per-scope workspaces under `agents.defaults.sandbox.workspaceRoot`.
|
||||||
|
|
||||||
### `agent.skipBootstrap`
|
### `agents.defaults.skipBootstrap`
|
||||||
|
|
||||||
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
|
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
|
||||||
|
|
||||||
|
|
@ -882,18 +902,18 @@ Use this for pre-seeded deployments where your workspace files come from a repo.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { skipBootstrap: true }
|
agents: { defaults: { skipBootstrap: true } }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `agent.userTimezone`
|
### `agents.defaults.userTimezone`
|
||||||
|
|
||||||
Sets the user’s timezone for **system prompt context** (not for timestamps in
|
Sets the user’s timezone for **system prompt context** (not for timestamps in
|
||||||
message envelopes). If unset, Clawdbot uses the host timezone at runtime.
|
message envelopes). If unset, Clawdbot uses the host timezone at runtime.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { userTimezone: "America/Chicago" }
|
agents: { defaults: { userTimezone: "America/Chicago" } }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -917,7 +937,7 @@ streaming, final replies) across providers unless already present.
|
||||||
|
|
||||||
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
||||||
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
||||||
configured `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
|
active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
|
||||||
|
|
||||||
`ackReactionScope` controls when reactions fire:
|
`ackReactionScope` controls when reactions fire:
|
||||||
- `group-mentions` (default): only when a group/room requires mentions **and** the bot was mentioned
|
- `group-mentions` (default): only when a group/room requires mentions **and** the bot was mentioned
|
||||||
|
|
@ -947,22 +967,22 @@ Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_V
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `agent`
|
### `agents.defaults`
|
||||||
|
|
||||||
Controls the embedded agent runtime (model/thinking/verbose/timeouts).
|
Controls the embedded agent runtime (model/thinking/verbose/timeouts).
|
||||||
`agent.models` defines the configured model catalog (and acts as the allowlist for `/model`).
|
`agents.defaults.models` defines the configured model catalog (and acts as the allowlist for `/model`).
|
||||||
`agent.model.primary` sets the default model; `agent.model.fallbacks` are global failovers.
|
`agents.defaults.model.primary` sets the default model; `agents.defaults.model.fallbacks` are global failovers.
|
||||||
`agent.imageModel` is optional and is **only used if the primary model lacks image input**.
|
`agents.defaults.imageModel` is optional and is **only used if the primary model lacks image input**.
|
||||||
Each `agent.models` entry can include:
|
Each `agents.defaults.models` entry can include:
|
||||||
- `alias` (optional model shortcut, e.g. `/opus`).
|
- `alias` (optional model shortcut, e.g. `/opus`).
|
||||||
- `params` (optional provider-specific API params passed through to the model request).
|
- `params` (optional provider-specific API params passed through to the model request).
|
||||||
|
|
||||||
Z.AI GLM-4.x models automatically enable thinking mode unless you:
|
Z.AI GLM-4.x models automatically enable thinking mode unless you:
|
||||||
- set `--thinking off`, or
|
- set `--thinking off`, or
|
||||||
- define `agent.models["zai/<model>"].params.thinking` yourself.
|
- define `agents.defaults.models["zai/<model>"].params.thinking` yourself.
|
||||||
|
|
||||||
Clawdbot also ships a few built-in alias shorthands. Defaults only apply when the model
|
Clawdbot also ships a few built-in alias shorthands. Defaults only apply when the model
|
||||||
is already present in `agent.models`:
|
is already present in `agents.defaults.models`:
|
||||||
|
|
||||||
- `opus` -> `anthropic/claude-opus-4-5`
|
- `opus` -> `anthropic/claude-opus-4-5`
|
||||||
- `sonnet` -> `anthropic/claude-sonnet-4-5`
|
- `sonnet` -> `anthropic/claude-sonnet-4-5`
|
||||||
|
|
@ -975,61 +995,63 @@ If you configure the same alias name (case-insensitive) yourself, your value win
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
models: {
|
||||||
"anthropic/claude-sonnet-4-1": { alias: "Sonnet" },
|
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||||
"openrouter/deepseek/deepseek-r1:free": {},
|
"anthropic/claude-sonnet-4-1": { alias: "Sonnet" },
|
||||||
"zai/glm-4.7": {
|
"openrouter/deepseek/deepseek-r1:free": {},
|
||||||
alias: "GLM",
|
"zai/glm-4.7": {
|
||||||
params: {
|
alias: "GLM",
|
||||||
thinking: {
|
params: {
|
||||||
type: "enabled",
|
thinking: {
|
||||||
clear_thinking: false
|
type: "enabled",
|
||||||
|
clear_thinking: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
model: {
|
||||||
model: {
|
primary: "anthropic/claude-opus-4-5",
|
||||||
primary: "anthropic/claude-opus-4-5",
|
fallbacks: [
|
||||||
fallbacks: [
|
"openrouter/deepseek/deepseek-r1:free",
|
||||||
"openrouter/deepseek/deepseek-r1:free",
|
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
|
||||||
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
|
]
|
||||||
]
|
},
|
||||||
},
|
imageModel: {
|
||||||
imageModel: {
|
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
|
||||||
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
|
fallbacks: [
|
||||||
fallbacks: [
|
"openrouter/google/gemini-2.0-flash-vision:free"
|
||||||
"openrouter/google/gemini-2.0-flash-vision:free"
|
]
|
||||||
]
|
},
|
||||||
},
|
thinkingDefault: "low",
|
||||||
thinkingDefault: "low",
|
verboseDefault: "off",
|
||||||
verboseDefault: "off",
|
elevatedDefault: "on",
|
||||||
elevatedDefault: "on",
|
timeoutSeconds: 600,
|
||||||
timeoutSeconds: 600,
|
mediaMaxMb: 5,
|
||||||
mediaMaxMb: 5,
|
heartbeat: {
|
||||||
heartbeat: {
|
every: "30m",
|
||||||
every: "30m",
|
target: "last"
|
||||||
target: "last"
|
},
|
||||||
},
|
maxConcurrent: 3,
|
||||||
maxConcurrent: 3,
|
subagents: {
|
||||||
subagents: {
|
maxConcurrent: 1,
|
||||||
maxConcurrent: 1,
|
archiveAfterMinutes: 60
|
||||||
archiveAfterMinutes: 60
|
},
|
||||||
},
|
bash: {
|
||||||
bash: {
|
backgroundMs: 10000,
|
||||||
backgroundMs: 10000,
|
timeoutSec: 1800,
|
||||||
timeoutSec: 1800,
|
cleanupMs: 1800000
|
||||||
cleanupMs: 1800000
|
},
|
||||||
},
|
contextTokens: 200000
|
||||||
contextTokens: 200000
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `agent.contextPruning` (tool-result pruning)
|
#### `agents.defaults.contextPruning` (tool-result pruning)
|
||||||
|
|
||||||
`agent.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM.
|
`agents.defaults.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM.
|
||||||
It does **not** modify the session history on disk (`*.jsonl` remains complete).
|
It does **not** modify the session history on disk (`*.jsonl` remains complete).
|
||||||
|
|
||||||
This is intended to reduce token usage for chatty agents that accumulate large tool outputs over time.
|
This is intended to reduce token usage for chatty agents that accumulate large tool outputs over time.
|
||||||
|
|
@ -1061,22 +1083,14 @@ Notes / current limitations:
|
||||||
Default (adaptive):
|
Default (adaptive):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: { defaults: { contextPruning: { mode: "adaptive" } } }
|
||||||
contextPruning: {
|
|
||||||
mode: "adaptive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable:
|
To disable:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: { defaults: { contextPruning: { mode: "off" } } }
|
||||||
contextPruning: {
|
|
||||||
mode: "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1091,28 +1105,26 @@ Defaults (when `mode` is `"adaptive"` or `"aggressive"`):
|
||||||
Example (aggressive, minimal):
|
Example (aggressive, minimal):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: { defaults: { contextPruning: { mode: "aggressive" } } }
|
||||||
contextPruning: {
|
|
||||||
mode: "aggressive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example (adaptive tuned):
|
Example (adaptive tuned):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
contextPruning: {
|
defaults: {
|
||||||
mode: "adaptive",
|
contextPruning: {
|
||||||
keepLastAssistants: 3,
|
mode: "adaptive",
|
||||||
softTrimRatio: 0.3,
|
keepLastAssistants: 3,
|
||||||
hardClearRatio: 0.5,
|
softTrimRatio: 0.3,
|
||||||
minPrunableToolChars: 50000,
|
hardClearRatio: 0.5,
|
||||||
softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
|
minPrunableToolChars: 50000,
|
||||||
hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
|
softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
|
||||||
// Optional: restrict pruning to specific tools (deny wins; supports "*" wildcards)
|
hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
|
||||||
tools: { deny: ["browser", "canvas"] },
|
// Optional: restrict pruning to specific tools (deny wins; supports "*" wildcards)
|
||||||
|
tools: { deny: ["browser", "canvas"] },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1121,36 +1133,34 @@ Example (adaptive tuned):
|
||||||
See [/concepts/session-pruning](/concepts/session-pruning) for behavior details.
|
See [/concepts/session-pruning](/concepts/session-pruning) for behavior details.
|
||||||
|
|
||||||
Block streaming:
|
Block streaming:
|
||||||
- `agent.blockStreamingDefault`: `"on"`/`"off"` (default on).
|
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default on).
|
||||||
- `agent.blockStreamingBreak`: `"text_end"` or `"message_end"` (default: text_end).
|
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"` (default: text_end).
|
||||||
- `agent.blockStreamingChunk`: soft chunking for streamed blocks. Defaults to
|
- `agents.defaults.blockStreamingChunk`: soft chunking for streamed blocks. Defaults to
|
||||||
800–1200 chars, prefers paragraph breaks (`\n\n`), then newlines, then sentences.
|
800–1200 chars, prefers paragraph breaks (`\n\n`), then newlines, then sentences.
|
||||||
Example:
|
Example:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: { defaults: { blockStreamingChunk: { minChars: 800, maxChars: 1200 } } }
|
||||||
blockStreamingChunk: { minChars: 800, maxChars: 1200 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
See [/concepts/streaming](/concepts/streaming) for behavior + chunking details.
|
See [/concepts/streaming](/concepts/streaming) for behavior + chunking details.
|
||||||
|
|
||||||
Typing indicators:
|
Typing indicators:
|
||||||
- `agent.typingMode`: `"never" | "instant" | "thinking" | "message"`. Defaults to
|
- `agents.defaults.typingMode`: `"never" | "instant" | "thinking" | "message"`. Defaults to
|
||||||
`instant` for direct chats / mentions and `message` for unmentioned group chats.
|
`instant` for direct chats / mentions and `message` for unmentioned group chats.
|
||||||
- `session.typingMode`: per-session override for the mode.
|
- `session.typingMode`: per-session override for the mode.
|
||||||
- `agent.typingIntervalSeconds`: how often the typing signal is refreshed (default: 6s).
|
- `agents.defaults.typingIntervalSeconds`: how often the typing signal is refreshed (default: 6s).
|
||||||
- `session.typingIntervalSeconds`: per-session override for the refresh interval.
|
- `session.typingIntervalSeconds`: per-session override for the refresh interval.
|
||||||
See [/concepts/typing-indicators](/concepts/typing-indicators) for behavior details.
|
See [/concepts/typing-indicators](/concepts/typing-indicators) for behavior details.
|
||||||
|
|
||||||
`agent.model.primary` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
|
`agents.defaults.model.primary` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
|
||||||
Aliases come from `agent.models.*.alias` (e.g. `Opus`).
|
Aliases come from `agents.defaults.models.*.alias` (e.g. `Opus`).
|
||||||
If you omit the provider, CLAWDBOT currently assumes `anthropic` as a temporary
|
If you omit the provider, CLAWDBOT currently assumes `anthropic` as a temporary
|
||||||
deprecation fallback.
|
deprecation fallback.
|
||||||
Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||||
`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
|
`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
|
||||||
|
|
||||||
`agent.heartbeat` configures periodic heartbeat runs:
|
`agents.defaults.heartbeat` configures periodic heartbeat runs:
|
||||||
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
|
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
|
||||||
`30m`. Set `0m` to disable.
|
`30m`. Set `0m` to disable.
|
||||||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
- `model`: optional override model for heartbeat runs (`provider/model`).
|
||||||
|
|
@ -1162,31 +1172,27 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||||
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
|
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
|
||||||
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
|
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
|
||||||
|
|
||||||
`agent.bash` configures background bash defaults:
|
`tools.bash` configures background bash defaults:
|
||||||
- `backgroundMs`: time before auto-background (ms, default 10000)
|
- `backgroundMs`: time before auto-background (ms, default 10000)
|
||||||
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
||||||
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
||||||
|
|
||||||
`agent.subagents` configures sub-agent defaults:
|
`agents.defaults.subagents` configures sub-agent defaults:
|
||||||
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
||||||
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
||||||
- `tools.allow` / `tools.deny`: per-subagent tool allow/deny policy (deny wins)
|
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
||||||
|
|
||||||
`agent.tools` configures a global tool allow/deny policy (deny wins).
|
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||||
This is applied even when the Docker sandbox is **off**.
|
This is applied even when the Docker sandbox is **off**.
|
||||||
|
|
||||||
Example (disable browser/canvas everywhere):
|
Example (disable browser/canvas everywhere):
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
tools: { deny: ["browser", "canvas"] }
|
||||||
tools: {
|
|
||||||
deny: ["browser", "canvas"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`agent.elevated` controls elevated (host) bash access:
|
`tools.elevated` controls elevated (host) bash access:
|
||||||
- `enabled`: allow elevated mode (default true)
|
- `enabled`: allow elevated mode (default true)
|
||||||
- `allowFrom`: per-provider allowlists (empty = disabled)
|
- `allowFrom`: per-provider allowlists (empty = disabled)
|
||||||
- `whatsapp`: E.164 numbers
|
- `whatsapp`: E.164 numbers
|
||||||
|
|
@ -1199,7 +1205,7 @@ Example (disable browser/canvas everywhere):
|
||||||
Example:
|
Example:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowFrom: {
|
allowFrom: {
|
||||||
|
|
@ -1212,16 +1218,16 @@ Example:
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `agent.elevated` is **global** (not per-agent). Availability is based on sender allowlists.
|
- `tools.elevated` is **global** (not per-agent). Availability is based on sender allowlists.
|
||||||
- `/elevated on|off` stores state per session key; inline directives apply to a single message.
|
- `/elevated on|off` stores state per session key; inline directives apply to a single message.
|
||||||
- Elevated `bash` runs on the host and bypasses sandboxing.
|
- Elevated `bash` runs on the host and bypasses sandboxing.
|
||||||
- Tool policy still applies; if `bash` is denied, elevated cannot be used.
|
- Tool policy still applies; if `bash` is denied, elevated cannot be used.
|
||||||
|
|
||||||
`agent.maxConcurrent` sets the maximum number of embedded agent runs that can
|
`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
|
||||||
execute in parallel across sessions. Each session is still serialized (one run
|
execute in parallel across sessions. Each session is still serialized (one run
|
||||||
per session key at a time). Default: 1.
|
per session key at a time). Default: 1.
|
||||||
|
|
||||||
### `agent.sandbox`
|
### `agents.defaults.sandbox`
|
||||||
|
|
||||||
Optional **Docker sandboxing** for the embedded agent. Intended for non-main
|
Optional **Docker sandboxing** for the embedded agent. Intended for non-main
|
||||||
sessions so they cannot access your host system.
|
sessions so they cannot access your host system.
|
||||||
|
|
@ -1236,7 +1242,8 @@ Defaults (if enabled):
|
||||||
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||||||
- `"rw"`: mount the agent workspace read/write at `/workspace`
|
- `"rw"`: mount the agent workspace read/write at `/workspace`
|
||||||
- auto-prune: idle > 24h OR age > 7d
|
- auto-prune: idle > 24h OR age > 7d
|
||||||
- tools: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn` (deny wins)
|
- tool policy: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn` (deny wins)
|
||||||
|
- configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
|
||||||
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
||||||
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
||||||
|
|
||||||
|
|
@ -1248,54 +1255,60 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "non-main", // off | non-main | all
|
sandbox: {
|
||||||
scope: "agent", // session | agent | shared (agent is default)
|
mode: "non-main", // off | non-main | all
|
||||||
workspaceAccess: "none", // none | ro | rw
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceAccess: "none", // none | ro | rw
|
||||||
docker: {
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
docker: {
|
||||||
containerPrefix: "clawdbot-sbx-",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
workdir: "/workspace",
|
containerPrefix: "clawdbot-sbx-",
|
||||||
readOnlyRoot: true,
|
workdir: "/workspace",
|
||||||
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
readOnlyRoot: true,
|
||||||
network: "none",
|
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
||||||
user: "1000:1000",
|
network: "none",
|
||||||
capDrop: ["ALL"],
|
user: "1000:1000",
|
||||||
env: { LANG: "C.UTF-8" },
|
capDrop: ["ALL"],
|
||||||
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
env: { LANG: "C.UTF-8" },
|
||||||
// Per-agent override (multi-agent): routing.agents.<agentId>.sandbox.docker.*
|
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
||||||
pidsLimit: 256,
|
// Per-agent override (multi-agent): agents.list[].sandbox.docker.*
|
||||||
memory: "1g",
|
pidsLimit: 256,
|
||||||
memorySwap: "2g",
|
memory: "1g",
|
||||||
cpus: 1,
|
memorySwap: "2g",
|
||||||
ulimits: {
|
cpus: 1,
|
||||||
nofile: { soft: 1024, hard: 2048 },
|
ulimits: {
|
||||||
nproc: 256
|
nofile: { soft: 1024, hard: 2048 },
|
||||||
|
nproc: 256
|
||||||
|
},
|
||||||
|
seccompProfile: "/path/to/seccomp.json",
|
||||||
|
apparmorProfile: "clawdbot-sandbox",
|
||||||
|
dns: ["1.1.1.1", "8.8.8.8"],
|
||||||
|
extraHosts: ["internal.service:10.0.0.5"]
|
||||||
},
|
},
|
||||||
seccompProfile: "/path/to/seccomp.json",
|
browser: {
|
||||||
apparmorProfile: "clawdbot-sandbox",
|
enabled: false,
|
||||||
dns: ["1.1.1.1", "8.8.8.8"],
|
image: "clawdbot-sandbox-browser:bookworm-slim",
|
||||||
extraHosts: ["internal.service:10.0.0.5"]
|
containerPrefix: "clawdbot-sbx-browser-",
|
||||||
},
|
cdpPort: 9222,
|
||||||
browser: {
|
vncPort: 5900,
|
||||||
enabled: false,
|
noVncPort: 6080,
|
||||||
image: "clawdbot-sandbox-browser:bookworm-slim",
|
headless: false,
|
||||||
containerPrefix: "clawdbot-sbx-browser-",
|
enableNoVnc: true
|
||||||
cdpPort: 9222,
|
},
|
||||||
vncPort: 5900,
|
prune: {
|
||||||
noVncPort: 6080,
|
idleHours: 24, // 0 disables idle pruning
|
||||||
headless: false,
|
maxAgeDays: 7 // 0 disables max-age pruning
|
||||||
enableNoVnc: true
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: {
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
|
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
|
||||||
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
||||||
},
|
|
||||||
prune: {
|
|
||||||
idleHours: 24, // 0 disables idle pruning
|
|
||||||
maxAgeDays: 7 // 0 disables max-age pruning
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1307,7 +1320,7 @@ Build the default sandbox image once with:
|
||||||
scripts/sandbox-setup.sh
|
scripts/sandbox-setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: sandbox containers default to `network: "none"`; set `agent.sandbox.docker.network`
|
Note: sandbox containers default to `network: "none"`; set `agents.defaults.sandbox.docker.network`
|
||||||
to `"bridge"` (or your custom network) if the agent needs outbound access.
|
to `"bridge"` (or your custom network) if the agent needs outbound access.
|
||||||
|
|
||||||
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
|
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
|
||||||
|
|
@ -1317,7 +1330,7 @@ Build the optional browser image with:
|
||||||
scripts/sandbox-browser-setup.sh
|
scripts/sandbox-browser-setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
When `agent.sandbox.browser.enabled=true`, the browser tool uses a sandboxed
|
When `agents.defaults.sandbox.browser.enabled=true`, the browser tool uses a sandboxed
|
||||||
Chromium instance (CDP). If noVNC is enabled (default when headless=false),
|
Chromium instance (CDP). If noVNC is enabled (default when headless=false),
|
||||||
the noVNC URL is injected into the system prompt so the agent can reference it.
|
the noVNC URL is injected into the system prompt so the agent can reference it.
|
||||||
This does not require `browser.enabled` in the main config; the sandbox control
|
This does not require `browser.enabled` in the main config; the sandbox control
|
||||||
|
|
@ -1335,14 +1348,16 @@ When `models.providers` is present, Clawdbot writes/merges a `models.json` into
|
||||||
- default behavior: **merge** (keeps existing providers, overrides on name)
|
- default behavior: **merge** (keeps existing providers, overrides on name)
|
||||||
- set `models.mode: "replace"` to overwrite the file contents
|
- set `models.mode: "replace"` to overwrite the file contents
|
||||||
|
|
||||||
Select the model via `agent.model.primary` (provider/model).
|
Select the model via `agents.defaults.model.primary` (provider/model).
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "custom-proxy/llama-3.1-8b" },
|
defaults: {
|
||||||
models: {
|
model: { primary: "custom-proxy/llama-3.1-8b" },
|
||||||
"custom-proxy/llama-3.1-8b": {}
|
models: {
|
||||||
|
"custom-proxy/llama-3.1-8b": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
|
|
@ -1376,9 +1391,11 @@ in your environment and reference the model by provider/model.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "zai/glm-4.7",
|
defaults: {
|
||||||
allowedModels: ["zai/glm-4.7"]
|
model: { primary: "zai/glm-4.7" },
|
||||||
|
models: { "zai/glm-4.7": {} }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1401,11 +1418,13 @@ via **LM Studio** using the **Responses API**.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "lmstudio/minimax-m2.1-gs32" },
|
defaults: {
|
||||||
models: {
|
model: { primary: "lmstudio/minimax-m2.1-gs32" },
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
models: {
|
||||||
"lmstudio/minimax-m2.1-gs32": { alias: "Minimax" }
|
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||||
|
"lmstudio/minimax-m2.1-gs32": { alias: "Minimax" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
|
|
@ -1475,7 +1494,7 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
|
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
|
||||||
- Sandbox note: `agent.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
|
- Sandbox note: `agents.defaults.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
|
||||||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||||||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||||||
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||||
|
|
@ -1684,7 +1703,7 @@ Hot-applied (no full gateway restart):
|
||||||
- `hooks` (webhook auth/path/mappings) + `hooks.gmail` (Gmail watcher restarted)
|
- `hooks` (webhook auth/path/mappings) + `hooks.gmail` (Gmail watcher restarted)
|
||||||
- `browser` (browser control server restart)
|
- `browser` (browser control server restart)
|
||||||
- `cron` (cron service restart + concurrency update)
|
- `cron` (cron service restart + concurrency update)
|
||||||
- `agent.heartbeat` (heartbeat runner restart)
|
- `agents.defaults.heartbeat` (heartbeat runner restart)
|
||||||
- `web` (WhatsApp web provider restart)
|
- `web` (WhatsApp web provider restart)
|
||||||
- `telegram`, `discord`, `signal`, `imessage` (provider restarts)
|
- `telegram`, `discord`, `signal`, `imessage` (provider restarts)
|
||||||
- `agent`, `models`, `routing`, `messages`, `session`, `whatsapp`, `logging`, `skills`, `ui`, `talk`, `identity`, `wizard` (dynamic reads)
|
- `agent`, `models`, `routing`, `messages`, `session`, `whatsapp`, `logging`, `skills`, `ui`, `talk`, `identity`, `wizard` (dynamic reads)
|
||||||
|
|
@ -1701,7 +1720,7 @@ Requires full Gateway restart:
|
||||||
To run multiple gateways on one host, isolate per-instance state + config and use unique ports:
|
To run multiple gateways on one host, isolate per-instance state + config and use unique ports:
|
||||||
- `CLAWDBOT_CONFIG_PATH` (per-instance config)
|
- `CLAWDBOT_CONFIG_PATH` (per-instance config)
|
||||||
- `CLAWDBOT_STATE_DIR` (sessions/creds)
|
- `CLAWDBOT_STATE_DIR` (sessions/creds)
|
||||||
- `agent.workspace` (memories)
|
- `agents.defaults.workspace` (memories)
|
||||||
- `gateway.port` (unique per instance)
|
- `gateway.port` (unique per instance)
|
||||||
|
|
||||||
Convenience flags (CLI):
|
Convenience flags (CLI):
|
||||||
|
|
@ -1771,7 +1790,7 @@ Mapping notes:
|
||||||
- `transform` can point to a JS/TS module that returns a hook action.
|
- `transform` can point to a JS/TS module that returns a hook action.
|
||||||
- `deliver: true` sends the final reply to a provider; `provider` defaults to `last` (falls back to WhatsApp).
|
- `deliver: true` sends the final reply to a provider; `provider` defaults to `last` (falls back to WhatsApp).
|
||||||
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage).
|
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage).
|
||||||
- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agent.models` is set).
|
- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set).
|
||||||
|
|
||||||
Gmail helper config (used by `clawdbot hooks gmail setup` / `run`):
|
Gmail helper config (used by `clawdbot hooks gmail setup` / `run`):
|
||||||
|
|
||||||
|
|
@ -1886,7 +1905,7 @@ clawdbot dns setup --apply
|
||||||
|
|
||||||
## Template variables
|
## Template variables
|
||||||
|
|
||||||
Template placeholders are expanded in `routing.transcribeAudio.command` (and any future templated command fields).
|
Template placeholders are expanded in `audio.transcription.command` (and any future templated command fields).
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,18 @@ legacy config format, so stale configs are repaired without manual intervention.
|
||||||
|
|
||||||
Current migrations:
|
Current migrations:
|
||||||
- `routing.allowFrom` → `whatsapp.allowFrom`
|
- `routing.allowFrom` → `whatsapp.allowFrom`
|
||||||
|
- `routing.groupChat.requireMention` → `whatsapp/telegram/imessage.groups."*".requireMention`
|
||||||
|
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
||||||
|
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
||||||
|
- `routing.queue` → `messages.queue`
|
||||||
|
- `routing.bindings` → top-level `bindings`
|
||||||
|
- `routing.agents`/`routing.defaultAgentId` → `agents.list` + `agents.list[].default`
|
||||||
|
- `routing.agentToAgent` → `tools.agentToAgent`
|
||||||
|
- `routing.transcribeAudio` → `audio.transcription`
|
||||||
|
- `identity` → `agents.list[].identity`
|
||||||
|
- `agent.*` → `agents.defaults` + `tools.*` (tools/elevated/bash/sandbox/subagents)
|
||||||
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
|
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
|
||||||
→ `agent.models` + `agent.model.primary/fallbacks` + `agent.imageModel.primary/fallbacks`
|
→ `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
|
||||||
|
|
||||||
### 3) Legacy state migrations (disk layout)
|
### 3) Legacy state migrations (disk layout)
|
||||||
Doctor can migrate older on-disk layouts into the current structure:
|
Doctor can migrate older on-disk layouts into the current structure:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Short guide to verify the WhatsApp Web / Baileys stack without guessing.
|
||||||
## When something fails
|
## When something fails
|
||||||
- `logged out` or status 409–515 → relink with `clawdbot providers logout` then `clawdbot providers login`.
|
- `logged out` or status 409–515 → relink with `clawdbot providers logout` then `clawdbot providers login`.
|
||||||
- Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy).
|
- Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy).
|
||||||
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`whatsapp.groups`, `routing.groupChat.mentionPatterns`).
|
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`).
|
||||||
|
|
||||||
## Dedicated "health" command
|
## Dedicated "health" command
|
||||||
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct Baileys socket from the CLI). It reports linked creds, auth age, Baileys connect result/status code, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.
|
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct Baileys socket from the CLI). It reports linked creds, auth age, Baileys connect result/status code, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ surface anything that needs attention without spamming you.
|
||||||
|
|
||||||
## Defaults
|
## Defaults
|
||||||
|
|
||||||
- Interval: `30m` (set `agent.heartbeat.every`; use `0m` to disable).
|
- Interval: `30m` (set `agents.defaults.heartbeat.every`; use `0m` to disable).
|
||||||
- Prompt body (configurable via `agent.heartbeat.prompt`):
|
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
|
||||||
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
|
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
|
||||||
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
||||||
prompt includes a “Heartbeat” section and the run is flagged internally.
|
prompt includes a “Heartbeat” section and the run is flagged internally.
|
||||||
|
|
@ -33,14 +33,16 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
heartbeat: {
|
defaults: {
|
||||||
every: "30m", // default: 30m (0m disables)
|
heartbeat: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
every: "30m", // default: 30m (0m disables)
|
||||||
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
model: "anthropic/claude-opus-4-5",
|
||||||
to: "+15551234567", // optional provider-specific override
|
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
||||||
prompt: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.",
|
to: "+15551234567", // optional provider-specific override
|
||||||
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
|
prompt: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.",
|
||||||
|
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ Defaults (can be overridden via env/flags/config):
|
||||||
- `bridge.port=19002` (derived: `gateway.port+1`)
|
- `bridge.port=19002` (derived: `gateway.port+1`)
|
||||||
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
|
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
|
||||||
- `canvasHost.port=19005` (derived: `gateway.port+4`)
|
- `canvasHost.port=19005` (derived: `gateway.port+4`)
|
||||||
- `agent.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
|
- `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
|
||||||
|
|
||||||
Derived ports (rules of thumb):
|
Derived ports (rules of thumb):
|
||||||
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
|
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
|
||||||
|
|
@ -81,7 +81,7 @@ Checklist per instance:
|
||||||
- unique `gateway.port`
|
- unique `gateway.port`
|
||||||
- unique `CLAWDBOT_CONFIG_PATH`
|
- unique `CLAWDBOT_CONFIG_PATH`
|
||||||
- unique `CLAWDBOT_STATE_DIR`
|
- unique `CLAWDBOT_STATE_DIR`
|
||||||
- unique `agent.workspace`
|
- unique `agents.defaults.workspace`
|
||||||
- separate WhatsApp numbers (if using WA)
|
- separate WhatsApp numbers (if using WA)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
summary: "How Clawdbot sandboxing works: modes, scopes, workspace access, and images"
|
summary: "How Clawdbot sandboxing works: modes, scopes, workspace access, and images"
|
||||||
title: Sandboxing
|
title: Sandboxing
|
||||||
read_when: "You want a dedicated explanation of sandboxing or need to tune agent.sandbox."
|
read_when: "You want a dedicated explanation of sandboxing or need to tune agents.defaults.sandbox."
|
||||||
status: active
|
status: active
|
||||||
---
|
---
|
||||||
|
|
||||||
# Sandboxing
|
# Sandboxing
|
||||||
|
|
||||||
Clawdbot can run **tools inside Docker containers** to reduce blast radius.
|
Clawdbot can run **tools inside Docker containers** to reduce blast radius.
|
||||||
This is **optional** and controlled by configuration (`agent.sandbox` or
|
This is **optional** and controlled by configuration (`agents.defaults.sandbox` or
|
||||||
`routing.agents[id].sandbox`). If sandboxing is off, tools run on the host.
|
`agents.list[].sandbox`). If sandboxing is off, tools run on the host.
|
||||||
The Gateway stays on the host; tool execution runs in an isolated sandbox
|
The Gateway stays on the host; tool execution runs in an isolated sandbox
|
||||||
when enabled.
|
when enabled.
|
||||||
|
|
||||||
|
|
@ -18,16 +18,16 @@ and process access when the model does something dumb.
|
||||||
|
|
||||||
## What gets sandboxed
|
## What gets sandboxed
|
||||||
- Tool execution (`bash`, `read`, `write`, `edit`, `process`, etc.).
|
- Tool execution (`bash`, `read`, `write`, `edit`, `process`, etc.).
|
||||||
- Optional sandboxed browser (`agent.sandbox.browser`).
|
- Optional sandboxed browser (`agents.defaults.sandbox.browser`).
|
||||||
|
|
||||||
Not sandboxed:
|
Not sandboxed:
|
||||||
- The Gateway process itself.
|
- The Gateway process itself.
|
||||||
- Any tool explicitly allowed to run on the host (e.g. `agent.elevated`).
|
- Any tool explicitly allowed to run on the host (e.g. `tools.elevated`).
|
||||||
- **Elevated bash runs on the host and bypasses sandboxing.**
|
- **Elevated bash runs on the host and bypasses sandboxing.**
|
||||||
- If sandboxing is off, `agent.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated).
|
- If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated).
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
`agent.sandbox.mode` controls **when** sandboxing is used:
|
`agents.defaults.sandbox.mode` controls **when** sandboxing is used:
|
||||||
- `"off"`: no sandboxing.
|
- `"off"`: no sandboxing.
|
||||||
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
|
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
|
||||||
- `"all"`: every session runs in a sandbox.
|
- `"all"`: every session runs in a sandbox.
|
||||||
|
|
@ -35,13 +35,13 @@ Note: `"non-main"` is based on `session.mainKey` (default `"main"`), not agent i
|
||||||
Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
|
Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
`agent.sandbox.scope` controls **how many containers** are created:
|
`agents.defaults.sandbox.scope` controls **how many containers** are created:
|
||||||
- `"session"` (default): one container per session.
|
- `"session"` (default): one container per session.
|
||||||
- `"agent"`: one container per agent.
|
- `"agent"`: one container per agent.
|
||||||
- `"shared"`: one container shared by all sandboxed sessions.
|
- `"shared"`: one container shared by all sandboxed sessions.
|
||||||
|
|
||||||
## Workspace access
|
## Workspace access
|
||||||
`agent.sandbox.workspaceAccess` controls **what the sandbox can see**:
|
`agents.defaults.sandbox.workspaceAccess` controls **what the sandbox can see**:
|
||||||
- `"none"` (default): tools see a sandbox workspace under `~/.clawdbot/sandboxes`.
|
- `"none"` (default): tools see a sandbox workspace under `~/.clawdbot/sandboxes`.
|
||||||
- `"ro"`: mounts the agent workspace read-only at `/agent` (disables `write`/`edit`).
|
- `"ro"`: mounts the agent workspace read-only at `/agent` (disables `write`/`edit`).
|
||||||
- `"rw"`: mounts the agent workspace read/write at `/workspace`.
|
- `"rw"`: mounts the agent workspace read/write at `/workspace`.
|
||||||
|
|
@ -66,7 +66,7 @@ scripts/sandbox-browser-setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, sandbox containers run with **no network**.
|
By default, sandbox containers run with **no network**.
|
||||||
Override with `agent.sandbox.docker.network`.
|
Override with `agents.defaults.sandbox.docker.network`.
|
||||||
|
|
||||||
Docker installs and the containerized gateway live here:
|
Docker installs and the containerized gateway live here:
|
||||||
[Docker](/install/docker)
|
[Docker](/install/docker)
|
||||||
|
|
@ -75,28 +75,30 @@ Docker installs and the containerized gateway live here:
|
||||||
Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
||||||
globally or per-agent, sandboxing doesn’t bring it back.
|
globally or per-agent, sandboxing doesn’t bring it back.
|
||||||
|
|
||||||
`agent.elevated` is an explicit escape hatch that runs `bash` on the host.
|
`tools.elevated` is an explicit escape hatch that runs `bash` on the host.
|
||||||
Keep it locked down.
|
Keep it locked down.
|
||||||
|
|
||||||
## Multi-agent overrides
|
## Multi-agent overrides
|
||||||
Each agent can override sandbox + tools:
|
Each agent can override sandbox + tools:
|
||||||
`routing.agents[id].sandbox` and `routing.agents[id].tools`.
|
`agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools` for sandbox tool policy).
|
||||||
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for precedence.
|
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for precedence.
|
||||||
|
|
||||||
## Minimal enable example
|
## Minimal enable example
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "non-main",
|
sandbox: {
|
||||||
scope: "session",
|
mode: "non-main",
|
||||||
workspaceAccess: "none"
|
scope: "session",
|
||||||
|
workspaceAccess: "none"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Related docs
|
## Related docs
|
||||||
- [Sandbox Configuration](/gateway/configuration#agent-sandbox)
|
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||||
- [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools)
|
- [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools)
|
||||||
- [Security](/gateway/security)
|
- [Security](/gateway/security)
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,13 @@ Keep config + state private on the gateway host:
|
||||||
"*": { "requireMention": true }
|
"*": { "requireMention": true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"agents": {
|
||||||
"groupChat": {
|
"list": [
|
||||||
"mentionPatterns": ["@clawd", "@mybot"]
|
{
|
||||||
}
|
"id": "main",
|
||||||
|
"groupChat": { "mentionPatterns": ["@clawd", "@mybot"] }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -146,7 +149,7 @@ Consider running your AI on a separate phone number from your personal one:
|
||||||
### 4. Read-Only Mode (Today, via sandbox + tools)
|
### 4. Read-Only Mode (Today, via sandbox + tools)
|
||||||
|
|
||||||
You can already build a read-only profile by combining:
|
You can already build a read-only profile by combining:
|
||||||
- `sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access)
|
- `agents.defaults.sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access)
|
||||||
- tool allow/deny lists that block `write`, `edit`, `bash`, `process`, etc.
|
- tool allow/deny lists that block `write`, `edit`, `bash`, `process`, etc.
|
||||||
|
|
||||||
We may add a single `readOnlyMode` flag later to simplify this configuration.
|
We may add a single `readOnlyMode` flag later to simplify this configuration.
|
||||||
|
|
@ -158,18 +161,18 @@ Dedicated doc: [Sandboxing](/gateway/sandboxing)
|
||||||
Two complementary approaches:
|
Two complementary approaches:
|
||||||
|
|
||||||
- **Run the full Gateway in Docker** (container boundary): [Docker](/install/docker)
|
- **Run the full Gateway in Docker** (container boundary): [Docker](/install/docker)
|
||||||
- **Tool sandbox** (`agent.sandbox`, host gateway + Docker-isolated tools): [Sandboxing](/gateway/sandboxing)
|
- **Tool sandbox** (`agents.defaults.sandbox`, host gateway + Docker-isolated tools): [Sandboxing](/gateway/sandboxing)
|
||||||
|
|
||||||
Note: to prevent cross-agent access, keep `sandbox.scope` at `"agent"` (default)
|
Note: to prevent cross-agent access, keep `agents.defaults.sandbox.scope` at `"agent"` (default)
|
||||||
or `"session"` for stricter per-session isolation. `scope: "shared"` uses a
|
or `"session"` for stricter per-session isolation. `scope: "shared"` uses a
|
||||||
single container/workspace.
|
single container/workspace.
|
||||||
|
|
||||||
Also consider agent workspace access inside the sandbox:
|
Also consider agent workspace access inside the sandbox:
|
||||||
- `agent.sandbox.workspaceAccess: "none"` (default) keeps the agent workspace off-limits; tools run against a sandbox workspace under `~/.clawdbot/sandboxes`
|
- `agents.defaults.sandbox.workspaceAccess: "none"` (default) keeps the agent workspace off-limits; tools run against a sandbox workspace under `~/.clawdbot/sandboxes`
|
||||||
- `workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
- `agents.defaults.sandbox.workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||||||
- `workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace`
|
- `agents.defaults.sandbox.workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace`
|
||||||
|
|
||||||
Important: `agent.elevated` is a **global**, sender-based escape hatch that runs bash on the host. Keep `agent.elevated.allowFrom` tight and don’t enable it for strangers. See [Elevated Mode](/tools/elevated).
|
Important: `tools.elevated` is a **global**, sender-based escape hatch that runs bash on the host. Keep `tools.elevated.allowFrom` tight and don’t enable it for strangers. See [Elevated Mode](/tools/elevated).
|
||||||
|
|
||||||
## Per-agent access profiles (multi-agent)
|
## Per-agent access profiles (multi-agent)
|
||||||
|
|
||||||
|
|
@ -187,13 +190,14 @@ Common use cases:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
personal: {
|
{
|
||||||
|
id: "personal",
|
||||||
workspace: "~/clawd-personal",
|
workspace: "~/clawd-personal",
|
||||||
sandbox: { mode: "off" }
|
sandbox: { mode: "off" }
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -202,9 +206,10 @@ Common use cases:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -216,7 +221,7 @@ Common use cases:
|
||||||
deny: ["write", "edit", "bash", "process", "browser"]
|
deny: ["write", "edit", "bash", "process", "browser"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -225,9 +230,10 @@ Common use cases:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
public: {
|
{
|
||||||
|
id: "public",
|
||||||
workspace: "~/clawd-public",
|
workspace: "~/clawd-public",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -239,7 +245,7 @@ Common use cases:
|
||||||
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,12 @@ or state drift because only one workspace is active.
|
||||||
Symptoms: `pwd` or file tools show `~/.clawdbot/sandboxes/...` even though you
|
Symptoms: `pwd` or file tools show `~/.clawdbot/sandboxes/...` even though you
|
||||||
expected the host workspace.
|
expected the host workspace.
|
||||||
|
|
||||||
**Why:** `agent.sandbox.mode: "non-main"` keys off `session.mainKey` (default `"main"`).
|
**Why:** `agents.defaults.sandbox.mode: "non-main"` keys off `session.mainKey` (default `"main"`).
|
||||||
Group/channel sessions use their own keys, so they are treated as non-main and
|
Group/channel sessions use their own keys, so they are treated as non-main and
|
||||||
get sandbox workspaces.
|
get sandbox workspaces.
|
||||||
|
|
||||||
**Fix options:**
|
**Fix options:**
|
||||||
- If you want host workspaces for an agent: set `routing.agents.<id>.sandbox.mode: "off"`.
|
- If you want host workspaces for an agent: set `agents.list[].sandbox.mode: "off"`.
|
||||||
- If you want host workspace access inside sandbox: set `workspaceAccess: "rw"` for that agent.
|
- If you want host workspace access inside sandbox: set `workspaceAccess: "rw"` for that agent.
|
||||||
|
|
||||||
### "Agent was aborted"
|
### "Agent was aborted"
|
||||||
|
|
@ -157,8 +157,8 @@ Look for `AllowFrom: ...` in the output.
|
||||||
**Check 2:** For group chats, is mention required?
|
**Check 2:** For group chats, is mention required?
|
||||||
```bash
|
```bash
|
||||||
# The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds.
|
# The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds.
|
||||||
# Multi-agent: `routing.agents.<agentId>.mentionPatterns` overrides global patterns.
|
# Multi-agent: `agents.list[].groupChat.mentionPatterns` overrides global patterns.
|
||||||
grep -n "routing\\|groupChat\\|mentionPatterns\\|whatsapp\\.groups\\|telegram\\.groups\\|imessage\\.groups\\|discord\\.guilds" \
|
grep -n "agents\\|groupChat\\|mentionPatterns\\|whatsapp\\.groups\\|telegram\\.groups\\|imessage\\.groups\\|discord\\.guilds" \
|
||||||
"${CLAWDBOT_CONFIG_PATH:-$HOME/.clawdbot/clawdbot.json}"
|
"${CLAWDBOT_CONFIG_PATH:-$HOME/.clawdbot/clawdbot.json}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,12 @@ Deep dive: [Sandboxing](/gateway/sandboxing)
|
||||||
|
|
||||||
### What it does
|
### What it does
|
||||||
|
|
||||||
When `agent.sandbox` is enabled, **non-main sessions** run tools inside a Docker
|
When `agents.defaults.sandbox` is enabled, **non-main sessions** run tools inside a Docker
|
||||||
container. The gateway stays on your host, but the tool execution is isolated:
|
container. The gateway stays on your host, but the tool execution is isolated:
|
||||||
- scope: `"agent"` by default (one container + workspace per agent)
|
- scope: `"agent"` by default (one container + workspace per agent)
|
||||||
- scope: `"session"` for per-session isolation
|
- scope: `"session"` for per-session isolation
|
||||||
- per-scope workspace folder mounted at `/workspace`
|
- per-scope workspace folder mounted at `/workspace`
|
||||||
- optional agent workspace access (`agent.sandbox.workspaceAccess`)
|
- optional agent workspace access (`agents.defaults.sandbox.workspaceAccess`)
|
||||||
- allow/deny tool policy (deny wins)
|
- allow/deny tool policy (deny wins)
|
||||||
- inbound media is copied into the active sandbox workspace (`media/inbound/*`) so tools can read it (with `workspaceAccess: "rw"`, this lands in the agent workspace)
|
- inbound media is copied into the active sandbox workspace (`media/inbound/*`) so tools can read it (with `workspaceAccess: "rw"`, this lands in the agent workspace)
|
||||||
|
|
||||||
|
|
@ -124,7 +124,7 @@ one container and one workspace.
|
||||||
### Per-agent sandbox profiles (multi-agent)
|
### Per-agent sandbox profiles (multi-agent)
|
||||||
|
|
||||||
If you use multi-agent routing, each agent can override sandbox + tool settings:
|
If you use multi-agent routing, each agent can override sandbox + tool settings:
|
||||||
`routing.agents[id].sandbox` and `routing.agents[id].tools`. This lets you run
|
`agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools`). This lets you run
|
||||||
mixed access levels in one gateway:
|
mixed access levels in one gateway:
|
||||||
- Full access (personal agent)
|
- Full access (personal agent)
|
||||||
- Read-only tools + read-only workspace (family/work agent)
|
- Read-only tools + read-only workspace (family/work agent)
|
||||||
|
|
@ -149,54 +149,60 @@ precedence, and troubleshooting.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "non-main", // off | non-main | all
|
sandbox: {
|
||||||
scope: "agent", // session | agent | shared (agent is default)
|
mode: "non-main", // off | non-main | all
|
||||||
workspaceAccess: "none", // none | ro | rw
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceAccess: "none", // none | ro | rw
|
||||||
docker: {
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
docker: {
|
||||||
workdir: "/workspace",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
readOnlyRoot: true,
|
workdir: "/workspace",
|
||||||
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
readOnlyRoot: true,
|
||||||
network: "none",
|
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
||||||
user: "1000:1000",
|
network: "none",
|
||||||
capDrop: ["ALL"],
|
user: "1000:1000",
|
||||||
env: { LANG: "C.UTF-8" },
|
capDrop: ["ALL"],
|
||||||
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
env: { LANG: "C.UTF-8" },
|
||||||
pidsLimit: 256,
|
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
||||||
memory: "1g",
|
pidsLimit: 256,
|
||||||
memorySwap: "2g",
|
memory: "1g",
|
||||||
cpus: 1,
|
memorySwap: "2g",
|
||||||
ulimits: {
|
cpus: 1,
|
||||||
nofile: { soft: 1024, hard: 2048 },
|
ulimits: {
|
||||||
nproc: 256
|
nofile: { soft: 1024, hard: 2048 },
|
||||||
|
nproc: 256
|
||||||
|
},
|
||||||
|
seccompProfile: "/path/to/seccomp.json",
|
||||||
|
apparmorProfile: "clawdbot-sandbox",
|
||||||
|
dns: ["1.1.1.1", "8.8.8.8"],
|
||||||
|
extraHosts: ["internal.service:10.0.0.5"]
|
||||||
},
|
},
|
||||||
seccompProfile: "/path/to/seccomp.json",
|
prune: {
|
||||||
apparmorProfile: "clawdbot-sandbox",
|
idleHours: 24, // 0 disables idle pruning
|
||||||
dns: ["1.1.1.1", "8.8.8.8"],
|
maxAgeDays: 7 // 0 disables max-age pruning
|
||||||
extraHosts: ["internal.service:10.0.0.5"]
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: {
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
|
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
|
||||||
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
||||||
},
|
|
||||||
prune: {
|
|
||||||
idleHours: 24, // 0 disables idle pruning
|
|
||||||
maxAgeDays: 7 // 0 disables max-age pruning
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Hardening knobs live under `agent.sandbox.docker`:
|
Hardening knobs live under `agents.defaults.sandbox.docker`:
|
||||||
`network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`,
|
`network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`,
|
||||||
`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`.
|
`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`.
|
||||||
|
|
||||||
Multi-agent: override `agent.sandbox.{docker,browser,prune}.*` per agent via `routing.agents.<agentId>.sandbox.{docker,browser,prune}.*`
|
Multi-agent: override `agents.defaults.sandbox.{docker,browser,prune}.*` per agent via `agents.list[].sandbox.{docker,browser,prune}.*`
|
||||||
(ignored when `agent.sandbox.scope` / `routing.agents.<agentId>.sandbox.scope` is `"shared"`).
|
(ignored when `agents.defaults.sandbox.scope` / `agents.list[].sandbox.scope` is `"shared"`).
|
||||||
|
|
||||||
### Build the default sandbox image
|
### Build the default sandbox image
|
||||||
|
|
||||||
|
|
@ -217,7 +223,7 @@ This builds `clawdbot-sandbox-common:bookworm-slim`. To use it:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: { sandbox: { docker: { image: "clawdbot-sandbox-common:bookworm-slim" } } }
|
agents: { defaults: { sandbox: { docker: { image: "clawdbot-sandbox-common:bookworm-slim" } } } }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -235,16 +241,18 @@ an optional noVNC observer (headful via Xvfb).
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Headful (Xvfb) reduces bot blocking vs headless.
|
- Headful (Xvfb) reduces bot blocking vs headless.
|
||||||
- Headless can still be used by setting `agent.sandbox.browser.headless=true`.
|
- Headless can still be used by setting `agents.defaults.sandbox.browser.headless=true`.
|
||||||
- No full desktop environment (GNOME) is needed; Xvfb provides the display.
|
- No full desktop environment (GNOME) is needed; Xvfb provides the display.
|
||||||
|
|
||||||
Use config:
|
Use config:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
browser: { enabled: true }
|
sandbox: {
|
||||||
|
browser: { enabled: true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,8 +262,10 @@ Custom browser image:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: { browser: { image: "my-clawdbot-browser" } }
|
defaults: {
|
||||||
|
sandbox: { browser: { image: "my-clawdbot-browser" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -266,7 +276,7 @@ When enabled, the agent receives:
|
||||||
|
|
||||||
Remember: if you use an allowlist for tools, add `browser` (and remove it from
|
Remember: if you use an allowlist for tools, add `browser` (and remove it from
|
||||||
deny) or the tool remains blocked.
|
deny) or the tool remains blocked.
|
||||||
Prune rules (`agent.sandbox.prune`) apply to browser containers too.
|
Prune rules (`agents.defaults.sandbox.prune`) apply to browser containers too.
|
||||||
|
|
||||||
### Custom sandbox image
|
### Custom sandbox image
|
||||||
|
|
||||||
|
|
@ -278,8 +288,10 @@ docker build -t my-clawdbot-sbx -f Dockerfile.sandbox .
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: { docker: { image: "my-clawdbot-sbx" } }
|
defaults: {
|
||||||
|
sandbox: { docker: { image: "my-clawdbot-sbx" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -310,7 +322,7 @@ Example:
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- Image missing: build with [`scripts/sandbox-setup.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/sandbox-setup.sh) or set `agent.sandbox.docker.image`.
|
- Image missing: build with [`scripts/sandbox-setup.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/sandbox-setup.sh) or set `agents.defaults.sandbox.docker.image`.
|
||||||
- Container not running: it will auto-create per session on demand.
|
- Container not running: it will auto-create per session on demand.
|
||||||
- Permission errors in sandbox: set `docker.user` to a UID:GID that matches your
|
- Permission errors in sandbox: set `docker.user` to a UID:GID that matches your
|
||||||
mounted workspace ownership (or chown the workspace folder).
|
mounted workspace ownership (or chown the workspace folder).
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ status: active
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Each agent in a multi-agent setup can now have its own:
|
Each agent in a multi-agent setup can now have its own:
|
||||||
- **Sandbox configuration** (`mode`, `scope`, `workspaceRoot`, `workspaceAccess`, `tools`)
|
- **Sandbox configuration** (`agents.list[].sandbox` overrides `agents.defaults.sandbox`)
|
||||||
- **Tool restrictions** (`allow`, `deny`)
|
- **Tool restrictions** (`tools.allow` / `tools.deny`, plus `agents.list[].tools`)
|
||||||
|
|
||||||
This allows you to run multiple agents with different security profiles:
|
This allows you to run multiple agents with different security profiles:
|
||||||
- Personal assistant with full access
|
- Personal assistant with full access
|
||||||
|
|
@ -28,18 +28,17 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"routing": {
|
"agents": {
|
||||||
"defaultAgentId": "main",
|
"list": [
|
||||||
"agents": {
|
{
|
||||||
"main": {
|
"id": "main",
|
||||||
|
"default": true,
|
||||||
"name": "Personal Assistant",
|
"name": "Personal Assistant",
|
||||||
"workspace": "~/clawd",
|
"workspace": "~/clawd",
|
||||||
"sandbox": {
|
"sandbox": { "mode": "off" }
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
// No tool restrictions - all tools available
|
|
||||||
},
|
},
|
||||||
"family": {
|
{
|
||||||
|
"id": "family",
|
||||||
"name": "Family Bot",
|
"name": "Family Bot",
|
||||||
"workspace": "~/clawd-family",
|
"workspace": "~/clawd-family",
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
|
|
@ -51,21 +50,21 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
"deny": ["bash", "write", "edit", "process", "browser"]
|
"deny": ["bash", "write", "edit", "process", "browser"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"bindings": [
|
},
|
||||||
{
|
"bindings": [
|
||||||
"agentId": "family",
|
{
|
||||||
"match": {
|
"agentId": "family",
|
||||||
"provider": "whatsapp",
|
"match": {
|
||||||
"accountId": "*",
|
"provider": "whatsapp",
|
||||||
"peer": {
|
"accountId": "*",
|
||||||
"kind": "group",
|
"peer": {
|
||||||
"id": "120363424282127706@g.us"
|
"kind": "group",
|
||||||
}
|
"id": "120363424282127706@g.us"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -79,13 +78,15 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"routing": {
|
"agents": {
|
||||||
"agents": {
|
"list": [
|
||||||
"personal": {
|
{
|
||||||
|
"id": "personal",
|
||||||
"workspace": "~/clawd-personal",
|
"workspace": "~/clawd-personal",
|
||||||
"sandbox": { "mode": "off" }
|
"sandbox": { "mode": "off" }
|
||||||
},
|
},
|
||||||
"work": {
|
{
|
||||||
|
"id": "work",
|
||||||
"workspace": "~/clawd-work",
|
"workspace": "~/clawd-work",
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"mode": "all",
|
"mode": "all",
|
||||||
|
|
@ -97,7 +98,7 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
"deny": ["browser", "gateway", "discord"]
|
"deny": ["browser", "gateway", "discord"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -108,21 +109,23 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agents": {
|
||||||
"sandbox": {
|
"defaults": {
|
||||||
"mode": "non-main", // Global default
|
"sandbox": {
|
||||||
"scope": "session"
|
"mode": "non-main", // Global default
|
||||||
}
|
"scope": "session"
|
||||||
},
|
}
|
||||||
"routing": {
|
},
|
||||||
"agents": {
|
"list": [
|
||||||
"main": {
|
{
|
||||||
|
"id": "main",
|
||||||
"workspace": "~/clawd",
|
"workspace": "~/clawd",
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"mode": "off" // Override: main never sandboxed
|
"mode": "off" // Override: main never sandboxed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"public": {
|
{
|
||||||
|
"id": "public",
|
||||||
"workspace": "~/clawd-public",
|
"workspace": "~/clawd-public",
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"mode": "all", // Override: public always sandboxed
|
"mode": "all", // Override: public always sandboxed
|
||||||
|
|
@ -133,7 +136,7 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
"deny": ["bash", "write", "edit"]
|
"deny": ["bash", "write", "edit"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -142,40 +145,40 @@ For how sandboxing behaves at runtime, see [Sandboxing](/gateway/sandboxing).
|
||||||
|
|
||||||
## Configuration Precedence
|
## Configuration Precedence
|
||||||
|
|
||||||
When both global (`agent.*`) and agent-specific (`routing.agents[id].*`) configs exist:
|
When both global (`agents.defaults.*`) and agent-specific (`agents.list[].*`) configs exist:
|
||||||
|
|
||||||
### Sandbox Config
|
### Sandbox Config
|
||||||
Agent-specific settings override global:
|
Agent-specific settings override global:
|
||||||
```
|
```
|
||||||
routing.agents[id].sandbox.mode > agent.sandbox.mode
|
agents.list[].sandbox.mode > agents.defaults.sandbox.mode
|
||||||
routing.agents[id].sandbox.scope > agent.sandbox.scope
|
agents.list[].sandbox.scope > agents.defaults.sandbox.scope
|
||||||
routing.agents[id].sandbox.workspaceRoot > agent.sandbox.workspaceRoot
|
agents.list[].sandbox.workspaceRoot > agents.defaults.sandbox.workspaceRoot
|
||||||
routing.agents[id].sandbox.workspaceAccess > agent.sandbox.workspaceAccess
|
agents.list[].sandbox.workspaceAccess > agents.defaults.sandbox.workspaceAccess
|
||||||
routing.agents[id].sandbox.docker.* > agent.sandbox.docker.*
|
agents.list[].sandbox.docker.* > agents.defaults.sandbox.docker.*
|
||||||
routing.agents[id].sandbox.browser.* > agent.sandbox.browser.*
|
agents.list[].sandbox.browser.* > agents.defaults.sandbox.browser.*
|
||||||
routing.agents[id].sandbox.prune.* > agent.sandbox.prune.*
|
agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.*
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
- `routing.agents[id].sandbox.{docker,browser,prune}.*` overrides `agent.sandbox.{docker,browser,prune}.*` for that agent (ignored when sandbox scope resolves to `"shared"`).
|
- `agents.list[].sandbox.{docker,browser,prune}.*` overrides `agents.defaults.sandbox.{docker,browser,prune}.*` for that agent (ignored when sandbox scope resolves to `"shared"`).
|
||||||
|
|
||||||
### Tool Restrictions
|
### Tool Restrictions
|
||||||
The filtering order is:
|
The filtering order is:
|
||||||
1. **Global tool policy** (`agent.tools`)
|
1. **Global tool policy** (`tools.allow` / `tools.deny`)
|
||||||
2. **Agent-specific tool policy** (`routing.agents[id].tools`)
|
2. **Agent-specific tool policy** (`agents.list[].tools`)
|
||||||
3. **Sandbox tool policy** (`agent.sandbox.tools` or `routing.agents[id].sandbox.tools`)
|
3. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`)
|
||||||
4. **Subagent tool policy** (if applicable)
|
4. **Subagent tool policy** (`tools.subagents.tools`, if applicable)
|
||||||
|
|
||||||
Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
|
Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
|
||||||
If `routing.agents[id].sandbox.tools` is set, it replaces `agent.sandbox.tools` for that agent.
|
If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
|
||||||
|
|
||||||
### Elevated Mode (global)
|
### Elevated Mode (global)
|
||||||
`agent.elevated` is **global** and **sender-based** (per-provider allowlist). It is **not** configurable per agent.
|
`tools.elevated` is **global** and **sender-based** (per-provider allowlist). It is **not** configurable per agent.
|
||||||
|
|
||||||
Mitigation patterns:
|
Mitigation patterns:
|
||||||
- Deny `bash` for untrusted agents (`routing.agents[id].tools.deny: ["bash"]`)
|
- Deny `bash` for untrusted agents (`agents.list[].tools.deny: ["bash"]`)
|
||||||
- Avoid allowlisting senders that route to restricted agents
|
- Avoid allowlisting senders that route to restricted agents
|
||||||
- Disable elevated globally (`agent.elevated.enabled: false`) if you only want sandboxed execution
|
- Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -184,10 +187,16 @@ Mitigation patterns:
|
||||||
**Before (single agent):**
|
**Before (single agent):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agents": {
|
||||||
"workspace": "~/clawd",
|
"defaults": {
|
||||||
|
"workspace": "~/clawd",
|
||||||
|
"sandbox": {
|
||||||
|
"mode": "non-main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"mode": "non-main",
|
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read", "write", "bash"],
|
"allow": ["read", "write", "bash"],
|
||||||
"deny": []
|
"deny": []
|
||||||
|
|
@ -200,21 +209,20 @@ Mitigation patterns:
|
||||||
**After (multi-agent with different profiles):**
|
**After (multi-agent with different profiles):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"routing": {
|
"agents": {
|
||||||
"defaultAgentId": "main",
|
"list": [
|
||||||
"agents": {
|
{
|
||||||
"main": {
|
"id": "main",
|
||||||
|
"default": true,
|
||||||
"workspace": "~/clawd",
|
"workspace": "~/clawd",
|
||||||
"sandbox": {
|
"sandbox": { "mode": "off" }
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The global `agent.workspace` and `agent.sandbox` are still supported for backward compatibility, but we recommend using `routing.agents` for clarity in multi-agent setups.
|
Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defaults` + `agents.list` going forward.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -254,10 +262,10 @@ The global `agent.workspace` and `agent.sandbox` are still supported for backwar
|
||||||
|
|
||||||
## Common Pitfall: "non-main"
|
## Common Pitfall: "non-main"
|
||||||
|
|
||||||
`sandbox.mode: "non-main"` is based on `session.mainKey` (default `"main"`),
|
`agents.defaults.sandbox.mode: "non-main"` is based on `session.mainKey` (default `"main"`),
|
||||||
not the agent id. Group/channel sessions always get their own keys, so they
|
not the agent id. Group/channel sessions always get their own keys, so they
|
||||||
are treated as non-main and will be sandboxed. If you want an agent to never
|
are treated as non-main and will be sandboxed. If you want an agent to never
|
||||||
sandbox, set `routing.agents.<id>.sandbox.mode: "off"`.
|
sandbox, set `agents.list[].sandbox.mode: "off"`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -289,8 +297,8 @@ After configuring multi-agent sandbox and tools:
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Agent not sandboxed despite `mode: "all"`
|
### Agent not sandboxed despite `mode: "all"`
|
||||||
- Check if there's a global `agent.sandbox.mode` that overrides it
|
- Check if there's a global `agents.defaults.sandbox.mode` that overrides it
|
||||||
- Agent-specific config takes precedence, so set `routing.agents[id].sandbox.mode: "all"`
|
- Agent-specific config takes precedence, so set `agents.list[].sandbox.mode: "all"`
|
||||||
|
|
||||||
### Tools still available despite deny list
|
### Tools still available despite deny list
|
||||||
- Check tool filtering order: global → agent → sandbox → subagent
|
- Check tool filtering order: global → agent → sandbox → subagent
|
||||||
|
|
@ -306,5 +314,5 @@ After configuring multi-agent sandbox and tools:
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [Multi-Agent Routing](/concepts/multi-agent)
|
- [Multi-Agent Routing](/concepts/multi-agent)
|
||||||
- [Sandbox Configuration](/gateway/configuration#agent-sandbox)
|
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||||
- [Session Management](/concepts/session)
|
- [Session Management](/concepts/session)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ read_when:
|
||||||
# Audio / Voice Notes — 2025-12-05
|
# Audio / Voice Notes — 2025-12-05
|
||||||
|
|
||||||
## What works
|
## What works
|
||||||
- **Optional transcription**: If `routing.transcribeAudio.command` is set in `~/.clawdbot/clawdbot.json`, CLAWDBOT will:
|
- **Optional transcription**: If `audio.transcription.command` is set in `~/.clawdbot/clawdbot.json`, CLAWDBOT will:
|
||||||
1) Download inbound audio to a temp path when WhatsApp only provides a URL.
|
1) Download inbound audio to a temp path when WhatsApp only provides a URL.
|
||||||
2) Run the configured CLI (templated with `{{MediaPath}}`), expecting transcript on stdout.
|
2) Run the configured CLI (templated with `{{MediaPath}}`), expecting transcript on stdout.
|
||||||
3) Replace `Body` with the transcript, set `{{Transcript}}`, and prepend the original media path plus a `Transcript:` section in the command prompt so models see both.
|
3) Replace `Body` with the transcript, set `{{Transcript}}`, and prepend the original media path plus a `Transcript:` section in the command prompt so models see both.
|
||||||
|
|
@ -17,8 +17,8 @@ read_when:
|
||||||
Requires `OPENAI_API_KEY` in env and `openai` CLI installed:
|
Requires `OPENAI_API_KEY` in env and `openai` CLI installed:
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
routing: {
|
audio: {
|
||||||
transcribeAudio: {
|
transcription: {
|
||||||
command: [
|
command: [
|
||||||
"openai",
|
"openai",
|
||||||
"api",
|
"api",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ CLAWDBOT is now **web-only** (Baileys). This document captures the current media
|
||||||
## Web Provider Behavior
|
## Web Provider Behavior
|
||||||
- Input: local file path **or** HTTP(S) URL.
|
- Input: local file path **or** HTTP(S) URL.
|
||||||
- Flow: load into a Buffer, detect media kind, and build the correct payload:
|
- Flow: load into a Buffer, detect media kind, and build the correct payload:
|
||||||
- **Images:** resize & recompress to JPEG (max side 2048px) targeting `agent.mediaMaxMb` (default 5 MB), capped at 6 MB.
|
- **Images:** resize & recompress to JPEG (max side 2048px) targeting `agents.defaults.mediaMaxMb` (default 5 MB), capped at 6 MB.
|
||||||
- **Audio/Voice/Video:** pass-through up to 16 MB; audio is sent as a voice note (`ptt: true`).
|
- **Audio/Voice/Video:** pass-through up to 16 MB; audio is sent as a voice note (`ptt: true`).
|
||||||
- **Documents:** anything else, up to 100 MB, with filename preserved when available.
|
- **Documents:** anything else, up to 100 MB, with filename preserved when available.
|
||||||
- WhatsApp GIF-style playback: send an MP4 with `gifPlayback: true` (CLI: `--gif-playback`) so mobile clients loop inline.
|
- WhatsApp GIF-style playback: send an MP4 with `gifPlayback: true` (CLI: `--gif-playback`) so mobile clients loop inline.
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,8 @@ Example “single server, only allow me, only allow #help”:
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
|
- `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
|
||||||
- `routing.groupChat.mentionPatterns` also count as mentions for guild messages.
|
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
- If `channels` is present, any channel not listed is denied by default.
|
- If `channels` is present, any channel not listed is denied by default.
|
||||||
|
|
||||||
### 6) Verify it works
|
### 6) Verify it works
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ DMs:
|
||||||
Groups:
|
Groups:
|
||||||
- `imessage.groupPolicy = open | allowlist | disabled`.
|
- `imessage.groupPolicy = open | allowlist | disabled`.
|
||||||
- `imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
|
- `imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
|
||||||
- Mention gating uses `routing.groupChat.mentionPatterns` (iMessage has no native mention metadata).
|
- Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) because iMessage has no native mention metadata.
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
|
|
||||||
## How it works (behavior)
|
## How it works (behavior)
|
||||||
- `imsg` streams message events; the gateway normalizes them into the shared provider envelope.
|
- `imsg` streams message events; the gateway normalizes them into the shared provider envelope.
|
||||||
|
|
@ -112,5 +112,5 @@ Provider options:
|
||||||
- `imessage.textChunkLimit`: outbound chunk size (chars).
|
- `imessage.textChunkLimit`: outbound chunk size (chars).
|
||||||
|
|
||||||
Related global options:
|
Related global options:
|
||||||
- `routing.groupChat.mentionPatterns`.
|
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
|
||||||
- `messages.responsePrefix`.
|
- `messages.responsePrefix`.
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,6 @@ Provider options:
|
||||||
- `signal.mediaMaxMb`: inbound/outbound media cap (MB).
|
- `signal.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||||
|
|
||||||
Related global options:
|
Related global options:
|
||||||
- `routing.groupChat.mentionPatterns` (Signal does not support native mentions).
|
- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- `messages.groupChat.mentionPatterns` (global fallback).
|
||||||
- `messages.responsePrefix`.
|
- `messages.responsePrefix`.
|
||||||
|
|
|
||||||
|
|
@ -248,8 +248,8 @@ Slack tool actions can be gated with `slack.actions.*`:
|
||||||
| emojiList | enabled | Custom emoji list |
|
| emojiList | enabled | Custom emoji list |
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `routing.groupChat.mentionPatterns` also count as mentions.
|
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions.
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
|
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
|
||||||
- Bot-authored messages are ignored by default; enable via `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
- Bot-authored messages are ignored by default; enable via `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
||||||
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
|
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@ group messages, so use admin if you need full visibility.
|
||||||
|
|
||||||
## How it works (behavior)
|
## How it works (behavior)
|
||||||
- Inbound messages are normalized into the shared provider envelope with reply context and media placeholders.
|
- Inbound messages are normalized into the shared provider envelope with reply context and media placeholders.
|
||||||
- Group replies require a mention by default (native @mention or `routing.groupChat.mentionPatterns`).
|
- Group replies require a mention by default (native @mention or `agents.list[].groupChat.mentionPatterns` / `messages.groupChat.mentionPatterns`).
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
- Replies always route back to the same Telegram chat.
|
- Replies always route back to the same Telegram chat.
|
||||||
- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agent.maxConcurrent`.
|
- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agents.defaults.maxConcurrent`.
|
||||||
|
|
||||||
## Formatting (Telegram HTML)
|
## Formatting (Telegram HTML)
|
||||||
- Outbound Telegram text uses `parse_mode: "HTML"` (Telegram’s supported tag subset).
|
- Outbound Telegram text uses `parse_mode: "HTML"` (Telegram’s supported tag subset).
|
||||||
|
|
@ -81,7 +81,7 @@ group messages, so use admin if you need full visibility.
|
||||||
|
|
||||||
## Group activation modes
|
## Group activation modes
|
||||||
|
|
||||||
By default, the bot only responds to mentions in groups (`@botname` or patterns in `routing.groupChat.mentionPatterns`). To change this behavior:
|
By default, the bot only responds to mentions in groups (`@botname` or patterns in `agents.list[].groupChat.mentionPatterns`). To change this behavior:
|
||||||
|
|
||||||
### Via config (recommended)
|
### Via config (recommended)
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ Provider options:
|
||||||
- `telegram.actions.sendMessage`: gate Telegram tool message sends.
|
- `telegram.actions.sendMessage`: gate Telegram tool message sends.
|
||||||
|
|
||||||
Related global options:
|
Related global options:
|
||||||
- `routing.groupChat.mentionPatterns` (mention gating patterns).
|
- `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
|
||||||
- `routing.agents.<agentId>.mentionPatterns` overrides for multi-agent setups.
|
- `messages.groupChat.mentionPatterns` (global fallback).
|
||||||
- `commands.native`, `commands.text`, `commands.useAccessGroups` (command behavior).
|
- `commands.native`, `commands.text`, `commands.useAccessGroups` (command behavior).
|
||||||
- `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`.
|
- `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`.
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ Behavior:
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
- Outbound text is chunked to `whatsapp.textChunkLimit` (default 4000).
|
- Outbound text is chunked to `whatsapp.textChunkLimit` (default 4000).
|
||||||
- Media items are capped by `agent.mediaMaxMb` (default 5 MB).
|
- Media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
|
||||||
|
|
||||||
## Outbound send (text + media)
|
## Outbound send (text + media)
|
||||||
- Uses active web listener; error if gateway not running.
|
- Uses active web listener; error if gateway not running.
|
||||||
|
|
@ -164,13 +164,13 @@ Behavior:
|
||||||
|
|
||||||
## Media limits + optimization
|
## Media limits + optimization
|
||||||
- Default cap: 5 MB (per media item).
|
- Default cap: 5 MB (per media item).
|
||||||
- Override: `agent.mediaMaxMb`.
|
- Override: `agents.defaults.mediaMaxMb`.
|
||||||
- Images are auto-optimized to JPEG under cap (resize + quality sweep).
|
- Images are auto-optimized to JPEG under cap (resize + quality sweep).
|
||||||
- Oversize media => error; media reply falls back to text warning.
|
- Oversize media => error; media reply falls back to text warning.
|
||||||
|
|
||||||
## Heartbeats
|
## Heartbeats
|
||||||
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
|
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
|
||||||
- **Agent heartbeat** is global (`agent.heartbeat.*`) and runs in the main session.
|
- **Agent heartbeat** is global (`agents.defaults.heartbeat.*`) and runs in the main session.
|
||||||
- Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) + `HEARTBEAT_OK` skip behavior.
|
- Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) + `HEARTBEAT_OK` skip behavior.
|
||||||
- Delivery defaults to the last used provider (or configured target).
|
- Delivery defaults to the last used provider (or configured target).
|
||||||
|
|
||||||
|
|
@ -189,16 +189,15 @@ Behavior:
|
||||||
- `whatsapp.groupPolicy` (group policy).
|
- `whatsapp.groupPolicy` (group policy).
|
||||||
- `whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
|
- `whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
|
||||||
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions).
|
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions).
|
||||||
- `routing.groupChat.mentionPatterns`
|
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
|
||||||
- Multi-agent override: `routing.agents.<agentId>.mentionPatterns` takes precedence.
|
- `messages.groupChat.historyLimit`
|
||||||
- `routing.groupChat.historyLimit`
|
|
||||||
- `messages.messagePrefix` (inbound prefix)
|
- `messages.messagePrefix` (inbound prefix)
|
||||||
- `messages.responsePrefix` (outbound prefix)
|
- `messages.responsePrefix` (outbound prefix)
|
||||||
- `agent.mediaMaxMb`
|
- `agents.defaults.mediaMaxMb`
|
||||||
- `agent.heartbeat.every`
|
- `agents.defaults.heartbeat.every`
|
||||||
- `agent.heartbeat.model` (optional override)
|
- `agents.defaults.heartbeat.model` (optional override)
|
||||||
- `agent.heartbeat.target`
|
- `agents.defaults.heartbeat.target`
|
||||||
- `agent.heartbeat.to`
|
- `agents.defaults.heartbeat.to`
|
||||||
- `session.*` (scope, idle, store, mainKey)
|
- `session.*` (scope, idle, store, mainKey)
|
||||||
- `web.enabled` (disable provider startup when false)
|
- `web.enabled` (disable provider startup when false)
|
||||||
- `web.heartbeatSeconds`
|
- `web.heartbeatSeconds`
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ read_when:
|
||||||
|
|
||||||
## First run (recommended)
|
## First run (recommended)
|
||||||
|
|
||||||
Clawdbot uses a dedicated workspace directory for the agent. Default: `~/clawd` (configurable via `agent.workspace`).
|
Clawdbot uses a dedicated workspace directory for the agent. Default: `~/clawd` (configurable via `agents.defaults.workspace`).
|
||||||
|
|
||||||
1) Create the workspace (if it doesn’t already exist):
|
1) Create the workspace (if it doesn’t already exist):
|
||||||
|
|
||||||
|
|
@ -30,13 +30,11 @@ cp docs/reference/templates/TOOLS.md ~/clawd/TOOLS.md
|
||||||
cp docs/reference/AGENTS.default.md ~/clawd/AGENTS.md
|
cp docs/reference/AGENTS.default.md ~/clawd/AGENTS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
4) Optional: choose a different workspace by setting `agent.workspace` (supports `~`):
|
4) Optional: choose a different workspace by setting `agents.defaults.workspace` (supports `~`):
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: { defaults: { workspace: "~/clawd" } }
|
||||||
workspace: "~/clawd"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ You’re putting an agent in a position to:
|
||||||
Start conservative:
|
Start conservative:
|
||||||
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
|
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
|
||||||
- Use a dedicated WhatsApp number for the assistant.
|
- Use a dedicated WhatsApp number for the assistant.
|
||||||
- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agent.heartbeat.every: "0m"`.
|
- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agents.defaults.heartbeat.every: "0m"`.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ clawdbot setup
|
||||||
|
|
||||||
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
|
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
|
||||||
|
|
||||||
Optional: choose a different workspace with `agent.workspace` (supports `~`).
|
Optional: choose a different workspace with `agents.defaults.workspace` (supports `~`).
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|
@ -173,9 +173,9 @@ Example:
|
||||||
|
|
||||||
By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt:
|
By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt:
|
||||||
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
|
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
|
||||||
Set `agent.heartbeat.every: "0m"` to disable.
|
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||||
|
|
||||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
|
||||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
|
|
|
||||||
|
|
@ -115,14 +115,14 @@ Everything lives under `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`):
|
||||||
|
|
||||||
Legacy single‑agent path: `~/.clawdbot/agent/*` (migrated by `clawdbot doctor`).
|
Legacy single‑agent path: `~/.clawdbot/agent/*` (migrated by `clawdbot doctor`).
|
||||||
|
|
||||||
Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and configured via `agent.workspace` (default: `~/clawd`).
|
Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and configured via `agents.defaults.workspace` (default: `~/clawd`).
|
||||||
|
|
||||||
### Can agents work outside the workspace?
|
### Can agents work outside the workspace?
|
||||||
|
|
||||||
Yes. The workspace is the **default cwd** and memory anchor, not a hard sandbox.
|
Yes. The workspace is the **default cwd** and memory anchor, not a hard sandbox.
|
||||||
Relative paths resolve inside the workspace, but absolute paths can access other
|
Relative paths resolve inside the workspace, but absolute paths can access other
|
||||||
host locations unless sandboxing is enabled. If you need isolation, use
|
host locations unless sandboxing is enabled. If you need isolation, use
|
||||||
[`agent.sandbox`](/gateway/sandboxing) or per‑agent sandbox settings. If you
|
[`agents.defaults.sandbox`](/gateway/sandboxing) or per‑agent sandbox settings. If you
|
||||||
want a repo to be the default working directory, point that agent’s
|
want a repo to be the default working directory, point that agent’s
|
||||||
`workspace` to the repo root. The Clawdbot repo is just source code; keep the
|
`workspace` to the repo root. The Clawdbot repo is just source code; keep the
|
||||||
workspace separate unless you intentionally want the agent to work inside it.
|
workspace separate unless you intentionally want the agent to work inside it.
|
||||||
|
|
@ -259,7 +259,7 @@ Direct chats collapse to the main session by default. Groups/channels have their
|
||||||
Clawdbot’s default model is whatever you set as:
|
Clawdbot’s default model is whatever you set as:
|
||||||
|
|
||||||
```
|
```
|
||||||
agent.model.primary
|
agents.defaults.model.primary
|
||||||
```
|
```
|
||||||
|
|
||||||
Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-5`). If you omit the provider, Clawdbot currently assumes `anthropic` as a temporary deprecation fallback — but you should still **explicitly** set `provider/model`.
|
Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-5`). If you omit the provider, Clawdbot currently assumes `anthropic` as a temporary deprecation fallback — but you should still **explicitly** set `provider/model`.
|
||||||
|
|
@ -282,7 +282,7 @@ You can list available models with `/model`, `/model list`, or `/model status`.
|
||||||
|
|
||||||
### Why do I see “Model … is not allowed” and then no reply?
|
### Why do I see “Model … is not allowed” and then no reply?
|
||||||
|
|
||||||
If `agent.models` is set, it becomes the **allowlist** for `/model` and any
|
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any
|
||||||
session overrides. Choosing a model that isn’t in that list returns:
|
session overrides. Choosing a model that isn’t in that list returns:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -290,11 +290,11 @@ Model "provider/model" is not allowed. Use /model to list available models.
|
||||||
```
|
```
|
||||||
|
|
||||||
That error is returned **instead of** a normal reply. Fix: add the model to
|
That error is returned **instead of** a normal reply. Fix: add the model to
|
||||||
`agent.models`, remove the allowlist, or pick a model from `/model list`.
|
`agents.defaults.models`, remove the allowlist, or pick a model from `/model list`.
|
||||||
|
|
||||||
### Are opus / sonnet / gpt built‑in shortcuts?
|
### Are opus / sonnet / gpt built‑in shortcuts?
|
||||||
|
|
||||||
Yes. Clawdbot ships a few default shorthands (only applied when the model exists in `agent.models`):
|
Yes. Clawdbot ships a few default shorthands (only applied when the model exists in `agents.defaults.models`):
|
||||||
|
|
||||||
- `opus` → `anthropic/claude-opus-4-5`
|
- `opus` → `anthropic/claude-opus-4-5`
|
||||||
- `sonnet` → `anthropic/claude-sonnet-4-5`
|
- `sonnet` → `anthropic/claude-sonnet-4-5`
|
||||||
|
|
@ -307,7 +307,7 @@ If you set your own alias with the same name, your value wins.
|
||||||
|
|
||||||
### How do I define/override model shortcuts (aliases)?
|
### How do I define/override model shortcuts (aliases)?
|
||||||
|
|
||||||
Aliases come from `agent.models.<modelId>.alias`. Example:
|
Aliases come from `agents.defaults.models.<modelId>.alias`. Example:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|
@ -359,7 +359,7 @@ If you reference a provider/model but the required provider key is missing, you
|
||||||
Failover happens in two stages:
|
Failover happens in two stages:
|
||||||
|
|
||||||
1) **Auth profile rotation** within the same provider.
|
1) **Auth profile rotation** within the same provider.
|
||||||
2) **Model fallback** to the next model in `agent.model.fallbacks`.
|
2) **Model fallback** to the next model in `agents.defaults.model.fallbacks`.
|
||||||
|
|
||||||
Cooldowns apply to failing profiles (exponential backoff), so Clawdbot can keep responding even when a provider is rate‑limited or temporarily failing.
|
Cooldowns apply to failing profiles (exponential backoff), so Clawdbot can keep responding even when a provider is rate‑limited or temporarily failing.
|
||||||
|
|
||||||
|
|
@ -387,7 +387,7 @@ It means the system attempted to use the auth profile ID `anthropic:default`, bu
|
||||||
|
|
||||||
If your model config includes Google Gemini as a fallback (or you switched to a Gemini shorthand), Clawdbot will try it during model fallback. If you haven’t configured Google credentials, you’ll see `No API key found for provider "google"`.
|
If your model config includes Google Gemini as a fallback (or you switched to a Gemini shorthand), Clawdbot will try it during model fallback. If you haven’t configured Google credentials, you’ll see `No API key found for provider "google"`.
|
||||||
|
|
||||||
Fix: either provide Google auth, or remove/avoid Google models in `agent.model.fallbacks` / aliases so fallback doesn’t route there.
|
Fix: either provide Google auth, or remove/avoid Google models in `agents.defaults.model.fallbacks` / aliases so fallback doesn’t route there.
|
||||||
|
|
||||||
## Auth profiles: what they are and how to manage them
|
## Auth profiles: what they are and how to manage them
|
||||||
|
|
||||||
|
|
@ -506,7 +506,7 @@ Yes, but you must isolate:
|
||||||
|
|
||||||
- `CLAWDBOT_CONFIG_PATH` (per‑instance config)
|
- `CLAWDBOT_CONFIG_PATH` (per‑instance config)
|
||||||
- `CLAWDBOT_STATE_DIR` (per‑instance state)
|
- `CLAWDBOT_STATE_DIR` (per‑instance state)
|
||||||
- `agent.workspace` (workspace isolation)
|
- `agents.defaults.workspace` (workspace isolation)
|
||||||
- `gateway.port` (unique ports)
|
- `gateway.port` (unique ports)
|
||||||
|
|
||||||
There are convenience CLI flags like `--dev` and `--profile <name>` that shift state dirs and ports.
|
There are convenience CLI flags like `--dev` and `--profile <name>` that shift state dirs and ports.
|
||||||
|
|
@ -619,7 +619,7 @@ You can add options like `debounce:2s cap:25 drop:summarize` for followup modes.
|
||||||
### “All models failed” — what should I check first?
|
### “All models failed” — what should I check first?
|
||||||
|
|
||||||
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
||||||
- **Model routing**: confirm `agent.model.primary` and fallbacks are models you can access.
|
- **Model routing**: confirm `agents.defaults.model.primary` and fallbacks are models you can access.
|
||||||
- **Gateway logs** in `/tmp/clawdbot/…` for the exact provider error.
|
- **Gateway logs** in `/tmp/clawdbot/…` for the exact provider error.
|
||||||
- **`/model status`** to see current configured models + shorthands.
|
- **`/model status`** to see current configured models + shorthands.
|
||||||
|
|
||||||
|
|
@ -658,7 +658,7 @@ clawdbot providers login
|
||||||
|
|
||||||
**Q: “What’s the default model for Anthropic with an API key?”**
|
**Q: “What’s the default model for Anthropic with an API key?”**
|
||||||
|
|
||||||
**A:** In Clawdbot, credentials and model selection are separate. Setting `ANTHROPIC_API_KEY` (or storing an Anthropic API key in auth profiles) enables authentication, but the actual default model is whatever you configure in `agent.model.primary` (for example, `anthropic/claude-sonnet-4-5` or `anthropic/claude-opus-4-5`). If you see `No credentials found for profile "anthropic:default"`, it means the Gateway couldn’t find Anthropic credentials in the expected `auth-profiles.json` for the agent that’s running.
|
**A:** In Clawdbot, credentials and model selection are separate. Setting `ANTHROPIC_API_KEY` (or storing an Anthropic API key in auth profiles) enables authentication, but the actual default model is whatever you configure in `agents.defaults.model.primary` (for example, `anthropic/claude-sonnet-4-5` or `anthropic/claude-opus-4-5`). If you see `No credentials found for profile "anthropic:default"`, it means the Gateway couldn’t find Anthropic credentials in the expected `auth-profiles.json` for the agent that’s running.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It set
|
||||||
|
|
||||||
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
|
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
|
||||||
|
|
||||||
Sandboxing note: `agent.sandbox.mode: "non-main"` uses `session.mainKey` (default `"main"`),
|
Sandboxing note: `agents.defaults.sandbox.mode: "non-main"` uses `session.mainKey` (default `"main"`),
|
||||||
so group/channel sessions are sandboxed. If you want the main agent to always
|
so group/channel sessions are sandboxed. If you want the main agent to always
|
||||||
run on host, set an explicit per-agent override:
|
run on host, set an explicit per-agent override:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,12 +71,12 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
||||||
|
|
||||||
2) **Model/Auth**
|
2) **Model/Auth**
|
||||||
- **Anthropic OAuth (Claude CLI)**: on macOS the wizard checks Keychain item "Claude Code-credentials" (choose "Always Allow" so launchd starts don't block); on Linux/Windows it reuses `~/.claude/.credentials.json` if present.
|
- **Anthropic OAuth (Claude CLI)**: on macOS the wizard checks Keychain item "Claude Code-credentials" (choose "Always Allow" so launchd starts don't block); on Linux/Windows it reuses `~/.claude/.credentials.json` if present.
|
||||||
- **Anthropic token (paste setup-token)**: run `claude setup-token` in your terminal, then paste the token (you can name it; blank = default).
|
- **Anthropic token (paste setup-token)**: run `claude setup-token` in your terminal, then paste the token (you can name it; blank = default).
|
||||||
- **OpenAI Codex OAuth (Codex CLI)**: if `~/.codex/auth.json` exists, the wizard can reuse it.
|
- **OpenAI Codex OAuth (Codex CLI)**: if `~/.codex/auth.json` exists, the wizard can reuse it.
|
||||||
- **OpenAI Codex OAuth**: browser flow; paste the `code#state`.
|
- **OpenAI Codex OAuth**: browser flow; paste the `code#state`.
|
||||||
- Sets `agent.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
|
- Sets `agents.defaults.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
|
||||||
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it.
|
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it.
|
||||||
- **API key**: stores the key for you.
|
- **API key**: stores the key for you.
|
||||||
- **Minimax M2.1 (LM Studio)**: config is auto‑written for the LM Studio endpoint.
|
- **Minimax M2.1 (LM Studio)**: config is auto‑written for the LM Studio endpoint.
|
||||||
- **Skip**: no auth configured yet.
|
- **Skip**: no auth configured yet.
|
||||||
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
|
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
|
||||||
|
|
@ -144,14 +144,14 @@ Use `clawdbot agents add <name>` to create a separate agent with its own workspa
|
||||||
sessions, and auth profiles. Running without `--workspace` launches the wizard.
|
sessions, and auth profiles. Running without `--workspace` launches the wizard.
|
||||||
|
|
||||||
What it sets:
|
What it sets:
|
||||||
- `routing.agents.<agentId>.name`
|
- `agents.list[].name`
|
||||||
- `routing.agents.<agentId>.workspace`
|
- `agents.list[].workspace`
|
||||||
- `routing.agents.<agentId>.agentDir`
|
- `agents.list[].agentDir`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Default workspaces follow `~/clawd-<agentId>`.
|
- Default workspaces follow `~/clawd-<agentId>`.
|
||||||
- Add `routing.bindings` to route inbound messages (the wizard can do this).
|
- Add `bindings` to route inbound messages (the wizard can do this).
|
||||||
- Non-interactive flags: `--model`, `--agent-dir`, `--bind`, `--non-interactive`.
|
- Non-interactive flags: `--model`, `--agent-dir`, `--bind`, `--non-interactive`.
|
||||||
|
|
||||||
## Non‑interactive mode
|
## Non‑interactive mode
|
||||||
|
|
||||||
|
|
@ -213,8 +213,8 @@ Notes:
|
||||||
## What the wizard writes
|
## What the wizard writes
|
||||||
|
|
||||||
Typical fields in `~/.clawdbot/clawdbot.json`:
|
Typical fields in `~/.clawdbot/clawdbot.json`:
|
||||||
- `agent.workspace`
|
- `agents.defaults.workspace`
|
||||||
- `agent.model` / `models.providers` (if Minimax chosen)
|
- `agents.defaults.model` / `models.providers` (if Minimax chosen)
|
||||||
- `gateway.*` (mode, bind, auth, tailscale)
|
- `gateway.*` (mode, bind, auth, tailscale)
|
||||||
- `telegram.botToken`, `discord.token`, `signal.*`, `imessage.*`
|
- `telegram.botToken`, `discord.token`, `signal.*`, `imessage.*`
|
||||||
- `skills.install.nodeManager`
|
- `skills.install.nodeManager`
|
||||||
|
|
@ -224,7 +224,7 @@ Typical fields in `~/.clawdbot/clawdbot.json`:
|
||||||
- `wizard.lastRunCommand`
|
- `wizard.lastRunCommand`
|
||||||
- `wizard.lastRunMode`
|
- `wizard.lastRunMode`
|
||||||
|
|
||||||
`clawdbot agents add` writes `routing.agents.<agentId>` and optional `routing.bindings`.
|
`clawdbot agents add` writes `agents.list[]` and optional `bindings`.
|
||||||
|
|
||||||
WhatsApp credentials go under `~/.clawdbot/credentials/whatsapp/<accountId>/`.
|
WhatsApp credentials go under `~/.clawdbot/credentials/whatsapp/<accountId>/`.
|
||||||
Sessions are stored under `~/.clawdbot/agents/<agentId>/sessions/`.
|
Sessions are stored under `~/.clawdbot/agents/<agentId>/sessions/`.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ read_when:
|
||||||
- Only `on|off` are accepted; anything else returns a hint and does not change state.
|
- Only `on|off` are accepted; anything else returns a hint and does not change state.
|
||||||
|
|
||||||
## What it controls (and what it doesn’t)
|
## What it controls (and what it doesn’t)
|
||||||
- **Global availability gate**: `agent.elevated` is global (not per-agent). If disabled or sender not allowlisted, elevated is unavailable everywhere.
|
- **Global availability gate**: `tools.elevated` is global (not per-agent). If disabled or sender not allowlisted, elevated is unavailable everywhere.
|
||||||
- **Per-session state**: `/elevated on|off` sets the elevated level for the current session key.
|
- **Per-session state**: `/elevated on|off` sets the elevated level for the current session key.
|
||||||
- **Inline directive**: `/elevated on` inside a message applies to that message only.
|
- **Inline directive**: `/elevated on` inside a message applies to that message only.
|
||||||
- **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned.
|
- **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned.
|
||||||
|
|
@ -31,7 +31,7 @@ Note:
|
||||||
## Resolution order
|
## Resolution order
|
||||||
1. Inline directive on the message (applies only to that message).
|
1. Inline directive on the message (applies only to that message).
|
||||||
2. Session override (set by sending a directive-only message).
|
2. Session override (set by sending a directive-only message).
|
||||||
3. Global default (`agent.elevatedDefault` in config).
|
3. Global default (`agents.defaults.elevatedDefault` in config).
|
||||||
|
|
||||||
## Setting a session default
|
## Setting a session default
|
||||||
- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`.
|
- Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`.
|
||||||
|
|
@ -40,10 +40,10 @@ Note:
|
||||||
- Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level.
|
- Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level.
|
||||||
|
|
||||||
## Availability + allowlists
|
## Availability + allowlists
|
||||||
- Feature gate: `agent.elevated.enabled` (default can be off via config even if the code supports it).
|
- Feature gate: `tools.elevated.enabled` (default can be off via config even if the code supports it).
|
||||||
- Sender allowlist: `agent.elevated.allowFrom` with per-provider allowlists (e.g. `discord`, `whatsapp`).
|
- Sender allowlist: `tools.elevated.allowFrom` with per-provider allowlists (e.g. `discord`, `whatsapp`).
|
||||||
- Both must pass; otherwise elevated is treated as unavailable.
|
- Both must pass; otherwise elevated is treated as unavailable.
|
||||||
- Discord fallback: if `agent.elevated.allowFrom.discord` is omitted, the `discord.dm.allowFrom` list is used as a fallback. Set `agent.elevated.allowFrom.discord` (even `[]`) to override.
|
- Discord fallback: if `tools.elevated.allowFrom.discord` is omitted, the `discord.dm.allowFrom` list is used as a fallback. Set `tools.elevated.allowFrom.discord` (even `[]`) to override.
|
||||||
|
|
||||||
## Logging + status
|
## Logging + status
|
||||||
- Elevated bash calls are logged at info level.
|
- Elevated bash calls are logged at info level.
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,12 @@ and the agent should rely on them directly.
|
||||||
|
|
||||||
## Disabling tools
|
## Disabling tools
|
||||||
|
|
||||||
You can globally allow/deny tools via `agent.tools` in `clawdbot.json`
|
You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.json`
|
||||||
(deny wins). This prevents disallowed tools from being sent to providers.
|
(deny wins). This prevents disallowed tools from being sent to providers.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
tools: { deny: ["browser"] }
|
||||||
tools: {
|
|
||||||
deny: ["browser"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -43,7 +39,7 @@ Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
- Use `process` to poll/log/write/kill/clear background sessions.
|
- Use `process` to poll/log/write/kill/clear background sessions.
|
||||||
- If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`.
|
- If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`.
|
||||||
- `elevated` is gated by `agent.elevated` (global sender allowlist) and runs on the host.
|
- `elevated` is gated by `tools.elevated` (global sender allowlist) and runs on the host.
|
||||||
- `elevated` only changes behavior when the agent is sandboxed (otherwise it’s a no-op).
|
- `elevated` only changes behavior when the agent is sandboxed (otherwise it’s a no-op).
|
||||||
|
|
||||||
### `process`
|
### `process`
|
||||||
|
|
@ -145,7 +141,7 @@ Core parameters:
|
||||||
- `maxBytesMb` (optional size cap)
|
- `maxBytesMb` (optional size cap)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Only available when `agent.imageModel` is configured (primary or fallbacks).
|
- Only available when `agents.defaults.imageModel` is configured (primary or fallbacks).
|
||||||
- Uses the image model directly (independent of the main chat model).
|
- Uses the image model directly (independent of the main chat model).
|
||||||
|
|
||||||
### `message`
|
### `message`
|
||||||
|
|
@ -219,7 +215,7 @@ Notes:
|
||||||
List agent ids that the current session may target with `sessions_spawn`.
|
List agent ids that the current session may target with `sessions_spawn`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Result is restricted to per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
- Result is restricted to per-agent allowlists (`agents.list[].subagents.allowAgents`).
|
||||||
- When `["*"]` is configured, the tool includes all configured agents and marks `allowAny: true`.
|
- When `["*"]` is configured, the tool includes all configured agents and marks `allowAny: true`.
|
||||||
|
|
||||||
## Parameters (common)
|
## Parameters (common)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Text + native (when enabled):
|
||||||
- `/verbose on|off` (alias: `/v`)
|
- `/verbose on|off` (alias: `/v`)
|
||||||
- `/reasoning on|off|stream` (alias: `/reason`; `stream` = Telegram draft only)
|
- `/reasoning on|off|stream` (alias: `/reason`; `stream` = Telegram draft only)
|
||||||
- `/elevated on|off` (alias: `/elev`)
|
- `/elevated on|off` (alias: `/elev`)
|
||||||
- `/model <name>` (or `/<alias>` from `agent.models.*.alias`)
|
- `/model <name>` (or `/<alias>` from `agents.defaults.models.*.alias`)
|
||||||
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
||||||
|
|
||||||
Text-only:
|
Text-only:
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ Tool params:
|
||||||
- `cleanup?` (`delete|keep`, default `keep`)
|
- `cleanup?` (`delete|keep`, default `keep`)
|
||||||
|
|
||||||
Allowlist:
|
Allowlist:
|
||||||
- `routing.agents.<agentId>.subagents.allowAgents`: list of agent ids that can be targeted via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
- `agents.list[].subagents.allowAgents`: list of agent ids that can be targeted via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
|
||||||
Discovery:
|
Discovery:
|
||||||
- Use `agents_list` to see which agent ids are currently allowed for `sessions_spawn`.
|
- Use `agents_list` to see which agent ids are currently allowed for `sessions_spawn`.
|
||||||
|
|
||||||
Auto-archive:
|
Auto-archive:
|
||||||
- Sub-agent sessions are automatically archived after `agent.subagents.archiveAfterMinutes` (default: 60).
|
- Sub-agent sessions are automatically archived after `agents.defaults.subagents.archiveAfterMinutes` (default: 60).
|
||||||
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
|
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
|
||||||
- `cleanup: "delete"` archives immediately after announce (still keeps the transcript via rename).
|
- `cleanup: "delete"` archives immediately after announce (still keeps the transcript via rename).
|
||||||
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
|
||||||
|
|
@ -67,9 +67,15 @@ Override via config:
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
subagents: {
|
||||||
|
maxConcurrent: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
subagents: {
|
subagents: {
|
||||||
maxConcurrent: 1,
|
|
||||||
tools: {
|
tools: {
|
||||||
// deny wins
|
// deny wins
|
||||||
deny: ["gateway", "cron"],
|
deny: ["gateway", "cron"],
|
||||||
|
|
@ -85,7 +91,7 @@ Override via config:
|
||||||
|
|
||||||
Sub-agents use a dedicated in-process queue lane:
|
Sub-agents use a dedicated in-process queue lane:
|
||||||
- Lane name: `subagent`
|
- Lane name: `subagent`
|
||||||
- Concurrency: `agent.subagents.maxConcurrent` (default `1`)
|
- Concurrency: `agents.defaults.subagents.maxConcurrent` (default `1`)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ read_when:
|
||||||
## Resolution order
|
## Resolution order
|
||||||
1. Inline directive on the message (applies only to that message).
|
1. Inline directive on the message (applies only to that message).
|
||||||
2. Session override (set by sending a directive-only message).
|
2. Session override (set by sending a directive-only message).
|
||||||
3. Global default (`agent.thinkingDefault` in config).
|
3. Global default (`agents.defaults.thinkingDefault` in config).
|
||||||
4. Fallback: low for reasoning-capable models; off otherwise.
|
4. Fallback: low for reasoning-capable models; off otherwise.
|
||||||
|
|
||||||
## Setting a session default
|
## Setting a session default
|
||||||
|
|
|
||||||
|
|
@ -231,8 +231,10 @@ const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||||
const expectedWorkspace = process.env.WORKSPACE_DIR;
|
const expectedWorkspace = process.env.WORKSPACE_DIR;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (cfg?.agent?.workspace !== expectedWorkspace) {
|
if (cfg?.agents?.defaults?.workspace !== expectedWorkspace) {
|
||||||
errors.push(`agent.workspace mismatch (got ${cfg?.agent?.workspace ?? "unset"})`);
|
errors.push(
|
||||||
|
`agents.defaults.workspace mismatch (got ${cfg?.agents?.defaults?.workspace ?? "unset"})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (cfg?.gateway?.mode !== "local") {
|
if (cfg?.gateway?.mode !== "local") {
|
||||||
errors.push(`gateway.mode mismatch (got ${cfg?.gateway?.mode ?? "unset"})`);
|
errors.push(`gateway.mode mismatch (got ${cfg?.gateway?.mode ?? "unset"})`);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ EOF
|
||||||
|
|
||||||
cat <<NOTE
|
cat <<NOTE
|
||||||
Built ${TARGET_IMAGE}.
|
Built ${TARGET_IMAGE}.
|
||||||
To use it, set agent.sandbox.docker.image to "${TARGET_IMAGE}" and restart.
|
To use it, set agents.defaults.sandbox.docker.image to "${TARGET_IMAGE}" and restart.
|
||||||
If you want a clean re-create, remove old sandbox containers:
|
If you want a clean re-create, remove old sandbox containers:
|
||||||
docker rm -f \$(docker ps -aq --filter label=clawdbot.sandbox=1)
|
docker rm -f \$(docker ps -aq --filter label=clawdbot.sandbox=1)
|
||||||
NOTE
|
NOTE
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ describe("resolveAgentConfig", () => {
|
||||||
|
|
||||||
it("should return undefined when agent id does not exist", () => {
|
it("should return undefined when agent id does not exist", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [{ id: "main", workspace: "~/clawd" }],
|
||||||
main: { workspace: "~/clawd" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = resolveAgentConfig(cfg, "nonexistent");
|
const result = resolveAgentConfig(cfg, "nonexistent");
|
||||||
|
|
@ -23,15 +21,16 @@ describe("resolveAgentConfig", () => {
|
||||||
|
|
||||||
it("should return basic agent config", () => {
|
it("should return basic agent config", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
name: "Main Agent",
|
name: "Main Agent",
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
agentDir: "~/.clawdbot/agents/main",
|
agentDir: "~/.clawdbot/agents/main",
|
||||||
model: "anthropic/claude-opus-4",
|
model: "anthropic/claude-opus-4",
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = resolveAgentConfig(cfg, "main");
|
const result = resolveAgentConfig(cfg, "main");
|
||||||
|
|
@ -40,6 +39,9 @@ describe("resolveAgentConfig", () => {
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
agentDir: "~/.clawdbot/agents/main",
|
agentDir: "~/.clawdbot/agents/main",
|
||||||
model: "anthropic/claude-opus-4",
|
model: "anthropic/claude-opus-4",
|
||||||
|
identity: undefined,
|
||||||
|
groupChat: undefined,
|
||||||
|
subagents: undefined,
|
||||||
sandbox: undefined,
|
sandbox: undefined,
|
||||||
tools: undefined,
|
tools: undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -47,9 +49,10 @@ describe("resolveAgentConfig", () => {
|
||||||
|
|
||||||
it("should return agent-specific sandbox config", () => {
|
it("should return agent-specific sandbox config", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
work: {
|
{
|
||||||
|
id: "work",
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -57,13 +60,9 @@ describe("resolveAgentConfig", () => {
|
||||||
perSession: false,
|
perSession: false,
|
||||||
workspaceAccess: "ro",
|
workspaceAccess: "ro",
|
||||||
workspaceRoot: "~/sandboxes",
|
workspaceRoot: "~/sandboxes",
|
||||||
tools: {
|
|
||||||
allow: ["read"],
|
|
||||||
deny: ["bash"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = resolveAgentConfig(cfg, "work");
|
const result = resolveAgentConfig(cfg, "work");
|
||||||
|
|
@ -73,25 +72,22 @@ describe("resolveAgentConfig", () => {
|
||||||
perSession: false,
|
perSession: false,
|
||||||
workspaceAccess: "ro",
|
workspaceAccess: "ro",
|
||||||
workspaceRoot: "~/sandboxes",
|
workspaceRoot: "~/sandboxes",
|
||||||
tools: {
|
|
||||||
allow: ["read"],
|
|
||||||
deny: ["bash"],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return agent-specific tools config", () => {
|
it("should return agent-specific tools config", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
restricted: {
|
{
|
||||||
|
id: "restricted",
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash", "write", "edit"],
|
deny: ["bash", "write", "edit"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = resolveAgentConfig(cfg, "restricted");
|
const result = resolveAgentConfig(cfg, "restricted");
|
||||||
|
|
@ -103,9 +99,10 @@ describe("resolveAgentConfig", () => {
|
||||||
|
|
||||||
it("should return both sandbox and tools config", () => {
|
it("should return both sandbox and tools config", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -116,7 +113,7 @@ describe("resolveAgentConfig", () => {
|
||||||
deny: ["bash"],
|
deny: ["bash"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = resolveAgentConfig(cfg, "family");
|
const result = resolveAgentConfig(cfg, "family");
|
||||||
|
|
@ -126,10 +123,8 @@ describe("resolveAgentConfig", () => {
|
||||||
|
|
||||||
it("should normalize agent id", () => {
|
it("should normalize agent id", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [{ id: "main", workspace: "~/clawd" }],
|
||||||
main: { workspace: "~/clawd" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Should normalize to "main" (default)
|
// Should normalize to "main" (default)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,24 @@ import {
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
|
import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
|
||||||
|
|
||||||
|
type AgentEntry = NonNullable<
|
||||||
|
NonNullable<ClawdbotConfig["agents"]>["list"]
|
||||||
|
>[number];
|
||||||
|
|
||||||
|
type ResolvedAgentConfig = {
|
||||||
|
name?: string;
|
||||||
|
workspace?: string;
|
||||||
|
agentDir?: string;
|
||||||
|
model?: string;
|
||||||
|
identity?: AgentEntry["identity"];
|
||||||
|
groupChat?: AgentEntry["groupChat"];
|
||||||
|
subagents?: AgentEntry["subagents"];
|
||||||
|
sandbox?: AgentEntry["sandbox"];
|
||||||
|
tools?: AgentEntry["tools"];
|
||||||
|
};
|
||||||
|
|
||||||
|
let defaultAgentWarned = false;
|
||||||
|
|
||||||
export function resolveAgentIdFromSessionKey(
|
export function resolveAgentIdFromSessionKey(
|
||||||
sessionKey?: string | null,
|
sessionKey?: string | null,
|
||||||
): string {
|
): string {
|
||||||
|
|
@ -18,46 +36,51 @@ export function resolveAgentIdFromSessionKey(
|
||||||
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listAgents(cfg: ClawdbotConfig): AgentEntry[] {
|
||||||
|
const list = cfg.agents?.list;
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
return list.filter((entry): entry is AgentEntry =>
|
||||||
|
Boolean(entry && typeof entry === "object"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDefaultAgentId(cfg: ClawdbotConfig): string {
|
||||||
|
const agents = listAgents(cfg);
|
||||||
|
if (agents.length === 0) return DEFAULT_AGENT_ID;
|
||||||
|
const defaults = agents.filter((agent) => agent?.default);
|
||||||
|
if (defaults.length > 1 && !defaultAgentWarned) {
|
||||||
|
defaultAgentWarned = true;
|
||||||
|
console.warn(
|
||||||
|
"Multiple agents marked default=true; using the first entry as default.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
||||||
|
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAgentEntry(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
agentId: string,
|
||||||
|
): AgentEntry | undefined {
|
||||||
|
const id = normalizeAgentId(agentId);
|
||||||
|
return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAgentConfig(
|
export function resolveAgentConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
):
|
): ResolvedAgentConfig | undefined {
|
||||||
| {
|
|
||||||
name?: string;
|
|
||||||
workspace?: string;
|
|
||||||
agentDir?: string;
|
|
||||||
model?: string;
|
|
||||||
subagents?: {
|
|
||||||
allowAgents?: string[];
|
|
||||||
};
|
|
||||||
sandbox?: {
|
|
||||||
mode?: "off" | "non-main" | "all";
|
|
||||||
workspaceAccess?: "none" | "ro" | "rw";
|
|
||||||
scope?: "session" | "agent" | "shared";
|
|
||||||
perSession?: boolean;
|
|
||||||
workspaceRoot?: string;
|
|
||||||
tools?: {
|
|
||||||
allow?: string[];
|
|
||||||
deny?: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
tools?: {
|
|
||||||
allow?: string[];
|
|
||||||
deny?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| undefined {
|
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const agents = cfg.routing?.agents;
|
const entry = resolveAgentEntry(cfg, id);
|
||||||
if (!agents || typeof agents !== "object") return undefined;
|
if (!entry) return undefined;
|
||||||
const entry = agents[id];
|
|
||||||
if (!entry || typeof entry !== "object") return undefined;
|
|
||||||
return {
|
return {
|
||||||
name: typeof entry.name === "string" ? entry.name : undefined,
|
name: typeof entry.name === "string" ? entry.name : undefined,
|
||||||
workspace:
|
workspace:
|
||||||
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||||
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
||||||
model: typeof entry.model === "string" ? entry.model : undefined,
|
model: typeof entry.model === "string" ? entry.model : undefined,
|
||||||
|
identity: entry.identity,
|
||||||
|
groupChat: entry.groupChat,
|
||||||
subagents:
|
subagents:
|
||||||
typeof entry.subagents === "object" && entry.subagents
|
typeof entry.subagents === "object" && entry.subagents
|
||||||
? entry.subagents
|
? entry.subagents
|
||||||
|
|
@ -71,9 +94,10 @@ export function resolveAgentWorkspaceDir(cfg: ClawdbotConfig, agentId: string) {
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||||
if (configured) return resolveUserPath(configured);
|
if (configured) return resolveUserPath(configured);
|
||||||
if (id === DEFAULT_AGENT_ID) {
|
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||||
const legacy = cfg.agent?.workspace?.trim();
|
if (id === defaultAgentId) {
|
||||||
if (legacy) return resolveUserPath(legacy);
|
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
||||||
|
if (fallback) return resolveUserPath(fallback);
|
||||||
return DEFAULT_AGENT_WORKSPACE_DIR;
|
return DEFAULT_AGENT_WORKSPACE_DIR;
|
||||||
}
|
}
|
||||||
return path.join(os.homedir(), `clawd-${id}`);
|
return path.join(os.homedir(), `clawd-${id}`);
|
||||||
|
|
|
||||||
|
|
@ -925,7 +925,10 @@ export function resolveAuthProfileOrder(params: {
|
||||||
|
|
||||||
// Still put preferredProfile first if specified
|
// Still put preferredProfile first if specified
|
||||||
if (preferredProfile && ordered.includes(preferredProfile)) {
|
if (preferredProfile && ordered.includes(preferredProfile)) {
|
||||||
return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)];
|
return [
|
||||||
|
preferredProfile,
|
||||||
|
...ordered.filter((e) => e !== preferredProfile),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ function formatUserTime(date: Date, timeZone: string): string | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||||
const models = cfg?.agent?.models ?? {};
|
const models = cfg?.agents?.defaults?.models ?? {};
|
||||||
const entries: Array<{ alias: string; model: string }> = [];
|
const entries: Array<{ alias: string; model: string }> = [];
|
||||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||||
const model = String(keyRaw ?? "").trim();
|
const model = String(keyRaw ?? "").trim();
|
||||||
|
|
@ -134,7 +134,9 @@ function buildSystemPrompt(params: {
|
||||||
contextFiles?: EmbeddedContextFile[];
|
contextFiles?: EmbeddedContextFile[];
|
||||||
modelDisplay: string;
|
modelDisplay: string;
|
||||||
}) {
|
}) {
|
||||||
const userTimezone = resolveUserTimezone(params.config?.agent?.userTimezone);
|
const userTimezone = resolveUserTimezone(
|
||||||
|
params.config?.agents?.defaults?.userTimezone,
|
||||||
|
);
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
|
|
@ -143,7 +145,7 @@ function buildSystemPrompt(params: {
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
reasoningTagHint: false,
|
reasoningTagHint: false,
|
||||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
params.config?.agent?.heartbeat?.prompt,
|
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||||
),
|
),
|
||||||
runtimeInfo: {
|
runtimeInfo: {
|
||||||
host: "clawdbot",
|
host: "clawdbot",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ describe("gateway tool", () => {
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing gateway tool");
|
if (!tool) throw new Error("missing gateway tool");
|
||||||
|
|
||||||
const raw = '{\n agent: { workspace: "~/clawd" }\n}\n';
|
const raw = '{\n agents: { defaults: { workspace: "~/clawd" } }\n}\n';
|
||||||
await tool.execute("call2", {
|
await tool.execute("call2", {
|
||||||
action: "config.apply",
|
action: "config.apply",
|
||||||
raw,
|
raw,
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,20 @@ describe("agents_list", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
name: "Main",
|
name: "Main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["research"],
|
allowAgents: ["research"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
research: {
|
{
|
||||||
|
id: "research",
|
||||||
name: "Research",
|
name: "Research",
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -87,20 +89,23 @@ describe("agents_list", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["*"],
|
allowAgents: ["*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
research: {
|
{
|
||||||
|
id: "research",
|
||||||
name: "Research",
|
name: "Research",
|
||||||
},
|
},
|
||||||
coder: {
|
{
|
||||||
|
id: "coder",
|
||||||
name: "Coder",
|
name: "Coder",
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,14 +136,15 @@ describe("agents_list", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["research"],
|
allowAgents: ["research"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -314,14 +314,15 @@ describe("subagents", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["beta"],
|
allowAgents: ["beta"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -365,14 +366,15 @@ describe("subagents", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["*"],
|
allowAgents: ["*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -416,14 +418,15 @@ describe("subagents", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["Research"],
|
allowAgents: ["Research"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -467,14 +470,15 @@ describe("subagents", () => {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
subagents: {
|
subagents: {
|
||||||
allowAgents: ["alpha"],
|
allowAgents: ["alpha"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function buildAllowedModelKeys(
|
||||||
defaultProvider: string,
|
defaultProvider: string,
|
||||||
): Set<string> | null {
|
): Set<string> | null {
|
||||||
const rawAllowlist = (() => {
|
const rawAllowlist = (() => {
|
||||||
const modelMap = cfg?.agent?.models ?? {};
|
const modelMap = cfg?.agents?.defaults?.models ?? {};
|
||||||
return Object.keys(modelMap);
|
return Object.keys(modelMap);
|
||||||
})();
|
})();
|
||||||
if (rawAllowlist.length === 0) return null;
|
if (rawAllowlist.length === 0) return null;
|
||||||
|
|
@ -85,7 +85,7 @@ function resolveImageFallbackCandidates(params: {
|
||||||
if (params.modelOverride?.trim()) {
|
if (params.modelOverride?.trim()) {
|
||||||
addRaw(params.modelOverride, false);
|
addRaw(params.modelOverride, false);
|
||||||
} else {
|
} else {
|
||||||
const imageModel = params.cfg?.agent?.imageModel as
|
const imageModel = params.cfg?.agents?.defaults?.imageModel as
|
||||||
| { primary?: string }
|
| { primary?: string }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -95,7 +95,7 @@ function resolveImageFallbackCandidates(params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageFallbacks = (() => {
|
const imageFallbacks = (() => {
|
||||||
const imageModel = params.cfg?.agent?.imageModel as
|
const imageModel = params.cfg?.agents?.defaults?.imageModel as
|
||||||
| { fallbacks?: string[] }
|
| { fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -142,7 +142,7 @@ function resolveFallbackCandidates(params: {
|
||||||
addCandidate({ provider, model }, false);
|
addCandidate({ provider, model }, false);
|
||||||
|
|
||||||
const modelFallbacks = (() => {
|
const modelFallbacks = (() => {
|
||||||
const model = params.cfg?.agent?.model as
|
const model = params.cfg?.agents?.defaults?.model as
|
||||||
| { fallbacks?: string[] }
|
| { fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -253,7 +253,7 @@ export async function runWithImageModelFallback<T>(params: {
|
||||||
});
|
});
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No image model configured. Set agent.imageModel.primary or agent.imageModel.fallbacks.",
|
"No image model configured. Set agents.defaults.imageModel.primary or agents.defaults.imageModel.fallbacks.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ const catalog = [
|
||||||
describe("buildAllowedModelSet", () => {
|
describe("buildAllowedModelSet", () => {
|
||||||
it("always allows the configured default model", () => {
|
it("always allows the configured default model", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"openai/gpt-4": { alias: "gpt4" },
|
models: {
|
||||||
|
"openai/gpt-4": { alias: "gpt4" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ClawdbotConfig;
|
} as ClawdbotConfig;
|
||||||
|
|
@ -41,7 +43,7 @@ describe("buildAllowedModelSet", () => {
|
||||||
|
|
||||||
it("includes the default model when no allowlist is set", () => {
|
it("includes the default model when no allowlist is set", () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {},
|
agents: { defaults: {} },
|
||||||
} as ClawdbotConfig;
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
const allowed = buildAllowedModelSet({
|
const allowed = buildAllowedModelSet({
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export function buildModelAliasIndex(params: {
|
||||||
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
|
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
|
||||||
const byKey = new Map<string, string[]>();
|
const byKey = new Map<string, string[]>();
|
||||||
|
|
||||||
const rawModels = params.cfg.agent?.models ?? {};
|
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
||||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||||
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
|
|
@ -109,7 +109,7 @@ export function resolveConfiguredModelRef(params: {
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
}): ModelRef {
|
}): ModelRef {
|
||||||
const rawModel = (() => {
|
const rawModel = (() => {
|
||||||
const raw = params.cfg.agent?.model as
|
const raw = params.cfg.agents?.defaults?.model as
|
||||||
| { primary?: string }
|
| { primary?: string }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -128,7 +128,7 @@ export function resolveConfiguredModelRef(params: {
|
||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (resolved) return resolved.ref;
|
if (resolved) return resolved.ref;
|
||||||
// TODO(steipete): drop this fallback once provider-less agent.model is fully deprecated.
|
// TODO(steipete): drop this fallback once provider-less agents.defaults.model is fully deprecated.
|
||||||
return { provider: "anthropic", model: trimmed };
|
return { provider: "anthropic", model: trimmed };
|
||||||
}
|
}
|
||||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
return { provider: params.defaultProvider, model: params.defaultModel };
|
||||||
|
|
@ -145,7 +145,7 @@ export function buildAllowedModelSet(params: {
|
||||||
allowedKeys: Set<string>;
|
allowedKeys: Set<string>;
|
||||||
} {
|
} {
|
||||||
const rawAllowlist = (() => {
|
const rawAllowlist = (() => {
|
||||||
const modelMap = params.cfg.agent?.models ?? {};
|
const modelMap = params.cfg.agents?.defaults?.models ?? {};
|
||||||
return Object.keys(modelMap);
|
return Object.keys(modelMap);
|
||||||
})();
|
})();
|
||||||
const allowAny = rawAllowlist.length === 0;
|
const allowAny = rawAllowlist.length === 0;
|
||||||
|
|
@ -203,7 +203,7 @@ export function resolveThinkingDefault(params: {
|
||||||
model: string;
|
model: string;
|
||||||
catalog?: ModelCatalogEntry[];
|
catalog?: ModelCatalogEntry[];
|
||||||
}): ThinkLevel {
|
}): ThinkLevel {
|
||||||
const configured = params.cfg.agent?.thinkingDefault;
|
const configured = params.cfg.agents?.defaults?.thinkingDefault;
|
||||||
if (configured) return configured;
|
if (configured) return configured;
|
||||||
const candidate = params.catalog?.find(
|
const candidate = params.catalog?.find(
|
||||||
(entry) => entry.provider === params.provider && entry.id === params.model,
|
(entry) => entry.provider === params.provider && entry.id === params.model,
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,14 @@ describe("resolveExtraParams", () => {
|
||||||
it("respects explicit thinking config from user (disable thinking)", () => {
|
it("respects explicit thinking config from user (disable thinking)", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
params: {
|
"zai/glm-4.7": {
|
||||||
thinking: {
|
params: {
|
||||||
type: "disabled",
|
thinking: {
|
||||||
|
type: "disabled",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -136,12 +138,14 @@ describe("resolveExtraParams", () => {
|
||||||
it("preserves other params while adding thinking config", () => {
|
it("preserves other params while adding thinking config", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
params: {
|
"zai/glm-4.7": {
|
||||||
temperature: 0.7,
|
params: {
|
||||||
max_tokens: 4096,
|
temperature: 0.7,
|
||||||
|
max_tokens: 4096,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -164,13 +168,15 @@ describe("resolveExtraParams", () => {
|
||||||
it("does not override explicit thinking config even if partial", () => {
|
it("does not override explicit thinking config even if partial", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
params: {
|
"zai/glm-4.7": {
|
||||||
thinking: {
|
params: {
|
||||||
type: "enabled",
|
thinking: {
|
||||||
// User explicitly omitted clear_thinking
|
type: "enabled",
|
||||||
|
// User explicitly omitted clear_thinking
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -214,12 +220,14 @@ describe("resolveExtraParams", () => {
|
||||||
it("passes through params for non-GLM models without modification", () => {
|
it("passes through params for non-GLM models without modification", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"openai/gpt-4": {
|
models: {
|
||||||
params: {
|
"openai/gpt-4": {
|
||||||
logprobs: true,
|
params: {
|
||||||
top_logprobs: 5,
|
logprobs: true,
|
||||||
|
top_logprobs: 5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -264,7 +272,7 @@ describe("resolveExtraParams", () => {
|
||||||
|
|
||||||
it("handles config with empty models gracefully", () => {
|
it("handles config with empty models gracefully", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: { agent: { models: {} } },
|
cfg: { agents: { defaults: { models: {} } } },
|
||||||
provider: "zai",
|
provider: "zai",
|
||||||
modelId: "glm-4.7",
|
modelId: "glm-4.7",
|
||||||
});
|
});
|
||||||
|
|
@ -280,12 +288,14 @@ describe("resolveExtraParams", () => {
|
||||||
it("model alias lookup uses exact provider/model key", () => {
|
it("model alias lookup uses exact provider/model key", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
alias: "smart",
|
"zai/glm-4.7": {
|
||||||
params: {
|
alias: "smart",
|
||||||
custom_param: "value",
|
params: {
|
||||||
|
custom_param: "value",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -307,11 +317,13 @@ describe("resolveExtraParams", () => {
|
||||||
it("treats thinking: null as explicit config (no auto-enable)", () => {
|
it("treats thinking: null as explicit config (no auto-enable)", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
params: {
|
"zai/glm-4.7": {
|
||||||
thinking: null,
|
params: {
|
||||||
|
thinking: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -374,11 +386,13 @@ describe("resolveExtraParams", () => {
|
||||||
it("thinkLevel: 'off' still passes through explicit config", () => {
|
it("thinkLevel: 'off' still passes through explicit config", () => {
|
||||||
const result = resolveExtraParams({
|
const result = resolveExtraParams({
|
||||||
cfg: {
|
cfg: {
|
||||||
agent: {
|
agents: {
|
||||||
models: {
|
defaults: {
|
||||||
"zai/glm-4.7": {
|
models: {
|
||||||
params: {
|
"zai/glm-4.7": {
|
||||||
custom_param: "value",
|
params: {
|
||||||
|
custom_param: "value",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||||
* - GLM-4.5/4.6: Interleaved thinking (clear_thinking: true) - reasoning cleared each turn
|
* - GLM-4.5/4.6: Interleaved thinking (clear_thinking: true) - reasoning cleared each turn
|
||||||
*
|
*
|
||||||
* Users can override via config:
|
* Users can override via config:
|
||||||
* agent.models["zai/glm-4.7"].params.thinking = { type: "disabled" }
|
* agents.defaults.models["zai/glm-4.7"].params.thinking = { type: "disabled" }
|
||||||
*
|
*
|
||||||
* Or disable via runtime flag: --thinking off
|
* Or disable via runtime flag: --thinking off
|
||||||
*
|
*
|
||||||
|
|
@ -119,7 +119,7 @@ export function resolveExtraParams(params: {
|
||||||
thinkLevel?: string;
|
thinkLevel?: string;
|
||||||
}): Record<string, unknown> | undefined {
|
}): Record<string, unknown> | undefined {
|
||||||
const modelKey = `${params.provider}/${params.modelId}`;
|
const modelKey = `${params.provider}/${params.modelId}`;
|
||||||
const modelConfig = params.cfg?.agent?.models?.[modelKey];
|
const modelConfig = params.cfg?.agents?.defaults?.models?.[modelKey];
|
||||||
let extraParams = modelConfig?.params ? { ...modelConfig.params } : undefined;
|
let extraParams = modelConfig?.params ? { ...modelConfig.params } : undefined;
|
||||||
|
|
||||||
// Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured
|
// Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured
|
||||||
|
|
@ -200,10 +200,10 @@ function resolveContextWindowTokens(params: {
|
||||||
if (fromModelsConfig) return fromModelsConfig;
|
if (fromModelsConfig) return fromModelsConfig;
|
||||||
|
|
||||||
const fromAgentConfig =
|
const fromAgentConfig =
|
||||||
typeof params.cfg?.agent?.contextTokens === "number" &&
|
typeof params.cfg?.agents?.defaults?.contextTokens === "number" &&
|
||||||
Number.isFinite(params.cfg.agent.contextTokens) &&
|
Number.isFinite(params.cfg.agents.defaults.contextTokens) &&
|
||||||
params.cfg.agent.contextTokens > 0
|
params.cfg.agents.defaults.contextTokens > 0
|
||||||
? Math.floor(params.cfg.agent.contextTokens)
|
? Math.floor(params.cfg.agents.defaults.contextTokens)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (fromAgentConfig) return fromAgentConfig;
|
if (fromAgentConfig) return fromAgentConfig;
|
||||||
|
|
||||||
|
|
@ -217,7 +217,7 @@ function buildContextPruningExtension(params: {
|
||||||
modelId: string;
|
modelId: string;
|
||||||
model: Model<Api> | undefined;
|
model: Model<Api> | undefined;
|
||||||
}): { additionalExtensionPaths?: string[] } {
|
}): { additionalExtensionPaths?: string[] } {
|
||||||
const raw = params.cfg?.agent?.contextPruning;
|
const raw = params.cfg?.agents?.defaults?.contextPruning;
|
||||||
if (raw?.mode !== "adaptive" && raw?.mode !== "aggressive") return {};
|
if (raw?.mode !== "adaptive" && raw?.mode !== "aggressive") return {};
|
||||||
|
|
||||||
const settings = computeEffectiveSettings(raw);
|
const settings = computeEffectiveSettings(raw);
|
||||||
|
|
@ -254,7 +254,7 @@ export type EmbeddedPiRunMeta = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||||
const models = cfg?.agent?.models ?? {};
|
const models = cfg?.agents?.defaults?.models ?? {};
|
||||||
const entries: Array<{ alias: string; model: string }> = [];
|
const entries: Array<{ alias: string; model: string }> = [];
|
||||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||||
const model = String(keyRaw ?? "").trim();
|
const model = String(keyRaw ?? "").trim();
|
||||||
|
|
@ -844,7 +844,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
bash: {
|
||||||
...params.config?.agent?.bash,
|
...params.config?.tools?.bash,
|
||||||
elevated: params.bashElevated,
|
elevated: params.bashElevated,
|
||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
|
|
@ -865,7 +865,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
||||||
const reasoningTagHint = provider === "ollama";
|
const reasoningTagHint = provider === "ollama";
|
||||||
const userTimezone = resolveUserTimezone(
|
const userTimezone = resolveUserTimezone(
|
||||||
params.config?.agent?.userTimezone,
|
params.config?.agents?.defaults?.userTimezone,
|
||||||
);
|
);
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||||
|
|
@ -875,7 +875,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
reasoningTagHint,
|
reasoningTagHint,
|
||||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
params.config?.agent?.heartbeat?.prompt,
|
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||||
),
|
),
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
sandboxInfo,
|
sandboxInfo,
|
||||||
|
|
@ -1157,7 +1157,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
bash: {
|
||||||
...params.config?.agent?.bash,
|
...params.config?.tools?.bash,
|
||||||
elevated: params.bashElevated,
|
elevated: params.bashElevated,
|
||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
|
|
@ -1178,7 +1178,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
||||||
const reasoningTagHint = provider === "ollama";
|
const reasoningTagHint = provider === "ollama";
|
||||||
const userTimezone = resolveUserTimezone(
|
const userTimezone = resolveUserTimezone(
|
||||||
params.config?.agent?.userTimezone,
|
params.config?.agents?.defaults?.userTimezone,
|
||||||
);
|
);
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const appendPrompt = buildEmbeddedSystemPrompt({
|
const appendPrompt = buildEmbeddedSystemPrompt({
|
||||||
|
|
@ -1188,7 +1188,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
reasoningTagHint,
|
reasoningTagHint,
|
||||||
heartbeatPrompt: resolveHeartbeatPrompt(
|
heartbeatPrompt: resolveHeartbeatPrompt(
|
||||||
params.config?.agent?.heartbeat?.prompt,
|
params.config?.agents?.defaults?.heartbeat?.prompt,
|
||||||
),
|
),
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
sandboxInfo,
|
sandboxInfo,
|
||||||
|
|
@ -1444,7 +1444,8 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackConfigured =
|
const fallbackConfigured =
|
||||||
(params.config?.agent?.model?.fallbacks?.length ?? 0) > 0;
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) >
|
||||||
|
0;
|
||||||
const authFailure = isAuthAssistantError(lastAssistant);
|
const authFailure = isAuthAssistantError(lastAssistant);
|
||||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,17 @@ import type { SandboxDockerConfig } from "./sandbox.js";
|
||||||
describe("Agent-specific tool filtering", () => {
|
describe("Agent-specific tool filtering", () => {
|
||||||
it("should apply global tool policy when no agent-specific policy exists", () => {
|
it("should apply global tool policy when no agent-specific policy exists", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
tools: {
|
||||||
tools: {
|
allow: ["read", "write"],
|
||||||
allow: ["read", "write"],
|
deny: ["bash"],
|
||||||
deny: ["bash"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -36,22 +35,21 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
it("should apply agent-specific tool policy", () => {
|
it("should apply agent-specific tool policy", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
tools: {
|
||||||
tools: {
|
allow: ["read", "write", "bash"],
|
||||||
allow: ["read", "write", "bash"],
|
deny: [],
|
||||||
deny: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
restricted: {
|
{
|
||||||
|
id: "restricted",
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"], // Agent override: only read
|
allow: ["read"], // Agent override: only read
|
||||||
deny: ["bash", "write", "edit"],
|
deny: ["bash", "write", "edit"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -71,20 +69,22 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
it("should allow different tool policies for different agents", () => {
|
it("should allow different tool policies for different agents", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
main: {
|
{
|
||||||
|
id: "main",
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
// No tools restriction - all tools available
|
// No tools restriction - all tools available
|
||||||
},
|
},
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash", "write", "edit", "process"],
|
deny: ["bash", "write", "edit", "process"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -116,20 +116,19 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
it("should prefer agent-specific tool policy over global", () => {
|
it("should prefer agent-specific tool policy over global", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
tools: {
|
||||||
tools: {
|
deny: ["browser"], // Global deny
|
||||||
deny: ["browser"], // Global deny
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
routing: {
|
agents: {
|
||||||
agents: {
|
list: [
|
||||||
work: {
|
{
|
||||||
|
id: "work",
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
tools: {
|
tools: {
|
||||||
deny: ["bash", "process"], // Agent deny (override)
|
deny: ["bash", "process"], // Agent deny (override)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -149,19 +148,16 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
it("should work with sandbox tools filtering", () => {
|
it("should work with sandbox tools filtering", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
tools: {
|
scope: "agent",
|
||||||
allow: ["read", "write", "bash"], // Sandbox allows these
|
|
||||||
deny: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "restricted",
|
||||||
restricted: {
|
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -172,6 +168,14 @@ describe("Agent-specific tool filtering", () => {
|
||||||
deny: ["bash", "write"],
|
deny: ["bash", "write"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: {
|
||||||
|
tools: {
|
||||||
|
allow: ["read", "write", "bash"], // Sandbox allows these
|
||||||
|
deny: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -216,10 +220,8 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
it("should run bash synchronously when process is denied", async () => {
|
it("should run bash synchronously when process is denied", async () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
tools: {
|
||||||
tools: {
|
deny: ["process"],
|
||||||
deny: ["process"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
sessionKey: "agent:main:subagent:test",
|
sessionKey: "agent:main:subagent:test",
|
||||||
// Intentionally partial config; only fields used by pi-tools are provided.
|
// Intentionally partial config; only fields used by pi-tools are provided.
|
||||||
config: {
|
config: {
|
||||||
agent: {
|
tools: {
|
||||||
subagents: {
|
subagents: {
|
||||||
tools: {
|
tools: {
|
||||||
// Policy matching is case-insensitive
|
// Policy matching is case-insensitive
|
||||||
|
|
@ -325,7 +325,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
|
|
||||||
it("filters tools by agent tool policy even without sandbox", () => {
|
it("filters tools by agent tool policy even without sandbox", () => {
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
config: { agent: { tools: { deny: ["browser"] } } },
|
config: { tools: { deny: ["browser"] } },
|
||||||
});
|
});
|
||||||
// NOTE: bash is capitalized to bypass Anthropic OAuth blocking
|
// NOTE: bash is capitalized to bypass Anthropic OAuth blocking
|
||||||
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPolicy {
|
function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPolicy {
|
||||||
const configured = cfg?.agent?.subagents?.tools;
|
const configured = cfg?.tools?.subagents?.tools;
|
||||||
const deny = [
|
const deny = [
|
||||||
...DEFAULT_SUBAGENT_TOOL_DENY,
|
...DEFAULT_SUBAGENT_TOOL_DENY,
|
||||||
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
...(Array.isArray(configured?.deny) ? configured.deny : []),
|
||||||
|
|
@ -466,7 +466,7 @@ function resolveEffectiveToolPolicy(params: {
|
||||||
? resolveAgentConfig(params.config, agentId)
|
? resolveAgentConfig(params.config, agentId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const hasAgentTools = agentConfig?.tools !== undefined;
|
const hasAgentTools = agentConfig?.tools !== undefined;
|
||||||
const globalTools = params.config?.agent?.tools;
|
const globalTools = params.config?.tools;
|
||||||
return {
|
return {
|
||||||
agentId,
|
agentId,
|
||||||
policy: hasAgentTools ? agentConfig?.tools : globalTools,
|
policy: hasAgentTools ? agentConfig?.tools : globalTools,
|
||||||
|
|
|
||||||
|
|
@ -56,18 +56,19 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
},
|
scope: "agent",
|
||||||
},
|
|
||||||
routing: {
|
|
||||||
agents: {
|
|
||||||
main: {
|
|
||||||
workspace: "~/clawd",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "main",
|
||||||
|
workspace: "~/clawd",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -85,18 +86,19 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
docker: {
|
scope: "agent",
|
||||||
setupCommand: "echo global",
|
docker: {
|
||||||
|
setupCommand: "echo global",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "work",
|
||||||
work: {
|
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -106,7 +108,7 @@ describe("Agent-specific sandbox config", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -133,18 +135,19 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "shared",
|
mode: "all",
|
||||||
docker: {
|
scope: "shared",
|
||||||
setupCommand: "echo global",
|
docker: {
|
||||||
|
setupCommand: "echo global",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "work",
|
||||||
work: {
|
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -154,7 +157,7 @@ describe("Agent-specific sandbox config", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -182,19 +185,20 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
docker: {
|
scope: "agent",
|
||||||
image: "global-image",
|
docker: {
|
||||||
network: "none",
|
image: "global-image",
|
||||||
|
network: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "work",
|
||||||
work: {
|
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -205,7 +209,7 @@ describe("Agent-specific sandbox config", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -224,21 +228,22 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all", // Global default
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all", // Global default
|
||||||
|
scope: "agent",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "main",
|
||||||
main: {
|
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "off", // Agent override
|
mode: "off", // Agent override
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -256,21 +261,22 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "off", // Global default
|
sandbox: {
|
||||||
|
mode: "off", // Global default
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "family",
|
||||||
family: {
|
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all", // Agent override
|
mode: "all", // Agent override
|
||||||
scope: "agent",
|
scope: "agent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -288,22 +294,23 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "session", // Global default
|
mode: "all",
|
||||||
|
scope: "session", // Global default
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "work",
|
||||||
work: {
|
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
scope: "agent", // Agent override
|
scope: "agent", // Agent override
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -322,16 +329,17 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes", // Global default
|
scope: "agent",
|
||||||
|
workspaceRoot: "~/.clawdbot/sandboxes", // Global default
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "isolated",
|
||||||
isolated: {
|
|
||||||
workspace: "~/clawd-isolated",
|
workspace: "~/clawd-isolated",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
|
@ -339,7 +347,7 @@ describe("Agent-specific sandbox config", () => {
|
||||||
workspaceRoot: "/tmp/isolated-sandboxes", // Agent override
|
workspaceRoot: "/tmp/isolated-sandboxes", // Agent override
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -359,28 +367,30 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "non-main",
|
sandbox: {
|
||||||
scope: "session",
|
mode: "non-main",
|
||||||
|
scope: "session",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "main",
|
||||||
main: {
|
|
||||||
workspace: "~/clawd",
|
workspace: "~/clawd",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "off", // main: no sandbox
|
mode: "off", // main: no sandbox
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
family: {
|
{
|
||||||
|
id: "family",
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all", // family: always sandbox
|
mode: "all", // family: always sandbox
|
||||||
scope: "agent",
|
scope: "agent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -406,29 +416,38 @@ describe("Agent-specific sandbox config", () => {
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agent: {
|
agents: {
|
||||||
sandbox: {
|
defaults: {
|
||||||
mode: "all",
|
sandbox: {
|
||||||
scope: "agent",
|
mode: "all",
|
||||||
tools: {
|
scope: "agent",
|
||||||
allow: ["read"],
|
|
||||||
deny: ["bash"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
list: [
|
||||||
routing: {
|
{
|
||||||
agents: {
|
id: "restricted",
|
||||||
restricted: {
|
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
scope: "agent",
|
scope: "agent",
|
||||||
tools: {
|
},
|
||||||
allow: ["read", "write"],
|
tools: {
|
||||||
deny: ["edit"],
|
sandbox: {
|
||||||
|
tools: {
|
||||||
|
allow: ["read", "write"],
|
||||||
|
deny: ["edit"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
sandbox: {
|
||||||
|
tools: {
|
||||||
|
allow: ["read"],
|
||||||
|
deny: ["bash"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ import {
|
||||||
import { normalizeAgentId } from "../routing/session-key.js";
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
import {
|
||||||
|
resolveAgentConfig,
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
|
} from "./agent-scope.js";
|
||||||
import { syncSkillsToWorkspace } from "./skills.js";
|
import { syncSkillsToWorkspace } from "./skills.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
|
|
@ -345,15 +348,14 @@ export function resolveSandboxConfigForAgent(
|
||||||
cfg?: ClawdbotConfig,
|
cfg?: ClawdbotConfig,
|
||||||
agentId?: string,
|
agentId?: string,
|
||||||
): SandboxConfig {
|
): SandboxConfig {
|
||||||
const agent = cfg?.agent?.sandbox;
|
const agent = cfg?.agents?.defaults?.sandbox;
|
||||||
|
|
||||||
// Agent-specific sandbox config overrides global
|
// Agent-specific sandbox config overrides global
|
||||||
let agentSandbox: typeof agent | undefined;
|
let agentSandbox: typeof agent | undefined;
|
||||||
if (agentId && cfg?.routing?.agents) {
|
const agentConfig =
|
||||||
const agentConfig = cfg.routing.agents[agentId];
|
cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||||
if (agentConfig && typeof agentConfig === "object") {
|
if (agentConfig?.sandbox) {
|
||||||
agentSandbox = agentConfig.sandbox;
|
agentSandbox = agentConfig.sandbox;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = resolveSandboxScope({
|
const scope = resolveSandboxScope({
|
||||||
|
|
@ -382,9 +384,13 @@ export function resolveSandboxConfigForAgent(
|
||||||
}),
|
}),
|
||||||
tools: {
|
tools: {
|
||||||
allow:
|
allow:
|
||||||
agentSandbox?.tools?.allow ?? agent?.tools?.allow ?? DEFAULT_TOOL_ALLOW,
|
agentConfig?.tools?.sandbox?.tools?.allow ??
|
||||||
|
cfg?.tools?.sandbox?.tools?.allow ??
|
||||||
|
DEFAULT_TOOL_ALLOW,
|
||||||
deny:
|
deny:
|
||||||
agentSandbox?.tools?.deny ?? agent?.tools?.deny ?? DEFAULT_TOOL_DENY,
|
agentConfig?.tools?.sandbox?.tools?.deny ??
|
||||||
|
cfg?.tools?.sandbox?.tools?.deny ??
|
||||||
|
DEFAULT_TOOL_DENY,
|
||||||
},
|
},
|
||||||
prune: resolveSandboxPruneConfig({
|
prune: resolveSandboxPruneConfig({
|
||||||
scope,
|
scope,
|
||||||
|
|
@ -1059,7 +1065,7 @@ export async function resolveSandboxContext(params: {
|
||||||
await ensureSandboxWorkspace(
|
await ensureSandboxWorkspace(
|
||||||
sandboxWorkspaceDir,
|
sandboxWorkspaceDir,
|
||||||
agentWorkspaceDir,
|
agentWorkspaceDir,
|
||||||
params.config?.agent?.skipBootstrap,
|
params.config?.agents?.defaults?.skipBootstrap,
|
||||||
);
|
);
|
||||||
if (cfg.workspaceAccess === "none") {
|
if (cfg.workspaceAccess === "none") {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1133,7 +1139,7 @@ export async function ensureSandboxWorkspaceForSession(params: {
|
||||||
await ensureSandboxWorkspace(
|
await ensureSandboxWorkspace(
|
||||||
sandboxWorkspaceDir,
|
sandboxWorkspaceDir,
|
||||||
agentWorkspaceDir,
|
agentWorkspaceDir,
|
||||||
params.config?.agent?.skipBootstrap,
|
params.config?.agents?.defaults?.skipBootstrap,
|
||||||
);
|
);
|
||||||
if (cfg.workspaceAccess === "none") {
|
if (cfg.workspaceAccess === "none") {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ let listenerStarted = false;
|
||||||
|
|
||||||
function resolveArchiveAfterMs() {
|
function resolveArchiveAfterMs() {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const minutes = cfg.agent?.subagents?.archiveAfterMinutes ?? 60;
|
const minutes = cfg.agents?.defaults?.subagents?.archiveAfterMinutes ?? 60;
|
||||||
if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
|
if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
|
||||||
return Math.max(1, Math.floor(minutes)) * 60_000;
|
return Math.max(1, Math.floor(minutes)) * 60_000;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const normalizeNumber = (value: unknown): number | undefined =>
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
export function resolveAgentTimeoutSeconds(cfg?: ClawdbotConfig): number {
|
export function resolveAgentTimeoutSeconds(cfg?: ClawdbotConfig): number {
|
||||||
const raw = normalizeNumber(cfg?.agent?.timeoutSeconds);
|
const raw = normalizeNumber(cfg?.agents?.defaults?.timeoutSeconds);
|
||||||
const seconds = raw ?? DEFAULT_AGENT_TIMEOUT_SECONDS;
|
const seconds = raw ?? DEFAULT_AGENT_TIMEOUT_SECONDS;
|
||||||
return Math.max(seconds, 1);
|
return Math.max(seconds, 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,17 @@ export function createAgentsListTool(opts?: {
|
||||||
.map((value) => normalizeAgentId(value)),
|
.map((value) => normalizeAgentId(value)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const configuredAgents = cfg.routing?.agents ?? {};
|
const configuredAgents = Array.isArray(cfg.agents?.list)
|
||||||
const configuredIds = Object.keys(configuredAgents).map((key) =>
|
? cfg.agents?.list
|
||||||
normalizeAgentId(key),
|
: [];
|
||||||
|
const configuredIds = configuredAgents.map((entry) =>
|
||||||
|
normalizeAgentId(entry.id),
|
||||||
);
|
);
|
||||||
const configuredNameMap = new Map<string, string>();
|
const configuredNameMap = new Map<string, string>();
|
||||||
for (const [key, value] of Object.entries(configuredAgents)) {
|
for (const entry of configuredAgents) {
|
||||||
if (!value || typeof value !== "object") continue;
|
const name = entry?.name?.trim() ?? "";
|
||||||
const name =
|
|
||||||
typeof (value as { name?: unknown }).name === "string"
|
|
||||||
? ((value as { name?: string }).name?.trim() ?? "")
|
|
||||||
: "";
|
|
||||||
if (!name) continue;
|
if (!name) continue;
|
||||||
configuredNameMap.set(normalizeAgentId(key), name);
|
configuredNameMap.set(normalizeAgentId(entry.id), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = new Set<string>();
|
const allowed = new Set<string>();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import type { AnyAgentTool } from "./common.js";
|
||||||
const DEFAULT_PROMPT = "Describe the image.";
|
const DEFAULT_PROMPT = "Describe the image.";
|
||||||
|
|
||||||
function ensureImageToolConfigured(cfg?: ClawdbotConfig): boolean {
|
function ensureImageToolConfigured(cfg?: ClawdbotConfig): boolean {
|
||||||
const imageModel = cfg?.agent?.imageModel as
|
const imageModel = cfg?.agents?.defaults?.imageModel as
|
||||||
| { primary?: string; fallbacks?: string[] }
|
| { primary?: string; fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
@ -45,7 +45,7 @@ function pickMaxBytes(
|
||||||
) {
|
) {
|
||||||
return Math.floor(maxBytesMb * 1024 * 1024);
|
return Math.floor(maxBytesMb * 1024 * 1024);
|
||||||
}
|
}
|
||||||
const configured = cfg?.agent?.mediaMaxMb;
|
const configured = cfg?.agents?.defaults?.mediaMaxMb;
|
||||||
if (
|
if (
|
||||||
typeof configured === "number" &&
|
typeof configured === "number" &&
|
||||||
Number.isFinite(configured) &&
|
Number.isFinite(configured) &&
|
||||||
|
|
@ -141,7 +141,7 @@ export function createImageTool(options?: {
|
||||||
label: "Image",
|
label: "Image",
|
||||||
name: "image",
|
name: "image",
|
||||||
description:
|
description:
|
||||||
"Analyze an image with the configured image model (agent.imageModel). Provide a prompt and image path or URL.",
|
"Analyze an image with the configured image model (agents.defaults.imageModel). Provide a prompt and image path or URL.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
prompt: Type.Optional(Type.String()),
|
prompt: Type.Optional(Type.String()),
|
||||||
image: Type.String(),
|
image: Type.String(),
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const SessionsHistoryToolSchema = Type.Object({
|
||||||
function resolveSandboxSessionToolsVisibility(
|
function resolveSandboxSessionToolsVisibility(
|
||||||
cfg: ReturnType<typeof loadConfig>,
|
cfg: ReturnType<typeof loadConfig>,
|
||||||
) {
|
) {
|
||||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isSpawnedSessionAllowed(params: {
|
async function isSpawnedSessionAllowed(params: {
|
||||||
|
|
@ -97,7 +97,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routingA2A = cfg.routing?.agentToAgent;
|
const routingA2A = cfg.tools?.agentToAgent;
|
||||||
const a2aEnabled = routingA2A?.enabled === true;
|
const a2aEnabled = routingA2A?.enabled === true;
|
||||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||||
? routingA2A.allow
|
? routingA2A.allow
|
||||||
|
|
@ -126,14 +126,13 @@ export function createSessionsHistoryTool(opts?: {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error:
|
error:
|
||||||
"Agent-to-agent history is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent access.",
|
"Agent-to-agent history is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
|
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error:
|
error: "Agent-to-agent history denied by tools.agentToAgent.allow.",
|
||||||
"Agent-to-agent history denied by routing.agentToAgent.allow.",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
||||||
loadConfig: () =>
|
loadConfig: () =>
|
||||||
({
|
({
|
||||||
session: { scope: "per-sender", mainKey: "main" },
|
session: { scope: "per-sender", mainKey: "main" },
|
||||||
routing: { agentToAgent: { enabled: false } },
|
tools: { agentToAgent: { enabled: false } },
|
||||||
}) as never,
|
}) as never,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -32,7 +32,7 @@ describe("sessions_list gating", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters out other agents when routing.agentToAgent.enabled is false", async () => {
|
it("filters out other agents when tools.agentToAgent.enabled is false", async () => {
|
||||||
const tool = createSessionsListTool({ agentSessionKey: "agent:main:main" });
|
const tool = createSessionsListTool({ agentSessionKey: "agent:main:main" });
|
||||||
const result = await tool.execute("call1", {});
|
const result = await tool.execute("call1", {});
|
||||||
expect(result.details).toMatchObject({
|
expect(result.details).toMatchObject({
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const SessionsListToolSchema = Type.Object({
|
||||||
function resolveSandboxSessionToolsVisibility(
|
function resolveSandboxSessionToolsVisibility(
|
||||||
cfg: ReturnType<typeof loadConfig>,
|
cfg: ReturnType<typeof loadConfig>,
|
||||||
) {
|
) {
|
||||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionsListTool(opts?: {
|
export function createSessionsListTool(opts?: {
|
||||||
|
|
@ -126,7 +126,7 @@ export function createSessionsListTool(opts?: {
|
||||||
|
|
||||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||||
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
||||||
const routingA2A = cfg.routing?.agentToAgent;
|
const routingA2A = cfg.tools?.agentToAgent;
|
||||||
const a2aEnabled = routingA2A?.enabled === true;
|
const a2aEnabled = routingA2A?.enabled === true;
|
||||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||||
? routingA2A.allow
|
? routingA2A.allow
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
||||||
loadConfig: () =>
|
loadConfig: () =>
|
||||||
({
|
({
|
||||||
session: { scope: "per-sender", mainKey: "main" },
|
session: { scope: "per-sender", mainKey: "main" },
|
||||||
routing: { agentToAgent: { enabled: false } },
|
tools: { agentToAgent: { enabled: false } },
|
||||||
}) as never,
|
}) as never,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -25,7 +25,7 @@ describe("sessions_send gating", () => {
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks cross-agent sends when routing.agentToAgent.enabled is false", async () => {
|
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
|
||||||
const tool = createSessionsSendTool({
|
const tool = createSessionsSendTool({
|
||||||
agentSessionKey: "agent:main:main",
|
agentSessionKey: "agent:main:main",
|
||||||
agentProvider: "whatsapp",
|
agentProvider: "whatsapp",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function createSessionsSendTool(opts?: {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||||
const visibility =
|
const visibility =
|
||||||
cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||||
const requesterInternalKey =
|
const requesterInternalKey =
|
||||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||||
? resolveInternalSessionKey({
|
? resolveInternalSessionKey({
|
||||||
|
|
@ -126,7 +126,7 @@ export function createSessionsSendTool(opts?: {
|
||||||
mainKey,
|
mainKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const routingA2A = cfg.routing?.agentToAgent;
|
const routingA2A = cfg.tools?.agentToAgent;
|
||||||
const a2aEnabled = routingA2A?.enabled === true;
|
const a2aEnabled = routingA2A?.enabled === true;
|
||||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||||
? routingA2A.allow
|
? routingA2A.allow
|
||||||
|
|
@ -156,7 +156,7 @@ export function createSessionsSendTool(opts?: {
|
||||||
runId: crypto.randomUUID(),
|
runId: crypto.randomUUID(),
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error:
|
error:
|
||||||
"Agent-to-agent messaging is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent sends.",
|
"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.",
|
||||||
sessionKey: displayKey,
|
sessionKey: displayKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +165,7 @@ export function createSessionsSendTool(opts?: {
|
||||||
runId: crypto.randomUUID(),
|
runId: crypto.randomUUID(),
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error:
|
error:
|
||||||
"Agent-to-agent messaging denied by routing.agentToAgent.allow.",
|
"Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||||
sessionKey: displayKey,
|
sessionKey: displayKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,11 @@ describe("block streaming", () => {
|
||||||
onBlockReply,
|
onBlockReply,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -140,9 +142,11 @@ describe("block streaming", () => {
|
||||||
onBlockReply,
|
onBlockReply,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
telegram: { allowFrom: ["*"] },
|
telegram: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -185,9 +189,11 @@ describe("block streaming", () => {
|
||||||
onBlockReply,
|
onBlockReply,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -239,9 +245,11 @@ describe("block streaming", () => {
|
||||||
blockReplyTimeoutMs: 10,
|
blockReplyTimeoutMs: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
telegram: { allowFrom: ["*"] },
|
telegram: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": { alias: " help " },
|
models: {
|
||||||
|
"anthropic/claude-opus-4-5": { alias: " help " },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
|
|
@ -108,9 +110,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -138,11 +142,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
routing: {
|
messages: {
|
||||||
queue: {
|
queue: {
|
||||||
mode: "collect",
|
mode: "collect",
|
||||||
debounceMs: 1500,
|
debounceMs: 1500,
|
||||||
|
|
@ -174,10 +180,12 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
thinkingDefault: "high",
|
workspace: path.join(home, "clawd"),
|
||||||
|
thinkingDefault: "high",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -198,9 +206,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -232,9 +242,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -270,9 +282,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -303,9 +317,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -330,9 +346,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/verbose on", From: "+1222", To: "+1222" },
|
{ Body: "/verbose on", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -352,10 +370,12 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
thinkingDefault: "high",
|
workspace: path.join(home, "clawd"),
|
||||||
|
thinkingDefault: "high",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -376,9 +396,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/think", From: "+1222", To: "+1222" },
|
{ Body: "/think", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -399,10 +421,12 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/verbose", From: "+1222", To: "+1222" },
|
{ Body: "/verbose", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
verboseDefault: "on",
|
workspace: path.join(home, "clawd"),
|
||||||
|
verboseDefault: "on",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -423,9 +447,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/reasoning", From: "+1222", To: "+1222" },
|
{ Body: "/reasoning", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
},
|
},
|
||||||
|
|
@ -452,10 +478,14 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
elevatedDefault: "on",
|
workspace: path.join(home, "clawd"),
|
||||||
|
elevatedDefault: "on",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1222"] },
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
},
|
},
|
||||||
|
|
@ -486,13 +516,17 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
sandbox: { mode: "off" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1222"] },
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
},
|
},
|
||||||
sandbox: { mode: "off" },
|
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["+1222"] },
|
whatsapp: { allowFrom: ["+1222"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
@ -520,9 +554,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1222"] },
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
},
|
},
|
||||||
|
|
@ -552,9 +590,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1222"] },
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
},
|
},
|
||||||
|
|
@ -585,9 +627,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1222"] },
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
},
|
},
|
||||||
|
|
@ -613,9 +659,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -644,9 +692,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -677,9 +727,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -690,9 +742,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/queue reset", From: "+1222", To: "+1222" },
|
{ Body: "/queue reset", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -749,9 +803,11 @@ describe("directive behavior", () => {
|
||||||
ctx,
|
ctx,
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -810,9 +866,11 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/verbose on", From: ctx.From, To: ctx.To },
|
{ Body: "/verbose on", From: ctx.From, To: ctx.To },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -825,9 +883,11 @@ describe("directive behavior", () => {
|
||||||
ctx,
|
ctx,
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -853,12 +913,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model", From: "+1222", To: "+1222" },
|
{ Body: "/model", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -883,12 +945,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model status", From: "+1222", To: "+1222" },
|
{ Body: "/model status", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -913,12 +977,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model list", From: "+1222", To: "+1222" },
|
{ Body: "/model list", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -943,12 +1009,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model", From: "+1222", To: "+1222" },
|
{ Body: "/model", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -972,11 +1040,13 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model list", From: "+1222", To: "+1222" },
|
{ Body: "/model list", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -999,12 +1069,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
|
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -1030,12 +1102,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "openai/gpt-4.1-mini" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "openai/gpt-4.1-mini" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"openai/gpt-4.1-mini": {},
|
models: {
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
"openai/gpt-4.1-mini": {},
|
||||||
|
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -1081,12 +1155,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
|
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "openai/gpt-4.1-mini" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "openai/gpt-4.1-mini" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"openai/gpt-4.1-mini": {},
|
models: {
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
"openai/gpt-4.1-mini": {},
|
||||||
|
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -1112,12 +1188,14 @@ describe("directive behavior", () => {
|
||||||
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
{ Body: "/model Opus", From: "+1222", To: "+1222" },
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "openai/gpt-4.1-mini" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "openai/gpt-4.1-mini" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"openai/gpt-4.1-mini": {},
|
models: {
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
"openai/gpt-4.1-mini": {},
|
||||||
|
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: { store: storePath },
|
session: { store: storePath },
|
||||||
|
|
@ -1151,12 +1229,14 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: { primary: "anthropic/claude-opus-4-5" },
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
models: {
|
workspace: path.join(home, "clawd"),
|
||||||
"anthropic/claude-opus-4-5": {},
|
models: {
|
||||||
"openai/gpt-4.1-mini": {},
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
|
|
@ -1204,9 +1284,11 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -1242,9 +1324,13 @@ describe("directive behavior", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1004"] },
|
allowFrom: { whatsapp: ["+1004"] },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
function makeCfg(home: string) {
|
||||||
return {
|
return {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
function makeCfg(home: string) {
|
||||||
return {
|
return {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,15 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
|
|
||||||
function makeCfg(home: string, queue?: Record<string, unknown>) {
|
function makeCfg(home: string, queue?: Record<string, unknown>) {
|
||||||
return {
|
return {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: path.join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: { allowFrom: ["*"] },
|
whatsapp: { allowFrom: ["*"] },
|
||||||
session: { store: path.join(home, "sessions.json") },
|
session: { store: path.join(home, "sessions.json") },
|
||||||
routing: queue ? { queue } : undefined,
|
messages: queue ? { queue } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,18 @@ const usageMocks = vi.hoisted(() => ({
|
||||||
|
|
||||||
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
||||||
|
|
||||||
|
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||||
import {
|
import {
|
||||||
abortEmbeddedPiRun,
|
abortEmbeddedPiRun,
|
||||||
compactEmbeddedPiSession,
|
compactEmbeddedPiSession,
|
||||||
runEmbeddedPiAgent,
|
runEmbeddedPiAgent,
|
||||||
} from "../agents/pi-embedded.js";
|
} from "../agents/pi-embedded.js";
|
||||||
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
||||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
import {
|
||||||
|
loadSessionStore,
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
|
resolveSessionKey,
|
||||||
|
} from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
|
|
@ -61,9 +66,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||||
|
|
||||||
function makeCfg(home: string) {
|
function makeCfg(home: string) {
|
||||||
return {
|
return {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -345,9 +352,11 @@ describe("trigger handling", () => {
|
||||||
it("allows owner to set send policy", async () => {
|
it("allows owner to set send policy", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1000"],
|
allowFrom: ["+1000"],
|
||||||
|
|
@ -381,9 +390,13 @@ describe("trigger handling", () => {
|
||||||
it("allows approved sender to toggle elevated mode", async () => {
|
it("allows approved sender to toggle elevated mode", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -420,9 +433,13 @@ describe("trigger handling", () => {
|
||||||
it("rejects elevated toggles when disabled", async () => {
|
it("rejects elevated toggles when disabled", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
|
|
@ -467,9 +484,13 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -510,9 +531,13 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -545,9 +570,13 @@ describe("trigger handling", () => {
|
||||||
it("allows elevated directive in groups when mentioned", async () => {
|
it("allows elevated directive in groups when mentioned", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -589,9 +618,13 @@ describe("trigger handling", () => {
|
||||||
it("allows elevated directive in direct chats without mentions", async () => {
|
it("allows elevated directive in direct chats without mentions", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -635,9 +668,13 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { whatsapp: ["+1000"] },
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
},
|
},
|
||||||
|
|
@ -668,9 +705,11 @@ describe("trigger handling", () => {
|
||||||
it("falls back to discord dm allowFrom for elevated approval", async () => {
|
it("falls back to discord dm allowFrom for elevated approval", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
dm: {
|
dm: {
|
||||||
|
|
@ -708,9 +747,13 @@ describe("trigger handling", () => {
|
||||||
it("treats explicit discord elevated allowlist as override", async () => {
|
it("treats explicit discord elevated allowlist as override", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowFrom: { discord: [] },
|
allowFrom: { discord: [] },
|
||||||
},
|
},
|
||||||
|
|
@ -799,9 +842,12 @@ describe("trigger handling", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const cfg = makeCfg(home);
|
const cfg = makeCfg(home);
|
||||||
cfg.agent = {
|
cfg.agents = {
|
||||||
...cfg.agent,
|
...cfg.agents,
|
||||||
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
defaults: {
|
||||||
|
...cfg.agents?.defaults,
|
||||||
|
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await getReplyFromConfig(
|
await getReplyFromConfig(
|
||||||
|
|
@ -941,15 +987,17 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
groups: { "*": { requireMention: false } },
|
groups: { "*": { requireMention: false } },
|
||||||
},
|
},
|
||||||
routing: {
|
messages: {
|
||||||
groupChat: {},
|
groupChat: {},
|
||||||
},
|
},
|
||||||
session: { store: join(home, "sessions.json") },
|
session: { store: join(home, "sessions.json") },
|
||||||
|
|
@ -985,9 +1033,11 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -1024,9 +1074,11 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -1056,9 +1108,11 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1999"],
|
allowFrom: ["+1999"],
|
||||||
|
|
@ -1083,9 +1137,11 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["+1999"],
|
allowFrom: ["+1999"],
|
||||||
|
|
@ -1124,9 +1180,11 @@ describe("trigger handling", () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
allowFrom: ["*"],
|
allowFrom: ["*"],
|
||||||
|
|
@ -1229,12 +1287,14 @@ describe("trigger handling", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
agent: {
|
agents: {
|
||||||
model: "anthropic/claude-opus-4-5",
|
defaults: {
|
||||||
workspace: join(home, "clawd"),
|
model: "anthropic/claude-opus-4-5",
|
||||||
sandbox: {
|
workspace: join(home, "clawd"),
|
||||||
mode: "non-main" as const,
|
sandbox: {
|
||||||
workspaceRoot: join(home, "sandboxes"),
|
mode: "non-main" as const,
|
||||||
|
workspaceRoot: join(home, "sandboxes"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
|
|
@ -1272,10 +1332,11 @@ describe("trigger handling", () => {
|
||||||
ctx,
|
ctx,
|
||||||
cfg.session?.mainKey,
|
cfg.session?.mainKey,
|
||||||
);
|
);
|
||||||
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||||
const sandbox = await ensureSandboxWorkspaceForSession({
|
const sandbox = await ensureSandboxWorkspaceForSession({
|
||||||
config: cfg,
|
config: cfg,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
workspaceDir: cfg.agent.workspace,
|
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
|
||||||
});
|
});
|
||||||
expect(sandbox).not.toBeNull();
|
expect(sandbox).not.toBeNull();
|
||||||
if (!sandbox) {
|
if (!sandbox) {
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export async function getReplyFromConfig(
|
||||||
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
||||||
const cfg = configOverride ?? loadConfig();
|
const cfg = configOverride ?? loadConfig();
|
||||||
const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey);
|
const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey);
|
||||||
const agentCfg = cfg.agent;
|
const agentCfg = cfg.agents?.defaults;
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
|
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
|
||||||
cfg,
|
cfg,
|
||||||
|
|
@ -239,7 +239,7 @@ export async function getReplyFromConfig(
|
||||||
resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||||
const workspace = await ensureAgentWorkspace({
|
const workspace = await ensureAgentWorkspace({
|
||||||
dir: workspaceDirRaw,
|
dir: workspaceDirRaw,
|
||||||
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
|
||||||
});
|
});
|
||||||
const workspaceDir = workspace.dir;
|
const workspaceDir = workspace.dir;
|
||||||
const agentDir = resolveAgentDir(cfg, agentId);
|
const agentDir = resolveAgentDir(cfg, agentId);
|
||||||
|
|
@ -257,7 +257,7 @@ export async function getReplyFromConfig(
|
||||||
opts?.onTypingController?.(typing);
|
opts?.onTypingController?.(typing);
|
||||||
|
|
||||||
let transcribedText: string | undefined;
|
let transcribedText: string | undefined;
|
||||||
if (cfg.routing?.transcribeAudio && isAudio(ctx.MediaType)) {
|
if (cfg.audio?.transcription && isAudio(ctx.MediaType)) {
|
||||||
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
|
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
|
||||||
if (transcribed?.text) {
|
if (transcribed?.text) {
|
||||||
transcribedText = transcribed.text;
|
transcribedText = transcribed.text;
|
||||||
|
|
@ -329,7 +329,7 @@ export async function getReplyFromConfig(
|
||||||
cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
|
cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const configuredAliases = Object.values(cfg.agent?.models ?? {})
|
const configuredAliases = Object.values(cfg.agents?.defaults?.models ?? {})
|
||||||
.map((entry) => entry.alias?.trim())
|
.map((entry) => entry.alias?.trim())
|
||||||
.filter((alias): alias is string => Boolean(alias))
|
.filter((alias): alias is string => Boolean(alias))
|
||||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||||
|
|
@ -391,7 +391,7 @@ export async function getReplyFromConfig(
|
||||||
sessionCtx.Provider?.trim().toLowerCase() ??
|
sessionCtx.Provider?.trim().toLowerCase() ??
|
||||||
ctx.Provider?.trim().toLowerCase() ??
|
ctx.Provider?.trim().toLowerCase() ??
|
||||||
"";
|
"";
|
||||||
const elevatedConfig = agentCfg?.elevated;
|
const elevatedConfig = cfg.tools?.elevated;
|
||||||
const discordElevatedFallback =
|
const discordElevatedFallback =
|
||||||
messageProviderKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
|
messageProviderKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
|
||||||
const elevatedEnabled = elevatedConfig?.enabled !== false;
|
const elevatedEnabled = elevatedConfig?.enabled !== false;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function resolveBlockStreamingChunking(
|
||||||
} {
|
} {
|
||||||
const providerKey = normalizeChunkProvider(provider);
|
const providerKey = normalizeChunkProvider(provider);
|
||||||
const textLimit = resolveTextChunkLimit(cfg, providerKey);
|
const textLimit = resolveTextChunkLimit(cfg, providerKey);
|
||||||
const chunkCfg = cfg?.agent?.blockStreamingChunk;
|
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||||
const maxRequested = Math.max(
|
const maxRequested = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX),
|
Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX),
|
||||||
|
|
|
||||||
|
|
@ -163,18 +163,19 @@ export async function buildStatusReply(params: {
|
||||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
||||||
defaultGroupActivation())
|
defaultGroupActivation())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
const statusText = buildStatusMessage({
|
const statusText = buildStatusMessage({
|
||||||
config: cfg,
|
config: cfg,
|
||||||
agent: {
|
agent: {
|
||||||
...cfg.agent,
|
...agentDefaults,
|
||||||
model: {
|
model: {
|
||||||
...cfg.agent?.model,
|
...agentDefaults.model,
|
||||||
primary: `${provider}/${model}`,
|
primary: `${provider}/${model}`,
|
||||||
},
|
},
|
||||||
contextTokens,
|
contextTokens,
|
||||||
thinkingDefault: cfg.agent?.thinkingDefault,
|
thinkingDefault: agentDefaults.thinkingDefault,
|
||||||
verboseDefault: cfg.agent?.verboseDefault,
|
verboseDefault: agentDefaults.verboseDefault,
|
||||||
elevatedDefault: cfg.agent?.elevatedDefault,
|
elevatedDefault: agentDefaults.elevatedDefault,
|
||||||
},
|
},
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
resolveConfiguredModelRef,
|
resolveConfiguredModelRef,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
|
import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
|
|
@ -363,16 +364,16 @@ export async function handleDirectiveOnly(params: {
|
||||||
currentElevatedLevel,
|
currentElevatedLevel,
|
||||||
} = params;
|
} = params;
|
||||||
const runtimeIsSandboxed = (() => {
|
const runtimeIsSandboxed = (() => {
|
||||||
const sandboxMode = params.cfg.agent?.sandbox?.mode ?? "off";
|
|
||||||
if (sandboxMode === "off") return false;
|
|
||||||
const sessionKey = params.sessionKey?.trim();
|
const sessionKey = params.sessionKey?.trim();
|
||||||
if (!sessionKey) return false;
|
if (!sessionKey) return false;
|
||||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||||
|
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
|
||||||
|
if (sandboxCfg.mode === "off") return false;
|
||||||
const mainKey = resolveAgentMainSessionKey({
|
const mainKey = resolveAgentMainSessionKey({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId,
|
agentId,
|
||||||
});
|
});
|
||||||
if (sandboxMode === "all") return true;
|
if (sandboxCfg.mode === "all") return true;
|
||||||
return sessionKey !== mainKey;
|
return sessionKey !== mainKey;
|
||||||
})();
|
})();
|
||||||
const shouldHintDirectRuntime =
|
const shouldHintDirectRuntime =
|
||||||
|
|
@ -394,7 +395,9 @@ export async function handleDirectiveOnly(params: {
|
||||||
provider: string;
|
provider: string;
|
||||||
id: string;
|
id: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
for (const raw of Object.keys(params.cfg.agent?.models ?? {})) {
|
for (const raw of Object.keys(
|
||||||
|
params.cfg.agents?.defaults?.models ?? {},
|
||||||
|
)) {
|
||||||
const resolved = resolveModelRefFromString({
|
const resolved = resolveModelRefFromString({
|
||||||
raw: String(raw),
|
raw: String(raw),
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
|
|
@ -851,7 +854,7 @@ export async function persistInlineDirectives(params: {
|
||||||
model: string;
|
model: string;
|
||||||
initialModelLabel: string;
|
initialModelLabel: string;
|
||||||
formatModelSwitchEvent: (label: string, alias?: string) => string;
|
formatModelSwitchEvent: (label: string, alias?: string) => string;
|
||||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
agentCfg: NonNullable<ClawdbotConfig["agents"]>["defaults"] | undefined;
|
||||||
}): Promise<{ provider: string; model: string; contextTokens: number }> {
|
}): Promise<{ provider: string; model: string; contextTokens: number }> {
|
||||||
const {
|
const {
|
||||||
directives,
|
directives,
|
||||||
|
|
@ -1007,13 +1010,16 @@ export function resolveDefaultModel(params: {
|
||||||
agentModelOverride && agentModelOverride.length > 0
|
agentModelOverride && agentModelOverride.length > 0
|
||||||
? {
|
? {
|
||||||
...params.cfg,
|
...params.cfg,
|
||||||
agent: {
|
agents: {
|
||||||
...params.cfg.agent,
|
...params.cfg.agents,
|
||||||
model: {
|
defaults: {
|
||||||
...(typeof params.cfg.agent?.model === "object"
|
...params.cfg.agents?.defaults,
|
||||||
? params.cfg.agent.model
|
model: {
|
||||||
: undefined),
|
...(typeof params.cfg.agents?.defaults?.model === "object"
|
||||||
primary: agentModelOverride,
|
? params.cfg.agents.defaults.model
|
||||||
|
: undefined),
|
||||||
|
primary: agentModelOverride,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
describe("mention helpers", () => {
|
describe("mention helpers", () => {
|
||||||
it("builds regexes and skips invalid patterns", () => {
|
it("builds regexes and skips invalid patterns", () => {
|
||||||
const regexes = buildMentionRegexes({
|
const regexes = buildMentionRegexes({
|
||||||
routing: {
|
messages: {
|
||||||
groupChat: { mentionPatterns: ["\\bclawd\\b", "(invalid"] },
|
groupChat: { mentionPatterns: ["\\bclawd\\b", "(invalid"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -23,7 +23,7 @@ describe("mention helpers", () => {
|
||||||
|
|
||||||
it("matches patterns case-insensitively", () => {
|
it("matches patterns case-insensitively", () => {
|
||||||
const regexes = buildMentionRegexes({
|
const regexes = buildMentionRegexes({
|
||||||
routing: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
|
messages: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
|
||||||
});
|
});
|
||||||
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
|
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -31,11 +31,16 @@ describe("mention helpers", () => {
|
||||||
it("uses per-agent mention patterns when configured", () => {
|
it("uses per-agent mention patterns when configured", () => {
|
||||||
const regexes = buildMentionRegexes(
|
const regexes = buildMentionRegexes(
|
||||||
{
|
{
|
||||||
routing: {
|
messages: {
|
||||||
groupChat: { mentionPatterns: ["\\bglobal\\b"] },
|
groupChat: { mentionPatterns: ["\\bglobal\\b"] },
|
||||||
agents: {
|
},
|
||||||
work: { mentionPatterns: ["\\bworkbot\\b"] },
|
agents: {
|
||||||
},
|
list: [
|
||||||
|
{
|
||||||
|
id: "work",
|
||||||
|
groupChat: { mentionPatterns: ["\\bworkbot\\b"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"work",
|
"work",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,62 @@
|
||||||
|
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
|
|
||||||
|
function escapeRegExp(text: string): string {
|
||||||
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const name = identity?.name?.trim();
|
||||||
|
if (name) {
|
||||||
|
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
|
||||||
|
const re = parts.length ? parts.join(String.raw`\s+`) : escapeRegExp(name);
|
||||||
|
patterns.push(String.raw`\b@?${re}\b`);
|
||||||
|
}
|
||||||
|
const emoji = identity?.emoji?.trim();
|
||||||
|
if (emoji) {
|
||||||
|
patterns.push(escapeRegExp(emoji));
|
||||||
|
}
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKSPACE_CHAR = "\u0008";
|
||||||
|
|
||||||
|
function normalizeMentionPattern(pattern: string): string {
|
||||||
|
if (!pattern.includes(BACKSPACE_CHAR)) return pattern;
|
||||||
|
return pattern.split(BACKSPACE_CHAR).join("\\b");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMentionPatterns(patterns: string[]): string[] {
|
||||||
|
return patterns.map(normalizeMentionPattern);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveMentionPatterns(
|
function resolveMentionPatterns(
|
||||||
cfg: ClawdbotConfig | undefined,
|
cfg: ClawdbotConfig | undefined,
|
||||||
agentId?: string,
|
agentId?: string,
|
||||||
): string[] {
|
): string[] {
|
||||||
if (!cfg) return [];
|
if (!cfg) return [];
|
||||||
const agentConfig = agentId ? cfg.routing?.agents?.[agentId] : undefined;
|
const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||||
if (agentConfig && Object.hasOwn(agentConfig, "mentionPatterns")) {
|
const agentGroupChat = agentConfig?.groupChat;
|
||||||
return agentConfig.mentionPatterns ?? [];
|
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
|
||||||
|
return agentGroupChat.mentionPatterns ?? [];
|
||||||
}
|
}
|
||||||
return cfg.routing?.groupChat?.mentionPatterns ?? [];
|
const globalGroupChat = cfg.messages?.groupChat;
|
||||||
|
if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
|
||||||
|
return globalGroupChat.mentionPatterns ?? [];
|
||||||
|
}
|
||||||
|
const derived = deriveMentionPatterns(agentConfig?.identity);
|
||||||
|
return derived.length > 0 ? derived : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMentionRegexes(
|
export function buildMentionRegexes(
|
||||||
cfg: ClawdbotConfig | undefined,
|
cfg: ClawdbotConfig | undefined,
|
||||||
agentId?: string,
|
agentId?: string,
|
||||||
): RegExp[] {
|
): RegExp[] {
|
||||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
const patterns = normalizeMentionPatterns(
|
||||||
|
resolveMentionPatterns(cfg, agentId),
|
||||||
|
);
|
||||||
return patterns
|
return patterns
|
||||||
.map((pattern) => {
|
.map((pattern) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -66,7 +105,9 @@ export function stripMentions(
|
||||||
agentId?: string,
|
agentId?: string,
|
||||||
): string {
|
): string {
|
||||||
let result = text;
|
let result = text;
|
||||||
const patterns = resolveMentionPatterns(cfg, agentId);
|
const patterns = normalizeMentionPatterns(
|
||||||
|
resolveMentionPatterns(cfg, agentId),
|
||||||
|
);
|
||||||
for (const p of patterns) {
|
for (const p of patterns) {
|
||||||
try {
|
try {
|
||||||
const re = new RegExp(p, "gi");
|
const re = new RegExp(p, "gi");
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ type ModelSelectionState = {
|
||||||
|
|
||||||
export async function createModelSelectionState(params: {
|
export async function createModelSelectionState(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
agentCfg:
|
||||||
|
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||||
|
| undefined;
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
sessionStore?: Record<string, SessionEntry>;
|
sessionStore?: Record<string, SessionEntry>;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
|
@ -201,7 +203,9 @@ export function resolveModelDirectiveSelection(params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveContextTokens(params: {
|
export function resolveContextTokens(params: {
|
||||||
agentCfg: ClawdbotConfig["agent"] | undefined;
|
agentCfg:
|
||||||
|
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||||
|
| undefined;
|
||||||
model: string;
|
model: string;
|
||||||
}): number {
|
}): number {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -553,7 +553,7 @@ export function resolveQueueSettings(params: {
|
||||||
inlineOptions?: Partial<QueueSettings>;
|
inlineOptions?: Partial<QueueSettings>;
|
||||||
}): QueueSettings {
|
}): QueueSettings {
|
||||||
const providerKey = params.provider?.trim().toLowerCase();
|
const providerKey = params.provider?.trim().toLowerCase();
|
||||||
const queueCfg = params.cfg.routing?.queue;
|
const queueCfg = params.cfg.messages?.queue;
|
||||||
const providerModeRaw =
|
const providerModeRaw =
|
||||||
providerKey && queueCfg?.byProvider
|
providerKey && queueCfg?.byProvider
|
||||||
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
|
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ import type {
|
||||||
VerboseLevel,
|
VerboseLevel,
|
||||||
} from "./thinking.js";
|
} from "./thinking.js";
|
||||||
|
|
||||||
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
type AgentConfig = Partial<
|
||||||
|
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
|
||||||
|
>;
|
||||||
|
|
||||||
export const formatTokenCount = formatTokenCountShared;
|
export const formatTokenCount = formatTokenCountShared;
|
||||||
|
|
||||||
|
|
@ -188,7 +190,11 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||||
const now = args.now ?? Date.now();
|
const now = args.now ?? Date.now();
|
||||||
const entry = args.sessionEntry;
|
const entry = args.sessionEntry;
|
||||||
const resolved = resolveConfiguredModelRef({
|
const resolved = resolveConfiguredModelRef({
|
||||||
cfg: { agent: args.agent ?? {} },
|
cfg: {
|
||||||
|
agents: {
|
||||||
|
defaults: args.agent ?? {},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig,
|
||||||
defaultProvider: DEFAULT_PROVIDER,
|
defaultProvider: DEFAULT_PROVIDER,
|
||||||
defaultModel: DEFAULT_MODEL,
|
defaultModel: DEFAULT_MODEL,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ describe("transcribeInboundAudio", () => {
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
const cfg = {
|
const cfg = {
|
||||||
routing: {
|
audio: {
|
||||||
transcribeAudio: {
|
transcription: {
|
||||||
command: ["echo", "{{MediaPath}}"],
|
command: ["echo", "{{MediaPath}}"],
|
||||||
timeoutSeconds: 5,
|
timeoutSeconds: 5,
|
||||||
},
|
},
|
||||||
|
|
@ -64,7 +64,7 @@ describe("transcribeInboundAudio", () => {
|
||||||
it("returns undefined when no transcription command", async () => {
|
it("returns undefined when no transcription command", async () => {
|
||||||
const { transcribeInboundAudio } = await import("./transcription.js");
|
const { transcribeInboundAudio } = await import("./transcription.js");
|
||||||
const res = await transcribeInboundAudio(
|
const res = await transcribeInboundAudio(
|
||||||
{ routing: {} } as never,
|
{ audio: {} } as never,
|
||||||
{} as never,
|
{} as never,
|
||||||
runtime as never,
|
runtime as never,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue