fix(tools): harden schemas and oauth tool names
This commit is contained in:
@@ -59,6 +59,7 @@ import {
|
|||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
getApiKeyForModel,
|
getApiKeyForModel,
|
||||||
resolveAuthProfileOrder,
|
resolveAuthProfileOrder,
|
||||||
|
resolveModelAuthMode,
|
||||||
} from "./model-auth.js";
|
} from "./model-auth.js";
|
||||||
import { ensureClawdbotModelsJson } from "./models-config.js";
|
import { ensureClawdbotModelsJson } from "./models-config.js";
|
||||||
import {
|
import {
|
||||||
@@ -853,6 +854,8 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
abortSignal: runAbortController.signal,
|
abortSignal: runAbortController.signal,
|
||||||
|
modelProvider: model.provider,
|
||||||
|
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
||||||
// No currentChannelId/currentThreadTs for compaction - not in message context
|
// No currentChannelId/currentThreadTs for compaction - not in message context
|
||||||
});
|
});
|
||||||
const machineName = await getMachineDisplayName();
|
const machineName = await getMachineDisplayName();
|
||||||
@@ -1234,6 +1237,8 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
abortSignal: runAbortController.signal,
|
abortSignal: runAbortController.signal,
|
||||||
|
modelProvider: model.provider,
|
||||||
|
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
||||||
currentChannelId: params.currentChannelId,
|
currentChannelId: params.currentChannelId,
|
||||||
currentThreadTs: params.currentThreadTs,
|
currentThreadTs: params.currentThreadTs,
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
|
|||||||
@@ -31,35 +31,19 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(parameters.required ?? []).toContain("action");
|
expect(parameters.required ?? []).toContain("action");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires raw for gateway config.apply tool calls", () => {
|
it("exposes raw for gateway config.apply tool calls", () => {
|
||||||
const tools = createClawdbotCodingTools();
|
const tools = createClawdbotCodingTools();
|
||||||
const gateway = tools.find((tool) => tool.name === "gateway");
|
const gateway = tools.find((tool) => tool.name === "gateway");
|
||||||
expect(gateway).toBeDefined();
|
expect(gateway).toBeDefined();
|
||||||
|
|
||||||
const parameters = gateway?.parameters as {
|
const parameters = gateway?.parameters as {
|
||||||
allOf?: Array<Record<string, unknown>>;
|
type?: unknown;
|
||||||
|
required?: string[];
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
const conditional = parameters.allOf?.find(
|
expect(parameters.type).toBe("object");
|
||||||
(entry) => "if" in entry && "then" in entry,
|
expect(parameters.properties?.raw).toBeDefined();
|
||||||
) as
|
expect(parameters.required ?? []).not.toContain("raw");
|
||||||
| { if?: Record<string, unknown>; then?: Record<string, unknown> }
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
expect(conditional).toBeDefined();
|
|
||||||
const thenRequired = conditional?.then?.required as string[] | undefined;
|
|
||||||
expect(thenRequired ?? []).toContain("raw");
|
|
||||||
|
|
||||||
const action = (
|
|
||||||
conditional?.if?.properties as Record<string, unknown> | undefined
|
|
||||||
)?.action as { const?: unknown; enum?: unknown[] } | undefined;
|
|
||||||
const values = new Set<string>();
|
|
||||||
if (typeof action?.const === "string") values.add(action.const);
|
|
||||||
if (Array.isArray(action?.enum)) {
|
|
||||||
for (const value of action.enum) {
|
|
||||||
if (typeof value === "string") values.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(values.has("config.apply")).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flattens anyOf-of-literals to enum for provider compatibility", () => {
|
it("flattens anyOf-of-literals to enum for provider compatibility", () => {
|
||||||
@@ -174,6 +158,24 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renames blocked tool names only for Anthropic OAuth", () => {
|
||||||
|
const tools = createClawdbotCodingTools({
|
||||||
|
modelProvider: "anthropic",
|
||||||
|
modelAuthMode: "oauth",
|
||||||
|
});
|
||||||
|
const names = new Set(tools.map((tool) => tool.name));
|
||||||
|
expect(names.has("Bash")).toBe(true);
|
||||||
|
expect(names.has("Read")).toBe(true);
|
||||||
|
expect(names.has("Write")).toBe(true);
|
||||||
|
expect(names.has("Edit")).toBe(true);
|
||||||
|
|
||||||
|
// Ensure the blocked lowercase variants are not present in the schema.
|
||||||
|
expect(names.has("bash")).toBe(false);
|
||||||
|
expect(names.has("read")).toBe(false);
|
||||||
|
expect(names.has("write")).toBe(false);
|
||||||
|
expect(names.has("edit")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("provides top-level object schemas for all tools", () => {
|
it("provides top-level object schemas for all tools", () => {
|
||||||
const tools = createClawdbotCodingTools();
|
const tools = createClawdbotCodingTools();
|
||||||
const offenders = tools
|
const offenders = tools
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type ProcessToolDefaults,
|
type ProcessToolDefaults,
|
||||||
} from "./bash-tools.js";
|
} from "./bash-tools.js";
|
||||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||||
|
import type { ModelAuthMode } from "./model-auth.js";
|
||||||
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
|
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
|
||||||
import { assertSandboxPath } from "./sandbox-paths.js";
|
import { assertSandboxPath } from "./sandbox-paths.js";
|
||||||
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
|
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
|
||||||
@@ -283,6 +284,28 @@ function normalizeToolNames(list?: string[]) {
|
|||||||
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
|
||||||
|
* Renaming to capitalized versions bypasses the block while maintaining compatibility
|
||||||
|
* with Anthropic API keys and other providers.
|
||||||
|
*/
|
||||||
|
const OAUTH_BLOCKED_TOOL_NAMES: Record<string, string> = {
|
||||||
|
bash: "Bash",
|
||||||
|
read: "Read",
|
||||||
|
write: "Write",
|
||||||
|
edit: "Edit",
|
||||||
|
};
|
||||||
|
|
||||||
|
function renameBlockedToolsForOAuth(tools: AnyAgentTool[]): AnyAgentTool[] {
|
||||||
|
return tools.map((tool) => {
|
||||||
|
const newName = OAUTH_BLOCKED_TOOL_NAMES[tool.name];
|
||||||
|
if (newName) {
|
||||||
|
return { ...tool, name: newName };
|
||||||
|
}
|
||||||
|
return tool;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
"sessions_list",
|
"sessions_list",
|
||||||
"sessions_history",
|
"sessions_history",
|
||||||
@@ -532,6 +555,16 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
config?: ClawdbotConfig;
|
config?: ClawdbotConfig;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
|
/**
|
||||||
|
* Provider of the currently selected model (used for provider-specific tool quirks).
|
||||||
|
* Example: "anthropic", "openai", "google", "openai-codex".
|
||||||
|
*/
|
||||||
|
modelProvider?: string;
|
||||||
|
/**
|
||||||
|
* Auth mode for the current provider. We only need this for Anthropic OAuth
|
||||||
|
* tool-name blocking quirks.
|
||||||
|
*/
|
||||||
|
modelAuthMode?: ModelAuthMode;
|
||||||
/** Current channel ID for auto-threading (Slack). */
|
/** Current channel ID for auto-threading (Slack). */
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
@@ -634,8 +667,11 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
)
|
)
|
||||||
: normalized;
|
: normalized;
|
||||||
|
|
||||||
// NOTE: Keep canonical (lowercase) tool names here.
|
// Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
|
||||||
// pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names
|
// Only apply the rename when we are actually using (or likely using) Anthropic OAuth.
|
||||||
// on the wire and maps them back for tool dispatch.
|
const provider = options?.modelProvider?.trim();
|
||||||
return withAbort;
|
const authMode = options?.modelAuthMode;
|
||||||
|
const isAnthropicOAuth =
|
||||||
|
provider === "anthropic" && (authMode === "oauth" || authMode === "mixed");
|
||||||
|
return isAnthropicOAuth ? renameBlockedToolsForOAuth(withAbort) : withAbort;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,20 +37,10 @@ const GatewayToolSchema = Type.Object({
|
|||||||
note: Type.Optional(Type.String()),
|
note: Type.Optional(Type.String()),
|
||||||
restartDelayMs: Type.Optional(Type.Number()),
|
restartDelayMs: Type.Optional(Type.Number()),
|
||||||
});
|
});
|
||||||
// Keep top-level object schemas while enforcing conditional requirements.
|
// NOTE: We intentionally avoid top-level `allOf`/`anyOf`/`oneOf` conditionals here:
|
||||||
(GatewayToolSchema as typeof GatewayToolSchema & { allOf?: unknown[] }).allOf =
|
// - OpenAI rejects tool schemas that include these keywords at the *top-level*.
|
||||||
[
|
// - Claude/Vertex has other JSON Schema quirks.
|
||||||
{
|
// Conditional requirements (like `raw` for config.apply) are enforced at runtime.
|
||||||
if: {
|
|
||||||
properties: {
|
|
||||||
action: { const: "config.apply" },
|
|
||||||
},
|
|
||||||
required: ["action"],
|
|
||||||
},
|
|
||||||
// biome-ignore lint/suspicious/noThenProperty: JSON Schema keyword.
|
|
||||||
then: { required: ["raw"] },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function createGatewayTool(opts?: {
|
export function createGatewayTool(opts?: {
|
||||||
agentSessionKey?: string;
|
agentSessionKey?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user