Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3368284b2a | ||
|
|
ece01d89fe | ||
|
|
9c0c4f50ec | ||
|
|
6729637f61 | ||
|
|
f50e06a1b6 | ||
|
|
2ae3b45ac1 | ||
|
|
780a43711f | ||
|
|
d682b604de |
No files matched your search
@@ -100,6 +100,7 @@
|
||||
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
|
||||
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/main/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
||||
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
|
||||
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
|
||||
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
|
||||
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
|
||||
- Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot)
|
||||
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
|
||||
|
||||
### Fixes
|
||||
- Gateway: honor `CLAWDBOT_LAUNCHD_LABEL` / `CLAWDBOT_SYSTEMD_UNIT` overrides when checking or restarting the daemon.
|
||||
- Tools: harden tool input schemas for strict providers (no unions; flattened actions; enum helpers + tests).
|
||||
- Discord: allow clearing channel parents via message tool without nullable schema inputs.
|
||||
|
||||
## 2026.1.12-4
|
||||
|
||||
|
||||
@@ -569,7 +569,9 @@ Inbound messages are routed to an agent via bindings.
|
||||
- `name`: display name for the agent.
|
||||
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
|
||||
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
||||
- `model`: per-agent default model (provider/model), overrides `agents.defaults.model` for that agent.
|
||||
- `model`: per-agent default model, overrides `agents.defaults.model` for that agent.
|
||||
- string form: `"provider/model"`, overrides only `agents.defaults.model.primary`
|
||||
- object form: `{ primary, fallbacks }` (fallbacks override `agents.defaults.model.fallbacks`; `[]` disables global fallbacks for that agent)
|
||||
- `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`).
|
||||
@@ -583,6 +585,7 @@ Inbound messages are routed to an agent via bindings.
|
||||
- `subagents`: per-agent sub-agent defaults.
|
||||
- `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
|
||||
- `tools`: per-agent tool restrictions (applied before sandbox tool policy).
|
||||
- `profile`: base tool profile (applied before allow/deny)
|
||||
- `allow`: array of allowed tool names
|
||||
- `deny`: array of denied tool names (deny wins)
|
||||
- `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.).
|
||||
@@ -1503,6 +1506,34 @@ Legacy: `tools.bash` is still accepted as an alias.
|
||||
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
||||
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
||||
|
||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`:
|
||||
- `minimal`: `session_status` only
|
||||
- `coding`: `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image`
|
||||
- `messaging`: `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status`
|
||||
- `full`: no restriction (same as unset)
|
||||
|
||||
Per-agent override: `agents.list[].tools.profile`.
|
||||
|
||||
Example (messaging-only by default, allow Slack + Discord tools too):
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
allow: ["slack", "discord"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example (coding profile, but deny exec/process everywhere):
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "coding",
|
||||
deny: ["group:runtime"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||
This is applied even when the Docker sandbox is **off**.
|
||||
|
||||
@@ -1513,6 +1544,17 @@ Example (disable browser/canvas everywhere):
|
||||
}
|
||||
```
|
||||
|
||||
Tool groups (shorthands) work in **global** and **per-agent** tool policies:
|
||||
- `group:runtime`: `exec`, `bash`, `process`
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins)
|
||||
|
||||
`tools.elevated` controls elevated (host) exec access:
|
||||
- `enabled`: allow elevated mode (default true)
|
||||
- `allowFrom`: per-provider allowlists (empty = disabled)
|
||||
|
||||
@@ -50,6 +50,7 @@ See [Sandboxing](/gateway/sandboxing) for the full matrix (scope, workspace moun
|
||||
## Tool policy: which tools exist/are callable
|
||||
|
||||
Two layers matter:
|
||||
- **Tool profile**: `tools.profile` and `agents.list[].tools.profile` (base allowlist)
|
||||
- **Global/per-agent tool policy**: `tools.allow`/`tools.deny` and `agents.list[].tools.allow`/`agents.list[].tools.deny`
|
||||
- **Sandbox tool policy** (only applies when sandboxed): `tools.sandbox.tools.allow`/`tools.sandbox.tools.deny` and `agents.list[].tools.sandbox.tools.*`
|
||||
|
||||
@@ -59,7 +60,7 @@ Rules of thumb:
|
||||
|
||||
### Tool groups (shorthands)
|
||||
|
||||
For sandbox tool policy, you can use `group:*` entries that expand to multiple tools:
|
||||
Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple tools:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -78,6 +79,11 @@ Available groups:
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins)
|
||||
|
||||
## Elevated: exec-only “run on host”
|
||||
|
||||
|
||||
@@ -106,6 +106,28 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
|
||||
|
||||
---
|
||||
|
||||
### Example 2b: Global coding profile + messaging-only agent
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": { "profile": "coding" },
|
||||
"agents": {
|
||||
"list": [
|
||||
{
|
||||
"id": "support",
|
||||
"tools": { "profile": "messaging", "allow": ["slack"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- default agents get coding tools
|
||||
- `support` agent is messaging-only (+ Slack tool)
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Different Sandbox Modes per Agent
|
||||
|
||||
```json
|
||||
@@ -165,22 +187,29 @@ agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.*
|
||||
|
||||
### Tool Restrictions
|
||||
The filtering order is:
|
||||
1. **Global tool policy** (`tools.allow` / `tools.deny`)
|
||||
2. **Agent-specific tool policy** (`agents.list[].tools`)
|
||||
3. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`)
|
||||
4. **Subagent tool policy** (`tools.subagents.tools`, if applicable)
|
||||
1. **Tool profile** (`tools.profile` or `agents.list[].tools.profile`)
|
||||
2. **Global tool policy** (`tools.allow` / `tools.deny`)
|
||||
3. **Agent-specific tool policy** (`agents.list[].tools`)
|
||||
4. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`)
|
||||
5. **Subagent tool policy** (`tools.subagents.tools`, if applicable)
|
||||
|
||||
Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
|
||||
If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
|
||||
If `agents.list[].tools.profile` is set, it overrides `tools.profile` for that agent.
|
||||
|
||||
### Tool groups (shorthands)
|
||||
|
||||
Sandbox tool policy supports `group:*` entries that expand to multiple concrete tools:
|
||||
Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple concrete tools:
|
||||
|
||||
- `group:runtime`: `exec`, `bash`, `process`
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins)
|
||||
|
||||
### Elevated Mode
|
||||
`tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow).
|
||||
|
||||
@@ -22,6 +22,77 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
||||
}
|
||||
```
|
||||
|
||||
## Tool profiles (base allowlist)
|
||||
|
||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||
Per-agent override: `agents.list[].tools.profile`.
|
||||
|
||||
Profiles:
|
||||
- `minimal`: `session_status` only
|
||||
- `coding`: `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image`
|
||||
- `messaging`: `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status`
|
||||
- `full`: no restriction (same as unset)
|
||||
|
||||
Example (messaging-only by default, allow Slack + Discord tools too):
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
allow: ["slack", "discord"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example (coding profile, but deny exec/process everywhere):
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "coding",
|
||||
deny: ["group:runtime"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example (global coding profile, messaging-only support agent):
|
||||
```json5
|
||||
{
|
||||
tools: { profile: "coding" },
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "support",
|
||||
tools: { profile: "messaging", allow: ["slack"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tool groups (shorthands)
|
||||
|
||||
Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple tools.
|
||||
Use these in `tools.allow` / `tools.deny`.
|
||||
|
||||
Available groups:
|
||||
- `group:runtime`: `exec`, `bash`, `process`
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins)
|
||||
|
||||
Example (allow only file tools + browser):
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
allow: ["group:fs", "browser"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Plugins + tools
|
||||
|
||||
Plugins can register **additional tools** (and CLI commands) beyond the core set.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentModelPrimary,
|
||||
} from "./agent-scope.js";
|
||||
|
||||
describe("resolveAgentConfig", () => {
|
||||
it("should return undefined when no agents config exists", () => {
|
||||
@@ -47,6 +51,68 @@ describe("resolveAgentConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("supports per-agent model primary+fallbacks", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4",
|
||||
fallbacks: ["openai/gpt-4.1"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4",
|
||||
fallbacks: ["openai/gpt-5.2"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveAgentModelPrimary(cfg, "linus")).toBe(
|
||||
"anthropic/claude-opus-4",
|
||||
);
|
||||
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual([
|
||||
"openai/gpt-5.2",
|
||||
]);
|
||||
|
||||
// If fallbacks isn't present, we don't override the global fallbacks.
|
||||
const cfgNoOverride: ClawdbotConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Explicit empty list disables global fallbacks for that agent.
|
||||
const cfgDisable: ClawdbotConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentModelFallbacksOverride(cfgDisable, "linus")).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return agent-specific sandbox config", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
|
||||
@@ -21,7 +21,7 @@ type ResolvedAgentConfig = {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
model?: AgentEntry["model"];
|
||||
memorySearch?: AgentEntry["memorySearch"];
|
||||
humanDelay?: AgentEntry["humanDelay"];
|
||||
identity?: AgentEntry["identity"];
|
||||
@@ -95,7 +95,11 @@ export function resolveAgentConfig(
|
||||
workspace:
|
||||
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
||||
model: typeof entry.model === "string" ? entry.model : undefined,
|
||||
model:
|
||||
typeof entry.model === "string" ||
|
||||
(entry.model && typeof entry.model === "object")
|
||||
? entry.model
|
||||
: undefined,
|
||||
memorySearch: entry.memorySearch,
|
||||
humanDelay: entry.humanDelay,
|
||||
identity: entry.identity,
|
||||
@@ -109,6 +113,28 @@ export function resolveAgentConfig(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentModelPrimary(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
): string | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||
if (!raw) return undefined;
|
||||
if (typeof raw === "string") return raw.trim() || undefined;
|
||||
const primary = raw.primary?.trim();
|
||||
return primary || undefined;
|
||||
}
|
||||
|
||||
export function resolveAgentModelFallbacksOverride(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
): string[] | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||
if (!raw || typeof raw === "string") return undefined;
|
||||
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
||||
if (!Object.hasOwn(raw, "fallbacks")) return undefined;
|
||||
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
||||
}
|
||||
|
||||
export function resolveAgentWorkspaceDir(cfg: ClawdbotConfig, agentId: string) {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||
|
||||
@@ -40,20 +40,6 @@ const DEFAULT_PATH =
|
||||
process.env.PATH ??
|
||||
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||
|
||||
// NOTE: Using Type.Unsafe with enum instead of Type.Union([Type.Literal(...)])
|
||||
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
|
||||
// Type.Union of literals compiles to { anyOf: [{enum:["a"]}, {enum:["b"]}, ...] }
|
||||
// which is valid but not accepted. A flat enum { type: "string", enum: [...] } works.
|
||||
const _stringEnum = <T extends readonly string[]>(
|
||||
values: T,
|
||||
options?: { description?: string },
|
||||
) =>
|
||||
Type.Unsafe<T[number]>({
|
||||
type: "string",
|
||||
enum: values as unknown as string[],
|
||||
...options,
|
||||
});
|
||||
|
||||
export type ExecToolDefaults = {
|
||||
backgroundMs?: number;
|
||||
timeoutSec?: number;
|
||||
|
||||
@@ -124,6 +124,106 @@ describe("runWithModelFallback", () => {
|
||||
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
||||
});
|
||||
|
||||
it("does not append configured primary when fallbacksOverride is set", async () => {
|
||||
const cfg = makeCfg({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const run = vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.reject(Object.assign(new Error("nope"), { status: 401 })),
|
||||
);
|
||||
|
||||
await expect(
|
||||
runWithModelFallback({
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
fallbacksOverride: ["anthropic/claude-haiku-3-5"],
|
||||
run,
|
||||
}),
|
||||
).rejects.toThrow("All models failed");
|
||||
|
||||
expect(run.mock.calls).toEqual([
|
||||
["anthropic", "claude-opus-4-5"],
|
||||
["anthropic", "claude-haiku-3-5"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses fallbacksOverride instead of agents.defaults.model.fallbacks", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
fallbacks: ["openai/gpt-5.2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const calls: Array<{ provider: string; model: string }> = [];
|
||||
|
||||
const res = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
fallbacksOverride: ["openai/gpt-4.1"],
|
||||
run: async (provider, model) => {
|
||||
calls.push({ provider, model });
|
||||
if (provider === "anthropic") {
|
||||
throw Object.assign(new Error("nope"), { status: 401 });
|
||||
}
|
||||
if (provider === "openai" && model === "gpt-4.1") {
|
||||
return "ok";
|
||||
}
|
||||
throw new Error(`unexpected candidate: ${provider}/${model}`);
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.result).toBe("ok");
|
||||
expect(calls).toEqual([
|
||||
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||
{ provider: "openai", model: "gpt-4.1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats an empty fallbacksOverride as disabling global fallbacks", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
fallbacks: ["openai/gpt-5.2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const calls: Array<{ provider: string; model: string }> = [];
|
||||
|
||||
await expect(
|
||||
runWithModelFallback({
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
fallbacksOverride: [],
|
||||
run: async (provider, model) => {
|
||||
calls.push({ provider, model });
|
||||
throw new Error("primary failed");
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("primary failed");
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back on missing API key errors", async () => {
|
||||
const cfg = makeCfg();
|
||||
const run = vi
|
||||
|
||||
@@ -126,6 +126,8 @@ function resolveFallbackCandidates(params: {
|
||||
cfg: ClawdbotConfig | undefined;
|
||||
provider: string;
|
||||
model: string;
|
||||
/** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */
|
||||
fallbacksOverride?: string[];
|
||||
}): ModelCandidate[] {
|
||||
const provider = params.provider.trim() || DEFAULT_PROVIDER;
|
||||
const model = params.model.trim() || DEFAULT_MODEL;
|
||||
@@ -159,6 +161,7 @@ function resolveFallbackCandidates(params: {
|
||||
addCandidate({ provider, model }, false);
|
||||
|
||||
const modelFallbacks = (() => {
|
||||
if (params.fallbacksOverride !== undefined) return params.fallbacksOverride;
|
||||
const model = params.cfg?.agents?.defaults?.model as
|
||||
| { fallbacks?: string[] }
|
||||
| string
|
||||
@@ -177,7 +180,11 @@ function resolveFallbackCandidates(params: {
|
||||
addCandidate(resolved.ref, true);
|
||||
}
|
||||
|
||||
if (primary?.provider && primary.model) {
|
||||
if (
|
||||
params.fallbacksOverride === undefined &&
|
||||
primary?.provider &&
|
||||
primary.model
|
||||
) {
|
||||
addCandidate({ provider: primary.provider, model: primary.model }, false);
|
||||
}
|
||||
|
||||
@@ -188,6 +195,8 @@ export async function runWithModelFallback<T>(params: {
|
||||
cfg: ClawdbotConfig | undefined;
|
||||
provider: string;
|
||||
model: string;
|
||||
/** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */
|
||||
fallbacksOverride?: string[];
|
||||
run: (provider: string, model: string) => Promise<T>;
|
||||
onError?: (attempt: {
|
||||
provider: string;
|
||||
@@ -202,7 +211,12 @@ export async function runWithModelFallback<T>(params: {
|
||||
model: string;
|
||||
attempts: FallbackAttempt[];
|
||||
}> {
|
||||
const candidates = resolveFallbackCandidates(params);
|
||||
const candidates = resolveFallbackCandidates({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
fallbacksOverride: params.fallbacksOverride,
|
||||
});
|
||||
const attempts: FallbackAttempt[] = [];
|
||||
let lastError: unknown;
|
||||
|
||||
|
||||
+142
-1
@@ -5,6 +5,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import sharp from "sharp";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||
import { __testing, createClawdbotCodingTools } from "./pi-tools.js";
|
||||
import { createBrowserTool } from "./tools/browser-tool.js";
|
||||
|
||||
@@ -16,7 +17,7 @@ describe("createClawdbotCodingTools", () => {
|
||||
expect(schema.anyOf).toBeUndefined();
|
||||
});
|
||||
|
||||
it("merges properties for union tool schemas", () => {
|
||||
it("keeps browser tool schema properties after normalization", () => {
|
||||
const tools = createClawdbotCodingTools();
|
||||
const browser = tools.find((tool) => tool.name === "browser");
|
||||
expect(browser).toBeDefined();
|
||||
@@ -266,6 +267,95 @@ describe("createClawdbotCodingTools", () => {
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("avoids anyOf/oneOf/allOf in tool schemas", () => {
|
||||
const tools = createClawdbotCodingTools();
|
||||
const offenders: Array<{
|
||||
name: string;
|
||||
keyword: string;
|
||||
path: string;
|
||||
}> = [];
|
||||
const keywords = new Set(["anyOf", "oneOf", "allOf"]);
|
||||
|
||||
const walk = (value: unknown, path: string, name: string): void => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value)) {
|
||||
for (const [index, entry] of value.entries()) {
|
||||
walk(entry, `${path}[${index}]`, name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object") return;
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const [key, entry] of Object.entries(record)) {
|
||||
const nextPath = path ? `${path}.${key}` : key;
|
||||
if (keywords.has(key)) {
|
||||
offenders.push({ name, keyword: key, path: nextPath });
|
||||
}
|
||||
walk(entry, nextPath, name);
|
||||
}
|
||||
};
|
||||
|
||||
for (const tool of tools) {
|
||||
walk(tool.parameters, "", tool.name);
|
||||
}
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps raw core tool schemas union-free", () => {
|
||||
const tools = createClawdbotTools();
|
||||
const coreTools = new Set([
|
||||
"browser",
|
||||
"canvas",
|
||||
"nodes",
|
||||
"cron",
|
||||
"message",
|
||||
"gateway",
|
||||
"agents_list",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
"memory_search",
|
||||
"memory_get",
|
||||
"image",
|
||||
]);
|
||||
const offenders: Array<{
|
||||
name: string;
|
||||
keyword: string;
|
||||
path: string;
|
||||
}> = [];
|
||||
const keywords = new Set(["anyOf", "oneOf", "allOf"]);
|
||||
|
||||
const walk = (value: unknown, path: string, name: string): void => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value)) {
|
||||
for (const [index, entry] of value.entries()) {
|
||||
walk(entry, `${path}[${index}]`, name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object") return;
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const [key, entry] of Object.entries(record)) {
|
||||
const nextPath = path ? `${path}.${key}` : key;
|
||||
if (keywords.has(key)) {
|
||||
offenders.push({ name, keyword: key, path: nextPath });
|
||||
}
|
||||
walk(entry, nextPath, name);
|
||||
}
|
||||
};
|
||||
|
||||
for (const tool of tools) {
|
||||
if (!coreTools.has(tool.name)) continue;
|
||||
walk(tool.parameters, "", tool.name);
|
||||
}
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not expose provider-specific message tools", () => {
|
||||
const tools = createClawdbotCodingTools({ messageProvider: "discord" });
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
@@ -517,6 +607,57 @@ describe("createClawdbotCodingTools", () => {
|
||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||
});
|
||||
|
||||
it("applies tool profiles before allow/deny policies", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: { tools: { profile: "messaging" } },
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
expect(names.has("message")).toBe(true);
|
||||
expect(names.has("sessions_send")).toBe(true);
|
||||
expect(names.has("sessions_spawn")).toBe(false);
|
||||
expect(names.has("exec")).toBe(false);
|
||||
expect(names.has("browser")).toBe(false);
|
||||
});
|
||||
|
||||
it("expands group shorthands in global tool policy", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: { tools: { allow: ["group:fs"] } },
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
expect(names.has("read")).toBe(true);
|
||||
expect(names.has("write")).toBe(true);
|
||||
expect(names.has("edit")).toBe(true);
|
||||
expect(names.has("exec")).toBe(false);
|
||||
expect(names.has("browser")).toBe(false);
|
||||
});
|
||||
|
||||
it("expands group shorthands in global tool deny policy", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
config: { tools: { deny: ["group:fs"] } },
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
expect(names.has("read")).toBe(false);
|
||||
expect(names.has("write")).toBe(false);
|
||||
expect(names.has("edit")).toBe(false);
|
||||
expect(names.has("exec")).toBe(true);
|
||||
});
|
||||
|
||||
it("lets agent profiles override global profiles", () => {
|
||||
const tools = createClawdbotCodingTools({
|
||||
sessionKey: "agent:work:main",
|
||||
config: {
|
||||
tools: { profile: "coding" },
|
||||
agents: {
|
||||
list: [{ id: "work", tools: { profile: "messaging" } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
expect(names.has("message")).toBe(true);
|
||||
expect(names.has("exec")).toBe(false);
|
||||
expect(names.has("read")).toBe(false);
|
||||
});
|
||||
|
||||
it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => {
|
||||
const tools = createClawdbotCodingTools();
|
||||
|
||||
|
||||
+26
-23
@@ -28,6 +28,11 @@ import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
|
||||
import { assertSandboxPath } from "./sandbox-paths.js";
|
||||
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
|
||||
import { sanitizeToolResultImages } from "./tool-images.js";
|
||||
import {
|
||||
expandToolGroups,
|
||||
normalizeToolName,
|
||||
resolveToolProfilePolicy,
|
||||
} from "./tool-policy.js";
|
||||
|
||||
// NOTE(steipete): Upstream read now does file-magic MIME detection; we keep the wrapper
|
||||
// to normalize payloads and sanitize oversized images before they hit providers.
|
||||
@@ -291,21 +296,6 @@ function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
return cleanSchemaForGemini(schema);
|
||||
}
|
||||
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
bash: "exec",
|
||||
"apply-patch": "apply_patch",
|
||||
};
|
||||
|
||||
function normalizeToolName(name: string) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
function normalizeToolNames(list?: string[]) {
|
||||
if (!list) return [];
|
||||
return list.map(normalizeToolName).filter(Boolean);
|
||||
}
|
||||
|
||||
function isOpenAIProvider(provider?: string) {
|
||||
const normalized = provider?.trim().toLowerCase();
|
||||
return normalized === "openai" || normalized === "openai-codex";
|
||||
@@ -357,8 +347,8 @@ function isToolAllowedByPolicyName(
|
||||
policy?: SandboxToolPolicy,
|
||||
): boolean {
|
||||
if (!policy) return true;
|
||||
const deny = new Set(normalizeToolNames(policy.deny));
|
||||
const allowRaw = normalizeToolNames(policy.allow);
|
||||
const deny = new Set(expandToolGroups(policy.deny));
|
||||
const allowRaw = expandToolGroups(policy.allow);
|
||||
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
|
||||
const normalized = normalizeToolName(name);
|
||||
if (deny.has(normalized)) return false;
|
||||
@@ -391,11 +381,15 @@ function resolveEffectiveToolPolicy(params: {
|
||||
: undefined;
|
||||
const agentTools = agentConfig?.tools;
|
||||
const hasAgentToolPolicy =
|
||||
Array.isArray(agentTools?.allow) || Array.isArray(agentTools?.deny);
|
||||
Array.isArray(agentTools?.allow) ||
|
||||
Array.isArray(agentTools?.deny) ||
|
||||
typeof agentTools?.profile === "string";
|
||||
const globalTools = params.config?.tools;
|
||||
const profile = agentTools?.profile ?? globalTools?.profile;
|
||||
return {
|
||||
agentId,
|
||||
policy: hasAgentToolPolicy ? agentTools : globalTools,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -703,10 +697,15 @@ export function createClawdbotCodingTools(options?: {
|
||||
}): AnyAgentTool[] {
|
||||
const execToolName = "exec";
|
||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||
const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({
|
||||
const {
|
||||
agentId,
|
||||
policy: effectiveToolsPolicy,
|
||||
profile,
|
||||
} = resolveEffectiveToolPolicy({
|
||||
config: options?.config,
|
||||
sessionKey: options?.sessionKey,
|
||||
});
|
||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||
const scopeKey =
|
||||
options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
const subagentPolicy =
|
||||
@@ -714,6 +713,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
? resolveSubagentToolPolicy(options.config)
|
||||
: undefined;
|
||||
const allowBackground = isToolAllowedByPolicies("process", [
|
||||
profilePolicy,
|
||||
effectiveToolsPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
@@ -829,12 +829,15 @@ export function createClawdbotCodingTools(options?: {
|
||||
hasRepliedRef: options?.hasRepliedRef,
|
||||
}),
|
||||
];
|
||||
const toolsFiltered = effectiveToolsPolicy
|
||||
? filterToolsByPolicy(tools, effectiveToolsPolicy)
|
||||
const toolsFiltered = profilePolicy
|
||||
? filterToolsByPolicy(tools, profilePolicy)
|
||||
: tools;
|
||||
const sandboxed = sandbox
|
||||
? filterToolsByPolicy(toolsFiltered, sandbox.tools)
|
||||
const policyFiltered = effectiveToolsPolicy
|
||||
? filterToolsByPolicy(toolsFiltered, effectiveToolsPolicy)
|
||||
: toolsFiltered;
|
||||
const sandboxed = sandbox
|
||||
? filterToolsByPolicy(policyFiltered, sandbox.tools)
|
||||
: policyFiltered;
|
||||
const subagentFiltered = subagentPolicy
|
||||
? filterToolsByPolicy(sandboxed, subagentPolicy)
|
||||
: sandboxed;
|
||||
|
||||
+5
-52
@@ -33,6 +33,7 @@ import {
|
||||
resolveSessionAgentId,
|
||||
} from "./agent-scope.js";
|
||||
import { syncSkillsToWorkspace } from "./skills.js";
|
||||
import { expandToolGroups } from "./tool-policy.js";
|
||||
import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
@@ -239,58 +240,10 @@ const BROWSER_BRIDGES = new Map<
|
||||
{ bridge: BrowserBridge; containerName: string }
|
||||
>();
|
||||
|
||||
function normalizeToolList(values?: string[]) {
|
||||
if (!values) return [];
|
||||
return values
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.map((value) => value.toLowerCase());
|
||||
}
|
||||
|
||||
const TOOL_GROUPS: Record<string, string[]> = {
|
||||
// NOTE: Keep canonical (lowercase) tool names here.
|
||||
"group:memory": ["memory_search", "memory_get"],
|
||||
// Basic workspace/file tools
|
||||
"group:fs": ["read", "write", "edit", "apply_patch"],
|
||||
// Session management tools
|
||||
"group:sessions": [
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
],
|
||||
// Host/runtime execution tools
|
||||
"group:runtime": ["exec", "bash", "process"],
|
||||
};
|
||||
|
||||
function expandToolGroupEntry(entry: string): string[] {
|
||||
const raw = entry.trim();
|
||||
if (!raw) return [];
|
||||
const lower = raw.toLowerCase();
|
||||
|
||||
const group = TOOL_GROUPS[lower];
|
||||
if (group) return group;
|
||||
return [raw];
|
||||
}
|
||||
|
||||
function expandToolGroups(values?: string[]): string[] {
|
||||
if (!values) return [];
|
||||
const out: string[] = [];
|
||||
for (const value of values) {
|
||||
for (const expanded of expandToolGroupEntry(value)) {
|
||||
const trimmed = expanded.trim();
|
||||
if (!trimmed) continue;
|
||||
out.push(trimmed);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isToolAllowed(policy: SandboxToolPolicy, name: string) {
|
||||
const deny = new Set(normalizeToolList(expandToolGroups(policy.deny)));
|
||||
const deny = new Set(expandToolGroups(policy.deny));
|
||||
if (deny.has(name.toLowerCase())) return false;
|
||||
const allow = normalizeToolList(expandToolGroups(policy.allow));
|
||||
const allow = expandToolGroups(policy.allow);
|
||||
if (allow.length === 0) return true;
|
||||
return allow.includes(name.toLowerCase());
|
||||
}
|
||||
@@ -687,8 +640,8 @@ export function formatSandboxToolPolicyBlockedMessage(params: {
|
||||
});
|
||||
if (!runtime.sandboxed) return undefined;
|
||||
|
||||
const deny = new Set(normalizeToolList(runtime.toolPolicy.deny));
|
||||
const allow = normalizeToolList(runtime.toolPolicy.allow);
|
||||
const deny = new Set(expandToolGroups(runtime.toolPolicy.deny));
|
||||
const allow = expandToolGroups(runtime.toolPolicy.allow);
|
||||
const allowSet = allow.length > 0 ? new Set(allow) : null;
|
||||
const blockedByDeny = deny.has(tool);
|
||||
const blockedByAllow = allowSet ? !allowSet.has(tool) : false;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
type StringEnumOptions<T extends readonly string[]> = {
|
||||
description?: string;
|
||||
title?: string;
|
||||
default?: T[number];
|
||||
};
|
||||
|
||||
// NOTE: Avoid Type.Union([Type.Literal(...)]) which compiles to anyOf.
|
||||
// Some providers reject anyOf in tool schemas; a flat string enum is safer.
|
||||
export function stringEnum<T extends readonly string[]>(
|
||||
values: T,
|
||||
options: StringEnumOptions<T> = {},
|
||||
) {
|
||||
return Type.Unsafe<T[number]>({
|
||||
type: "string",
|
||||
enum: [...values],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function optionalStringEnum<T extends readonly string[]>(
|
||||
values: T,
|
||||
options: StringEnumOptions<T> = {},
|
||||
) {
|
||||
return Type.Optional(stringEnum(values, options));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
expandToolGroups,
|
||||
resolveToolProfilePolicy,
|
||||
TOOL_GROUPS,
|
||||
} from "./tool-policy.js";
|
||||
|
||||
describe("tool-policy", () => {
|
||||
it("expands groups and normalizes aliases", () => {
|
||||
const expanded = expandToolGroups([
|
||||
"group:runtime",
|
||||
"BASH",
|
||||
"apply-patch",
|
||||
"group:fs",
|
||||
]);
|
||||
const set = new Set(expanded);
|
||||
expect(set.has("exec")).toBe(true);
|
||||
expect(set.has("bash")).toBe(true);
|
||||
expect(set.has("process")).toBe(true);
|
||||
expect(set.has("apply_patch")).toBe(true);
|
||||
expect(set.has("read")).toBe(true);
|
||||
expect(set.has("write")).toBe(true);
|
||||
expect(set.has("edit")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves known profiles and ignores unknown ones", () => {
|
||||
const coding = resolveToolProfilePolicy("coding");
|
||||
expect(coding?.allow).toContain("group:fs");
|
||||
expect(resolveToolProfilePolicy("nope")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes core tool groups in group:clawdbot", () => {
|
||||
const group = TOOL_GROUPS["group:clawdbot"];
|
||||
expect(group).toContain("browser");
|
||||
expect(group).toContain("message");
|
||||
expect(group).toContain("session_status");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
||||
|
||||
type ToolProfilePolicy = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
bash: "exec",
|
||||
"apply-patch": "apply_patch",
|
||||
};
|
||||
|
||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
// NOTE: Keep canonical (lowercase) tool names here.
|
||||
"group:memory": ["memory_search", "memory_get"],
|
||||
// Basic workspace/file tools
|
||||
"group:fs": ["read", "write", "edit", "apply_patch"],
|
||||
// Host/runtime execution tools
|
||||
"group:runtime": ["exec", "bash", "process"],
|
||||
// Session management tools
|
||||
"group:sessions": [
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
],
|
||||
// UI helpers
|
||||
"group:ui": ["browser", "canvas"],
|
||||
// Automation + infra
|
||||
"group:automation": ["cron", "gateway"],
|
||||
// Messaging surface
|
||||
"group:messaging": ["message"],
|
||||
// Nodes + device tools
|
||||
"group:nodes": ["nodes"],
|
||||
// All Clawdbot native tools (excludes provider plugins).
|
||||
"group:clawdbot": [
|
||||
"browser",
|
||||
"canvas",
|
||||
"nodes",
|
||||
"cron",
|
||||
"message",
|
||||
"gateway",
|
||||
"agents_list",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"sessions_spawn",
|
||||
"session_status",
|
||||
"memory_search",
|
||||
"memory_get",
|
||||
"image",
|
||||
],
|
||||
};
|
||||
|
||||
const TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
|
||||
minimal: {
|
||||
allow: ["session_status"],
|
||||
},
|
||||
coding: {
|
||||
allow: [
|
||||
"group:fs",
|
||||
"group:runtime",
|
||||
"group:sessions",
|
||||
"group:memory",
|
||||
"image",
|
||||
],
|
||||
},
|
||||
messaging: {
|
||||
allow: [
|
||||
"group:messaging",
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
"sessions_send",
|
||||
"session_status",
|
||||
],
|
||||
},
|
||||
full: {},
|
||||
};
|
||||
|
||||
export function normalizeToolName(name: string) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
export function normalizeToolList(list?: string[]) {
|
||||
if (!list) return [];
|
||||
return list.map(normalizeToolName).filter(Boolean);
|
||||
}
|
||||
|
||||
export function expandToolGroups(list?: string[]) {
|
||||
const normalized = normalizeToolList(list);
|
||||
const expanded: string[] = [];
|
||||
for (const value of normalized) {
|
||||
const group = TOOL_GROUPS[value];
|
||||
if (group) {
|
||||
expanded.push(...group);
|
||||
continue;
|
||||
}
|
||||
expanded.push(value);
|
||||
}
|
||||
return Array.from(new Set(expanded));
|
||||
}
|
||||
|
||||
export function resolveToolProfilePolicy(
|
||||
profile?: string,
|
||||
): ToolProfilePolicy | undefined {
|
||||
if (!profile) return undefined;
|
||||
const resolved = TOOL_PROFILES[profile as ToolProfileId];
|
||||
if (!resolved) return undefined;
|
||||
if (!resolved.allow && !resolved.deny) return undefined;
|
||||
return {
|
||||
allow: resolved.allow ? [...resolved.allow] : undefined,
|
||||
deny: resolved.deny ? [...resolved.deny] : undefined,
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { resolveBrowserConfig } from "../../browser/config.js";
|
||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import {
|
||||
type AnyAgentTool,
|
||||
imageResultFromFile,
|
||||
@@ -43,16 +44,35 @@ const BROWSER_ACT_KINDS = [
|
||||
"close",
|
||||
] as const;
|
||||
|
||||
type BrowserActKind = (typeof BROWSER_ACT_KINDS)[number];
|
||||
const BROWSER_TOOL_ACTIONS = [
|
||||
"status",
|
||||
"start",
|
||||
"stop",
|
||||
"tabs",
|
||||
"open",
|
||||
"focus",
|
||||
"close",
|
||||
"snapshot",
|
||||
"screenshot",
|
||||
"navigate",
|
||||
"console",
|
||||
"pdf",
|
||||
"upload",
|
||||
"dialog",
|
||||
"act",
|
||||
] as const;
|
||||
|
||||
const BROWSER_TARGETS = ["sandbox", "host", "custom"] as const;
|
||||
|
||||
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
|
||||
|
||||
const BROWSER_IMAGE_TYPES = ["png", "jpeg"] as const;
|
||||
|
||||
// NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...])
|
||||
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
|
||||
// The discriminator (kind) determines which properties are relevant; runtime validates.
|
||||
const BrowserActSchema = Type.Object({
|
||||
kind: Type.Unsafe<BrowserActKind>({
|
||||
type: "string",
|
||||
enum: [...BROWSER_ACT_KINDS],
|
||||
}),
|
||||
kind: stringEnum(BROWSER_ACT_KINDS),
|
||||
// Common fields
|
||||
targetId: Type.Optional(Type.String()),
|
||||
ref: Type.Optional(Type.String()),
|
||||
@@ -89,37 +109,15 @@ const BrowserActSchema = Type.Object({
|
||||
// A root-level `Type.Union([...])` compiles to `{ anyOf: [...] }` (no `type`),
|
||||
// which OpenAI rejects ("Invalid schema ... type: None"). Keep this schema an object.
|
||||
const BrowserToolSchema = Type.Object({
|
||||
action: Type.Union([
|
||||
Type.Literal("status"),
|
||||
Type.Literal("start"),
|
||||
Type.Literal("stop"),
|
||||
Type.Literal("tabs"),
|
||||
Type.Literal("open"),
|
||||
Type.Literal("focus"),
|
||||
Type.Literal("close"),
|
||||
Type.Literal("snapshot"),
|
||||
Type.Literal("screenshot"),
|
||||
Type.Literal("navigate"),
|
||||
Type.Literal("console"),
|
||||
Type.Literal("pdf"),
|
||||
Type.Literal("upload"),
|
||||
Type.Literal("dialog"),
|
||||
Type.Literal("act"),
|
||||
]),
|
||||
target: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("sandbox"),
|
||||
Type.Literal("host"),
|
||||
Type.Literal("custom"),
|
||||
]),
|
||||
),
|
||||
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
||||
target: optionalStringEnum(BROWSER_TARGETS),
|
||||
profile: Type.Optional(Type.String()),
|
||||
controlUrl: Type.Optional(Type.String()),
|
||||
targetUrl: Type.Optional(Type.String()),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
limit: Type.Optional(Type.Number()),
|
||||
maxChars: Type.Optional(Type.Number()),
|
||||
format: Type.Optional(Type.Union([Type.Literal("aria"), Type.Literal("ai")])),
|
||||
format: optionalStringEnum(BROWSER_SNAPSHOT_FORMATS),
|
||||
interactive: Type.Optional(Type.Boolean()),
|
||||
compact: Type.Optional(Type.Boolean()),
|
||||
depth: Type.Optional(Type.Number()),
|
||||
@@ -128,7 +126,7 @@ const BrowserToolSchema = Type.Object({
|
||||
fullPage: Type.Optional(Type.Boolean()),
|
||||
ref: Type.Optional(Type.String()),
|
||||
element: Type.Optional(Type.String()),
|
||||
type: Type.Optional(Type.Union([Type.Literal("png"), Type.Literal("jpeg")])),
|
||||
type: optionalStringEnum(BROWSER_IMAGE_TYPES),
|
||||
level: Type.Optional(Type.String()),
|
||||
paths: Type.Optional(Type.Array(Type.String())),
|
||||
inputRef: Type.Optional(Type.String()),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
parseCanvasSnapshotPayload,
|
||||
} from "../../cli/nodes-canvas.js";
|
||||
import { imageMimeFromFormat } from "../../media/mime.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import {
|
||||
type AnyAgentTool,
|
||||
imageResult,
|
||||
@@ -17,76 +18,44 @@ import {
|
||||
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
import { resolveNodeId } from "./nodes-utils.js";
|
||||
|
||||
const CanvasToolSchema = Type.Union([
|
||||
Type.Object({
|
||||
action: Type.Literal("present"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
target: Type.Optional(Type.String()),
|
||||
x: Type.Optional(Type.Number()),
|
||||
y: Type.Optional(Type.Number()),
|
||||
width: Type.Optional(Type.Number()),
|
||||
height: Type.Optional(Type.Number()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("hide"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("navigate"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
url: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("eval"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
javaScript: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("snapshot"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
format: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("png"),
|
||||
Type.Literal("jpg"),
|
||||
Type.Literal("jpeg"),
|
||||
]),
|
||||
),
|
||||
maxWidth: Type.Optional(Type.Number()),
|
||||
quality: Type.Optional(Type.Number()),
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("a2ui_push"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
jsonl: Type.Optional(Type.String()),
|
||||
jsonlPath: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("a2ui_reset"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
}),
|
||||
]);
|
||||
const CANVAS_ACTIONS = [
|
||||
"present",
|
||||
"hide",
|
||||
"navigate",
|
||||
"eval",
|
||||
"snapshot",
|
||||
"a2ui_push",
|
||||
"a2ui_reset",
|
||||
] as const;
|
||||
|
||||
const CANVAS_SNAPSHOT_FORMATS = ["png", "jpg", "jpeg"] as const;
|
||||
|
||||
// Flattened schema: runtime validates per-action requirements.
|
||||
const CanvasToolSchema = Type.Object({
|
||||
action: stringEnum(CANVAS_ACTIONS),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
// present
|
||||
target: Type.Optional(Type.String()),
|
||||
x: Type.Optional(Type.Number()),
|
||||
y: Type.Optional(Type.Number()),
|
||||
width: Type.Optional(Type.Number()),
|
||||
height: Type.Optional(Type.Number()),
|
||||
// navigate
|
||||
url: Type.Optional(Type.String()),
|
||||
// eval
|
||||
javaScript: Type.Optional(Type.String()),
|
||||
// snapshot
|
||||
format: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS),
|
||||
maxWidth: Type.Optional(Type.Number()),
|
||||
quality: Type.Optional(Type.Number()),
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
// a2ui_push
|
||||
jsonl: Type.Optional(Type.String()),
|
||||
jsonlPath: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export function createCanvasTool(): AnyAgentTool {
|
||||
return {
|
||||
|
||||
@@ -3,89 +3,42 @@ import {
|
||||
normalizeCronJobCreate,
|
||||
normalizeCronJobPatch,
|
||||
} from "../../cron/normalize.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
|
||||
// NOTE: We use Type.Object({}, { additionalProperties: true }) for job/patch
|
||||
// instead of CronAddParamsSchema/CronJobPatchSchema because:
|
||||
//
|
||||
// 1. CronAddParamsSchema contains nested Type.Union (for schedule, payload, etc.)
|
||||
// 2. TypeBox compiles Type.Union to JSON Schema `anyOf`
|
||||
// 3. pi-ai's sanitizeSchemaForGoogle() strips `anyOf` from nested properties
|
||||
// 4. This leaves empty schemas `{}` which Claude rejects as invalid
|
||||
//
|
||||
// The actual validation happens at runtime via normalizeCronJobCreate/Patch
|
||||
// and the gateway's validateCronAddParams. This schema just needs to accept
|
||||
// any object so the AI can pass through the job definition.
|
||||
//
|
||||
// See: https://github.com/anthropics/anthropic-cookbook/blob/main/misc/tool_use_best_practices.md
|
||||
// Claude requires valid JSON Schema 2020-12 with explicit types.
|
||||
// instead of CronAddParamsSchema/CronJobPatchSchema because the gateway schemas
|
||||
// contain nested unions. Tool schemas need to stay provider-friendly, so we
|
||||
// accept "any object" here and validate at runtime.
|
||||
|
||||
const CronToolSchema = Type.Union([
|
||||
Type.Object({
|
||||
action: Type.Literal("status"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("list"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
includeDisabled: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("add"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
job: Type.Object({}, { additionalProperties: true }),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("update"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
jobId: Type.Optional(Type.String()),
|
||||
id: Type.Optional(Type.String()),
|
||||
patch: Type.Object({}, { additionalProperties: true }),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("remove"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
jobId: Type.Optional(Type.String()),
|
||||
id: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("run"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
jobId: Type.Optional(Type.String()),
|
||||
id: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("runs"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
jobId: Type.Optional(Type.String()),
|
||||
id: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("wake"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
text: Type.String(),
|
||||
mode: Type.Optional(
|
||||
Type.Union([Type.Literal("now"), Type.Literal("next-heartbeat")]),
|
||||
),
|
||||
}),
|
||||
]);
|
||||
const CRON_ACTIONS = [
|
||||
"status",
|
||||
"list",
|
||||
"add",
|
||||
"update",
|
||||
"remove",
|
||||
"run",
|
||||
"runs",
|
||||
"wake",
|
||||
] as const;
|
||||
|
||||
const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const;
|
||||
|
||||
// Flattened schema: runtime validates per-action requirements.
|
||||
const CronToolSchema = Type.Object({
|
||||
action: stringEnum(CRON_ACTIONS),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
includeDisabled: Type.Optional(Type.Boolean()),
|
||||
job: Type.Optional(Type.Object({}, { additionalProperties: true })),
|
||||
jobId: Type.Optional(Type.String()),
|
||||
id: Type.Optional(Type.String()),
|
||||
patch: Type.Optional(Type.Object({}, { additionalProperties: true })),
|
||||
text: Type.Optional(Type.String()),
|
||||
mode: optionalStringEnum(CRON_WAKE_MODES),
|
||||
});
|
||||
|
||||
export function createCronTool(): AnyAgentTool {
|
||||
return {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
function readParentIdParam(
|
||||
params: Record<string, unknown>,
|
||||
): string | null | undefined {
|
||||
if (params.clearParent === true) return null;
|
||||
if (params.parentId === null) return null;
|
||||
return readStringParam(params, "parentId");
|
||||
}
|
||||
|
||||
@@ -218,6 +218,26 @@ describe("handleDiscordGuildAction - channel management", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears the channel parent when clearParent is true", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"channelEdit",
|
||||
{
|
||||
channelId: "C1",
|
||||
clearParent: true,
|
||||
},
|
||||
channelsEnabled,
|
||||
);
|
||||
expect(editChannelDiscord).toHaveBeenCalledWith({
|
||||
channelId: "C1",
|
||||
name: undefined,
|
||||
topic: undefined,
|
||||
position: undefined,
|
||||
parentId: null,
|
||||
nsfw: undefined,
|
||||
rateLimitPerUser: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes a channel", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"channelDelete",
|
||||
@@ -264,6 +284,24 @@ describe("handleDiscordGuildAction - channel management", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears the channel parent on move when clearParent is true", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"channelMove",
|
||||
{
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
clearParent: true,
|
||||
},
|
||||
channelsEnabled,
|
||||
);
|
||||
expect(moveChannelDiscord).toHaveBeenCalledWith({
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
parentId: null,
|
||||
position: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a category with type=4", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"categoryCreate",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type RestartSentinelPayload,
|
||||
writeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool } from "./gateway.js";
|
||||
|
||||
@@ -18,16 +19,11 @@ const GATEWAY_ACTIONS = [
|
||||
"update.run",
|
||||
] as const;
|
||||
|
||||
type GatewayAction = (typeof GATEWAY_ACTIONS)[number];
|
||||
|
||||
// NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...])
|
||||
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
|
||||
// The discriminator (action) determines which properties are relevant; runtime validates.
|
||||
const GatewayToolSchema = Type.Object({
|
||||
action: Type.Unsafe<GatewayAction>({
|
||||
type: "string",
|
||||
enum: [...GATEWAY_ACTIONS],
|
||||
}),
|
||||
action: stringEnum(GATEWAY_ACTIONS),
|
||||
// restart
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ProviderMessageActionName,
|
||||
} from "../../providers/plugins/types.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
|
||||
|
||||
@@ -90,12 +91,17 @@ const MessageToolCommonSchema = {
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
name: Type.Optional(Type.String()),
|
||||
type: Type.Optional(Type.Number()),
|
||||
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
parentId: Type.Optional(Type.String()),
|
||||
topic: Type.Optional(Type.String()),
|
||||
position: Type.Optional(Type.Number()),
|
||||
nsfw: Type.Optional(Type.Boolean()),
|
||||
rateLimitPerUser: Type.Optional(Type.Number()),
|
||||
categoryId: Type.Optional(Type.String()),
|
||||
clearParent: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Clear the parent/category when supported by the provider.",
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
function buildMessageToolSchemaFromActions(
|
||||
@@ -105,31 +111,10 @@ function buildMessageToolSchemaFromActions(
|
||||
const props: Record<string, unknown> = { ...MessageToolCommonSchema };
|
||||
if (!options.includeButtons) delete props.buttons;
|
||||
|
||||
const schemas: Array<ReturnType<typeof Type.Object>> = [];
|
||||
if (actions.includes("send")) {
|
||||
schemas.push(
|
||||
Type.Object({
|
||||
action: Type.Literal("send"),
|
||||
to: Type.String(),
|
||||
message: Type.String(),
|
||||
...props,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const nonSendActions = actions.filter((action) => action !== "send");
|
||||
if (nonSendActions.length > 0) {
|
||||
schemas.push(
|
||||
Type.Object({
|
||||
action: Type.Union(
|
||||
nonSendActions.map((action) => Type.Literal(action)),
|
||||
),
|
||||
...props,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return schemas.length === 1 ? schemas[0] : Type.Union(schemas);
|
||||
return Type.Object({
|
||||
action: stringEnum(actions),
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
|
||||
|
||||
+61
-139
@@ -18,151 +18,73 @@ import {
|
||||
} from "../../cli/nodes-screen.js";
|
||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||
import { imageMimeFromFormat } from "../../media/mime.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { sanitizeToolResultImages } from "../tool-images.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
import { resolveNodeId } from "./nodes-utils.js";
|
||||
|
||||
const NodesToolSchema = Type.Union([
|
||||
Type.Object({
|
||||
action: Type.Literal("status"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
const NODES_TOOL_ACTIONS = [
|
||||
"status",
|
||||
"describe",
|
||||
"pending",
|
||||
"approve",
|
||||
"reject",
|
||||
"notify",
|
||||
"camera_snap",
|
||||
"camera_list",
|
||||
"camera_clip",
|
||||
"screen_record",
|
||||
"location_get",
|
||||
"run",
|
||||
] as const;
|
||||
|
||||
const NOTIFY_PRIORITIES = ["passive", "active", "timeSensitive"] as const;
|
||||
const NOTIFY_DELIVERIES = ["system", "overlay", "auto"] as const;
|
||||
const CAMERA_FACING = ["front", "back", "both"] as const;
|
||||
const LOCATION_ACCURACY = ["coarse", "balanced", "precise"] as const;
|
||||
|
||||
// Flattened schema: runtime validates per-action requirements.
|
||||
const NodesToolSchema = Type.Object({
|
||||
action: stringEnum(NODES_TOOL_ACTIONS),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.Optional(Type.String()),
|
||||
requestId: Type.Optional(Type.String()),
|
||||
// notify
|
||||
title: Type.Optional(Type.String()),
|
||||
body: Type.Optional(Type.String()),
|
||||
sound: Type.Optional(Type.String()),
|
||||
priority: optionalStringEnum(NOTIFY_PRIORITIES),
|
||||
delivery: optionalStringEnum(NOTIFY_DELIVERIES),
|
||||
// camera_snap / camera_clip
|
||||
facing: optionalStringEnum(CAMERA_FACING, {
|
||||
description: "camera_snap: front/back/both; camera_clip: front/back only.",
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("describe"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("pending"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("approve"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
requestId: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("reject"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
requestId: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("notify"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
title: Type.Optional(Type.String()),
|
||||
body: Type.Optional(Type.String()),
|
||||
sound: Type.Optional(Type.String()),
|
||||
priority: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("passive"),
|
||||
Type.Literal("active"),
|
||||
Type.Literal("timeSensitive"),
|
||||
]),
|
||||
),
|
||||
delivery: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("system"),
|
||||
Type.Literal("overlay"),
|
||||
Type.Literal("auto"),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("camera_snap"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
facing: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("front"),
|
||||
Type.Literal("back"),
|
||||
Type.Literal("both"),
|
||||
]),
|
||||
),
|
||||
maxWidth: Type.Optional(Type.Number()),
|
||||
quality: Type.Optional(Type.Number()),
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
deviceId: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("camera_list"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("camera_clip"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
facing: Type.Optional(
|
||||
Type.Union([Type.Literal("front"), Type.Literal("back")]),
|
||||
),
|
||||
duration: Type.Optional(Type.String()),
|
||||
durationMs: Type.Optional(Type.Number()),
|
||||
includeAudio: Type.Optional(Type.Boolean()),
|
||||
deviceId: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("screen_record"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
duration: Type.Optional(Type.String()),
|
||||
durationMs: Type.Optional(Type.Number()),
|
||||
fps: Type.Optional(Type.Number()),
|
||||
screenIndex: Type.Optional(Type.Number()),
|
||||
includeAudio: Type.Optional(Type.Boolean()),
|
||||
outPath: Type.Optional(Type.String()),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("location_get"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
maxAgeMs: Type.Optional(Type.Number()),
|
||||
locationTimeoutMs: Type.Optional(Type.Number()),
|
||||
desiredAccuracy: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("coarse"),
|
||||
Type.Literal("balanced"),
|
||||
Type.Literal("precise"),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("run"),
|
||||
gatewayUrl: Type.Optional(Type.String()),
|
||||
gatewayToken: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
node: Type.String(),
|
||||
command: Type.Array(Type.String()),
|
||||
cwd: Type.Optional(Type.String()),
|
||||
env: Type.Optional(Type.Array(Type.String())),
|
||||
commandTimeoutMs: Type.Optional(Type.Number()),
|
||||
invokeTimeoutMs: Type.Optional(Type.Number()),
|
||||
needsScreenRecording: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
]);
|
||||
maxWidth: Type.Optional(Type.Number()),
|
||||
quality: Type.Optional(Type.Number()),
|
||||
delayMs: Type.Optional(Type.Number()),
|
||||
deviceId: Type.Optional(Type.String()),
|
||||
duration: Type.Optional(Type.String()),
|
||||
durationMs: Type.Optional(Type.Number()),
|
||||
includeAudio: Type.Optional(Type.Boolean()),
|
||||
// screen_record
|
||||
fps: Type.Optional(Type.Number()),
|
||||
screenIndex: Type.Optional(Type.Number()),
|
||||
outPath: Type.Optional(Type.String()),
|
||||
// location_get
|
||||
maxAgeMs: Type.Optional(Type.Number()),
|
||||
locationTimeoutMs: Type.Optional(Type.Number()),
|
||||
desiredAccuracy: optionalStringEnum(LOCATION_ACCURACY),
|
||||
// run
|
||||
command: Type.Optional(Type.Array(Type.String())),
|
||||
cwd: Type.Optional(Type.String()),
|
||||
env: Type.Optional(Type.Array(Type.String())),
|
||||
commandTimeoutMs: Type.Optional(Type.Number()),
|
||||
invokeTimeoutMs: Type.Optional(Type.Number()),
|
||||
needsScreenRecording: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
export function createNodesTool(): AnyAgentTool {
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
||||
import { registerSubagentRun } from "../subagent-registry.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
@@ -30,9 +31,7 @@ const SessionsSpawnToolSchema = Type.Object({
|
||||
runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
// Back-compat alias. Prefer runTimeoutSeconds.
|
||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
cleanup: Type.Optional(
|
||||
Type.Union([Type.Literal("delete"), Type.Literal("keep")]),
|
||||
),
|
||||
cleanup: optionalStringEnum(["delete", "keep"] as const),
|
||||
});
|
||||
|
||||
function normalizeModelSelection(value: unknown): string | undefined {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||
import { runCliAgent } from "../../agents/cli-runner.js";
|
||||
import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
@@ -394,6 +395,10 @@ export async function runReplyAgent(params: {
|
||||
cfg: followupRun.run.config,
|
||||
provider: followupRun.run.provider,
|
||||
model: followupRun.run.model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
followupRun.run.config,
|
||||
resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
|
||||
),
|
||||
run: (provider, model) =>
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: followupRun.run.sessionId,
|
||||
@@ -586,6 +591,10 @@ export async function runReplyAgent(params: {
|
||||
cfg: followupRun.run.config,
|
||||
provider: followupRun.run.provider,
|
||||
model: followupRun.run.model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
followupRun.run.config,
|
||||
resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
|
||||
),
|
||||
run: (provider, model) => {
|
||||
if (isCliProvider(provider, followupRun.run.config)) {
|
||||
const startedAt = Date.now();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentDir,
|
||||
resolveAgentModelPrimary,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
@@ -1593,7 +1593,7 @@ export function resolveDefaultModel(params: {
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const agentModelOverride = params.agentId
|
||||
? resolveAgentConfig(params.cfg, params.agentId)?.model?.trim()
|
||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
||||
: undefined;
|
||||
const cfg =
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import crypto from "node:crypto";
|
||||
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
type SessionEntry,
|
||||
updateSessionStoreEntry,
|
||||
} from "../../config/sessions.js";
|
||||
@@ -136,6 +138,10 @@ export function createFollowupRunner(params: {
|
||||
cfg: queued.run.config,
|
||||
provider: queued.run.provider,
|
||||
model: queued.run.model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
queued.run.config,
|
||||
resolveAgentIdFromSessionKey(queued.run.sessionKey),
|
||||
),
|
||||
run: (provider, model) =>
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: queued.run.sessionId,
|
||||
|
||||
+26
-1
@@ -1,6 +1,8 @@
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentModelPrimary,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
@@ -333,9 +335,28 @@ export async function agentCommand(
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
|
||||
const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId);
|
||||
const cfgForModelSelection = agentModelPrimary
|
||||
? {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
model: {
|
||||
...(typeof cfg.agents?.defaults?.model === "object"
|
||||
? cfg.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelPrimary,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: cfg;
|
||||
|
||||
const { provider: defaultProvider, model: defaultModel } =
|
||||
resolveConfiguredModelRef({
|
||||
cfg,
|
||||
cfg: cfgForModelSelection,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
@@ -442,6 +463,10 @@ export async function agentCommand(
|
||||
cfg,
|
||||
provider,
|
||||
model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
cfg,
|
||||
sessionAgentId,
|
||||
),
|
||||
run: (providerOverride, modelOverride) => {
|
||||
if (isCliProvider(providerOverride, cfg)) {
|
||||
const cliSessionId = getCliSessionId(sessionEntry, providerOverride);
|
||||
|
||||
@@ -142,7 +142,15 @@ function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) {
|
||||
const entry = listAgentEntries(cfg).find(
|
||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||
);
|
||||
if (entry?.model?.trim()) return entry.model.trim();
|
||||
if (entry?.model) {
|
||||
if (typeof entry.model === "string" && entry.model.trim()) {
|
||||
return entry.model.trim();
|
||||
}
|
||||
if (typeof entry.model === "object") {
|
||||
const primary = entry.model.primary?.trim();
|
||||
if (primary) return primary;
|
||||
}
|
||||
}
|
||||
const raw = cfg.agents?.defaults?.model;
|
||||
if (typeof raw === "string") return raw;
|
||||
return raw?.primary?.trim() || undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
||||
import { resolveAgentModelPrimary } from "../agents/agent-scope.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
@@ -152,7 +152,7 @@ export async function warnIfModelConfigLooksOff(
|
||||
options?: { agentId?: string; agentDir?: string },
|
||||
) {
|
||||
const agentModelOverride = options?.agentId
|
||||
? resolveAgentConfig(config, options.agentId)?.model?.trim()
|
||||
? resolveAgentModelPrimary(config, options.agentId)
|
||||
: undefined;
|
||||
const configWithModel =
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
|
||||
@@ -107,6 +107,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.audio.transcription.args": "Audio Transcription Args",
|
||||
"tools.audio.transcription.timeoutSeconds":
|
||||
"Audio Transcription Timeout (sec)",
|
||||
"tools.profile": "Tool Profile",
|
||||
"agents.list[].tools.profile": "Agent Tool Profile",
|
||||
"tools.exec.applyPatch.enabled": "Enable apply_patch",
|
||||
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
|
||||
+16
-1
@@ -988,7 +988,11 @@ export type QueueConfig = {
|
||||
drop?: QueueDropPolicy;
|
||||
};
|
||||
|
||||
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
||||
|
||||
export type AgentToolsConfig = {
|
||||
/** Base tool profile applied before allow/deny lists. */
|
||||
profile?: ToolProfileId;
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
/** Per-agent elevated exec gate (can only further restrict global tools.elevated). */
|
||||
@@ -1053,6 +1057,8 @@ export type MemorySearchConfig = {
|
||||
};
|
||||
|
||||
export type ToolsConfig = {
|
||||
/** Base tool profile applied before allow/deny lists. */
|
||||
profile?: ToolProfileId;
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
audio?: {
|
||||
@@ -1121,13 +1127,22 @@ export type ToolsConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentModelConfig =
|
||||
| string
|
||||
| {
|
||||
/** Primary model (provider/model). */
|
||||
primary?: string;
|
||||
/** Per-agent model fallbacks (provider/model). */
|
||||
fallbacks?: string[];
|
||||
};
|
||||
|
||||
export type AgentConfig = {
|
||||
id: string;
|
||||
default?: boolean;
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: string;
|
||||
model?: AgentModelConfig;
|
||||
memorySearch?: MemorySearchConfig;
|
||||
/** Human-like delay between block replies for this agent. */
|
||||
humanDelay?: HumanDelayConfig;
|
||||
|
||||
@@ -837,6 +837,15 @@ const ToolPolicySchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const ToolProfileSchema = z
|
||||
.union([
|
||||
z.literal("minimal"),
|
||||
z.literal("coding"),
|
||||
z.literal("messaging"),
|
||||
z.literal("full"),
|
||||
])
|
||||
.optional();
|
||||
|
||||
// Provider docking: allowlists keyed by provider id (no schema updates when adding providers).
|
||||
const ElevatedAllowFromSchema = z
|
||||
.record(z.string(), z.array(z.union([z.string(), z.number()])))
|
||||
@@ -866,6 +875,7 @@ const AgentSandboxSchema = z
|
||||
|
||||
const AgentToolsSchema = z
|
||||
.object({
|
||||
profile: ToolProfileSchema,
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
elevated: z
|
||||
@@ -930,14 +940,20 @@ const MemorySearchSchema = z
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const AgentModelSchema = z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
primary: z.string().optional(),
|
||||
fallbacks: z.array(z.string()).optional(),
|
||||
}),
|
||||
]);
|
||||
const AgentEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
default: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
model: AgentModelSchema.optional(),
|
||||
memorySearch: MemorySearchSchema,
|
||||
humanDelay: HumanDelaySchema.optional(),
|
||||
identity: IdentitySchema,
|
||||
@@ -962,6 +978,7 @@ const AgentEntrySchema = z.object({
|
||||
|
||||
const ToolsSchema = z
|
||||
.object({
|
||||
profile: ToolProfileSchema,
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
audio: z
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
@@ -449,6 +450,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
cfg: cfgWithAgentDefaults,
|
||||
provider,
|
||||
model,
|
||||
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||
params.cfg,
|
||||
agentId,
|
||||
),
|
||||
run: (providerOverride, modelOverride) => {
|
||||
if (isCliProvider(providerOverride, cfgWithAgentDefaults)) {
|
||||
const cliSessionId = getCliSessionId(
|
||||
|
||||
@@ -13,6 +13,14 @@ import type {
|
||||
|
||||
const providerId = "discord";
|
||||
|
||||
function readParentIdParam(
|
||||
params: Record<string, unknown>,
|
||||
): string | null | undefined {
|
||||
if (params.clearParent === true) return null;
|
||||
if (params.parentId === null) return null;
|
||||
return readStringParam(params, "parentId");
|
||||
}
|
||||
|
||||
export const discordMessageActions: ProviderMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledDiscordAccounts(cfg).filter(
|
||||
@@ -462,8 +470,7 @@ export const discordMessageActions: ProviderMessageActionAdapter = {
|
||||
const guildId = readStringParam(params, "guildId", { required: true });
|
||||
const name = readStringParam(params, "name", { required: true });
|
||||
const type = readNumberParam(params, "type", { integer: true });
|
||||
const parentId =
|
||||
params.parentId === null ? null : readStringParam(params, "parentId");
|
||||
const parentId = readParentIdParam(params);
|
||||
const topic = readStringParam(params, "topic");
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
const nsfw = typeof params.nsfw === "boolean" ? params.nsfw : undefined;
|
||||
@@ -489,8 +496,7 @@ export const discordMessageActions: ProviderMessageActionAdapter = {
|
||||
const name = readStringParam(params, "name");
|
||||
const topic = readStringParam(params, "topic");
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
const parentId =
|
||||
params.parentId === null ? null : readStringParam(params, "parentId");
|
||||
const parentId = readParentIdParam(params);
|
||||
const nsfw = typeof params.nsfw === "boolean" ? params.nsfw : undefined;
|
||||
const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", {
|
||||
integer: true,
|
||||
@@ -525,8 +531,7 @@ export const discordMessageActions: ProviderMessageActionAdapter = {
|
||||
const channelId = readStringParam(params, "channelId", {
|
||||
required: true,
|
||||
});
|
||||
const parentId =
|
||||
params.parentId === null ? null : readStringParam(params, "parentId");
|
||||
const parentId = readParentIdParam(params);
|
||||
const position = readNumberParam(params, "position", { integer: true });
|
||||
return await handleDiscordAction(
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user