Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 14:20:34 +07:00
parent 3cc7feab21
commit ed32394acd
14 changed files with 671 additions and 5845 deletions
@@ -0,0 +1,16 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "paseo-test-mcp", version: "1.0.0" });
server.tool(
"echo",
{ text: z.string() },
async ({ text }) => ({
content: [{ type: "text", text: `ECHO:${text}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
@@ -376,5 +376,20 @@ describe("curateAgentActivity", () => {
'[paseo__create_agent] {"cwd":"/tmp/repo","initialPrompt":"do the thing"}'
);
});
test("shows namespaced tool input JSON regardless of prefix format", () => {
const timeline: AgentTimelineItem[] = [
{
type: "tool_call",
callId: "m2",
name: "paseo_voice.speak",
input: { text: "hello from namespaced tool" },
status: "completed",
},
];
const result = curateAgentActivity(timeline);
expect(result).toBe('[paseo_voice.speak] {"text":"hello from namespaced tool"}');
});
});
});
@@ -1,5 +1,6 @@
import type { AgentTimelineItem } from "./agent-sdk-types.js";
import { extractPrincipalParam } from "../../utils/tool-call-parsers.js";
import { isLikelyExternalToolName } from "./tool-name-normalization.js";
const DEFAULT_MAX_ITEMS = 40;
const MAX_TOOL_INPUT_CHARS = 400;
@@ -26,17 +27,6 @@ function flushBuffers(lines: string[], buffers: { message: string; thought: stri
buffers.thought = "";
}
function isLikelyMcpToolCall(name: string): boolean {
const normalized = name.toLowerCase();
return (
normalized === "speak" ||
normalized.startsWith("mcp") ||
normalized.includes("mcp__") ||
normalized.startsWith("paseo") ||
normalized.includes("paseo__")
);
}
function formatToolInputJson(input: unknown): string | null {
if (input === undefined) {
return null;
@@ -158,7 +148,7 @@ export function curateAgentActivity(
case "tool_call": {
flushBuffers(lines, buffers);
const inputJson = formatToolInputJson(item.input);
if (isLikelyMcpToolCall(item.name) && inputJson) {
if (isLikelyExternalToolName(item.name) && inputJson) {
lines.push(`[${item.name}] ${inputJson}`);
break;
}
@@ -170,6 +170,77 @@ describe("Codex app-server provider (integration)", () => {
}
}, 120000);
test.runIf(isCodexInstalled())("round-trips a stdio MCP tool call", async () => {
const cleanup = useTempCodexSessionDir();
const cwd = tmpCwd("codex-mcp-roundtrip-");
const token = `MCP_ROUNDTRIP_${Date.now()}`;
const mcpScriptPath = path.resolve(process.cwd(), "scripts", "mcp-echo-test-server.mjs");
try {
const client = new CodexAppServerAgentClient(logger);
const session = await client.createSession({
provider: "codex",
cwd,
modeId: "read-only",
model: CODEX_TEST_MODEL,
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
mcpServers: {
paseo_test: {
type: "stdio",
command: process.execPath,
args: [mcpScriptPath],
},
},
});
const result = await session.run(
[
"You must call the MCP tool named paseo_test.echo exactly once.",
`Call it with text: ${token}`,
"Do not use shell or any non-MCP tools.",
"After the tool call, respond with exactly the tool output text.",
].join(" ")
);
await session.close();
const toolCalls = result.timeline.filter(
(item): item is Extract<AgentTimelineItem, { type: "tool_call" }> =>
item.type === "tool_call"
);
const toolNames = toolCalls.map((item) => item.name);
const distinctMcpCalls = new Map<string, Extract<AgentTimelineItem, { type: "tool_call" }>>();
for (const call of toolCalls) {
if (call.name !== "paseo_test.echo") {
continue;
}
const key = String(call.callId ?? `${call.name}:${JSON.stringify(call.input ?? {})}`);
const existing = distinctMcpCalls.get(key);
if (!existing || call.status === "completed") {
distinctMcpCalls.set(key, call);
}
}
// Hard assertion: exactly one distinct call of the exact MCP tool.
expect(toolNames.every((name) => name === "paseo_test.echo")).toBe(true);
expect(distinctMcpCalls.size).toBe(1);
const mcpToolCall = Array.from(distinctMcpCalls.values())[0]!;
expect(mcpToolCall.name).toBe("paseo_test.echo");
expect(mcpToolCall.status).toBe("completed");
// Hard assertion: no non-MCP tools in this run.
expect(toolNames.every((name) => name === "paseo_test.echo")).toBe(true);
expect(toolNames.some((name) => name.toLowerCase().includes("shell"))).toBe(false);
// Hard assertion: roundtrip token must be present in the MCP tool I/O.
expect(JSON.stringify(mcpToolCall.input ?? {})).toContain(token);
expect(JSON.stringify(mcpToolCall.output ?? {})).toContain(`ECHO:${token}`);
expect(result.finalText).toContain(`ECHO:${token}`);
} finally {
cleanup();
rmSync(cwd, { recursive: true, force: true });
}
}, 120000);
test.runIf(isCodexInstalled())("listCommands includes custom prompts and executeCommand runs them", async () => {
const cleanup = useTempCodexSessionDir();
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
@@ -34,7 +34,7 @@ import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import { z } from "zod";
import { loadCodexPersistedTimeline } from "./codex-mcp-agent.js";
import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
@@ -1,38 +0,0 @@
import { describe, test, expect } from "vitest";
import { __test__ } from "./codex-mcp-agent.js";
describe("codex custom command plumbing", () => {
test("parseFrontMatter extracts metadata and body", () => {
const input = [
"---",
"description: Hello",
"argument-hint: NAME=<name>",
"---",
"",
"Say hi to $NAME",
"",
].join("\n");
const parsed = __test__.parseFrontMatter(input);
expect(parsed.frontMatter["description"]).toBe("Hello");
expect(parsed.frontMatter["argument-hint"]).toBe("NAME=<name>");
expect(parsed.body).toContain("Say hi to $NAME");
});
test("expandCodexCustomPrompt supports named + positional placeholders", () => {
const template = "A=$A B=$B 1=$1 2=$2 ARGS=$ARGUMENTS $$=$$";
const out = __test__.expandCodexCustomPrompt(template, "A=hello B=world one two");
expect(out).toContain("A=hello");
expect(out).toContain("B=world");
expect(out).toContain("1=one");
expect(out).toContain("2=two");
expect(out).toContain("ARGS=A=hello B=world one two");
expect(out).toContain("$=$");
});
test("tokenizeCommandArgs respects quotes", () => {
const tokens = __test__.tokenizeCommandArgs('A=\"hello world\" B=two three');
expect(tokens).toEqual(["A=hello world", "B=two", "three"]);
});
});
@@ -1,19 +0,0 @@
import { describe, expect, test } from "vitest";
import { __test__ } from "./codex-mcp-agent.js";
describe("codex resume error detection", () => {
test("detects missing session errors for thread_id", () => {
const error = new Error(
"Session not found for thread_id: 019bda3e-ffe4-7bc1-ae0e-9b992d7c9360"
);
expect(__test__.isMissingConversationIdError(error)).toBe(true);
});
test("detects missing session responses for thread_id", () => {
const response = {
content: [{ type: "text", text: "Session not found for thread_id: abc" }],
};
expect(__test__.isMissingConversationIdResponse(response)).toBe(true);
});
});
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { rmSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parseRolloutFile } from "./codex-mcp-agent.js";
import { parseRolloutFile } from "./codex-rollout-timeline.js";
describe("codex rollout parsing", () => {
let tmpDir: string;
@@ -0,0 +1,502 @@
import { promises as fs, type Dirent } from "node:fs";
import os from "node:os";
import path from "node:path";
import { z } from "zod";
import type { Logger } from "pino";
import type { AgentTimelineItem } from "../agent-sdk-types.js";
const MAX_ROLLOUT_SEARCH_DEPTH = 4;
function resolveCodexSessionRoot(): string | null {
if (process.env.CODEX_SESSION_DIR) {
return process.env.CODEX_SESSION_DIR;
}
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
return path.join(codexHome, "sessions");
}
async function findRolloutFile(
threadId: string,
root: string
): Promise<string | null> {
const stack: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }];
while (stack.length > 0) {
const { dir, depth } = stack.pop()!;
let entries: Dirent[];
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isFile()) {
const matchesThread = entry.name.includes(threadId);
const matchesPrefix = entry.name.startsWith("rollout-");
const matchesExtension =
entry.name.endsWith(".json") || entry.name.endsWith(".jsonl");
if (matchesThread && matchesPrefix && matchesExtension) {
return entryPath;
}
} else if (entry.isDirectory() && depth < MAX_ROLLOUT_SEARCH_DEPTH) {
stack.push({ dir: entryPath, depth: depth + 1 });
}
}
}
return null;
}
const RolloutContentItemSchema = z
.union([
z.object({ type: z.literal("input_text"), text: z.string() }),
z.object({ type: z.literal("output_text"), text: z.string() }),
z.object({ type: z.literal("reasoning_text"), text: z.string() }),
z.object({ type: z.literal("text"), text: z.string() }),
z
.object({
type: z.string(),
text: z.string().optional(),
message: z.string().optional(),
})
.passthrough(),
]);
const RolloutContentArraySchema = z.array(RolloutContentItemSchema);
function extractContentTextByType(content: unknown, itemType: string): string {
const parsed = RolloutContentArraySchema.safeParse(content);
if (!parsed.success) {
return "";
}
return parsed.data
.filter((item) => item.type === itemType)
.map((item) => item.text)
.join("\n")
.trim();
}
const RolloutResponseMessagePayloadSchema = z.object({
type: z.literal("message"),
role: z.enum(["user", "assistant"]).optional(),
content: z.unknown().optional(),
});
const RolloutResponseReasoningPayloadSchema = z.object({
type: z.literal("reasoning"),
content: z.unknown().optional(),
summary: z.array(z.object({ text: z.string().optional() })).optional(),
text: z.string().optional(),
});
const RolloutResponseFunctionCallPayloadSchema = z.object({
type: z.literal("function_call"),
name: z.string().optional(),
call_id: z.string().optional(),
arguments: z.string().optional(),
});
const RolloutResponseCustomToolCallPayloadSchema = z.object({
type: z.literal("custom_tool_call"),
name: z.string().optional(),
call_id: z.string().optional(),
arguments: z.string().optional(),
});
const RolloutResponseFunctionCallOutputPayloadSchema = z.object({
type: z.literal("function_call_output"),
call_id: z.string().optional(),
output: z.string().optional(),
});
const RolloutEventAgentReasoningPayloadSchema = z.object({
type: z.literal("agent_reasoning"),
text: z.string().optional(),
});
const RolloutEventAgentMessagePayloadSchema = z.object({
type: z.literal("agent_message"),
message: z
.union([
z.string(),
z
.object({
role: z.string().optional(),
message: z.string().optional(),
text: z.string().optional(),
})
.passthrough(),
])
.optional(),
});
const RolloutEventUserMessagePayloadSchema = z.object({
type: z.literal("user_message"),
message: z
.union([
z.string(),
z
.object({
role: z.string().optional(),
message: z.string().optional(),
text: z.string().optional(),
})
.passthrough(),
])
.optional(),
});
type RolloutResponseReasoningPayload = z.infer<
typeof RolloutResponseReasoningPayloadSchema
>;
type ParsedRolloutRecord =
| { kind: "timeline"; item: AgentTimelineItem }
| { kind: "call"; name: string; callId?: string; input?: unknown }
| { kind: "output"; callId: string; output: string }
| { kind: "ignore" };
const RolloutMessageContentSchema = z
.union([
z.string().transform((content) => content.trim()),
z.array(
z
.object({
text: z.string().optional(),
message: z.string().optional(),
})
.passthrough()
).transform((content) =>
content
.map((block) => block.text ?? block.message ?? "")
.map((text) => text.trim())
.filter(Boolean)
.join("\n")
.trim()
),
]);
function extractMessageText(content: unknown): string {
const parsed = RolloutMessageContentSchema.safeParse(content);
if (!parsed.success) {
return "";
}
return parsed.data;
}
const RolloutEventMessageTextSchema = z
.union([
z.string(),
z
.object({
message: z.string().optional(),
text: z.string().optional(),
})
.passthrough()
.transform((message) => message.message ?? message.text ?? ""),
]);
function extractEventMessageText(message: unknown): string {
const parsed = RolloutEventMessageTextSchema.safeParse(message);
if (!parsed.success) {
return "";
}
return parsed.data;
}
function isSyntheticRolloutUserMessage(text: string): boolean {
const normalized = text.trim();
if (!normalized) {
return false;
}
const lower = normalized.toLowerCase();
if (
lower.startsWith("# agents.md instructions for") &&
lower.includes("<instructions>")
) {
return true;
}
if (lower.startsWith("<environment_context>")) {
return true;
}
return false;
}
function extractReasoningText(payload: RolloutResponseReasoningPayload): string {
if (Array.isArray(payload.summary)) {
const text = payload.summary
.map((item) => item.text ?? "")
.filter(Boolean)
.join("\n")
.trim();
if (text) {
return text;
}
}
const contentText = extractContentTextByType(payload.content, "reasoning_text");
if (contentText) {
return contentText;
}
if (typeof payload.text === "string") {
return payload.text;
}
return "";
}
function parseJsonLikeString(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return value;
}
}
const FunctionCallInputNormalizationSchema = z
.union([
z.object({ cmd: z.string() }).transform((input) => ({ name: "Bash", input: { command: input.cmd } })),
z
.object({ command: z.array(z.string()) })
.transform((input) => ({ name: "Bash", input: { command: input.command[2] ?? "" } })),
z.unknown().transform((input) => ({ name: "unknown", input })),
]);
const RolloutResponseRecordSchema = z
.union([
RolloutResponseMessagePayloadSchema.transform((payload): ParsedRolloutRecord => {
const text = extractMessageText(payload.content);
const itemType = payload.role === "assistant" ? "assistant_message" : "user_message";
const shouldEmit = text.length > 0 && (itemType !== "user_message" || !isSyntheticRolloutUserMessage(text));
return shouldEmit
? { kind: "timeline", item: { type: itemType, text } }
: { kind: "ignore" };
}),
RolloutResponseReasoningPayloadSchema.transform((payload): ParsedRolloutRecord => {
const text = extractReasoningText(payload);
return text.length > 0
? { kind: "timeline", item: { type: "reasoning", text } }
: { kind: "ignore" };
}),
z
.union([
RolloutResponseFunctionCallPayloadSchema,
RolloutResponseCustomToolCallPayloadSchema,
])
.transform((payload): ParsedRolloutRecord => {
const rawName = payload.name ?? "unknown";
const parsedArguments = payload.arguments ? parseJsonLikeString(payload.arguments) : undefined;
const normalized =
rawName === "exec_command" || rawName === "shell"
? FunctionCallInputNormalizationSchema.parse(parsedArguments)
: { name: rawName, input: parsedArguments };
const skip = rawName === "write_stdin";
return skip
? { kind: "ignore" }
: {
kind: "call",
name: normalized.name,
callId: payload.call_id,
input: normalized.input,
};
}),
RolloutResponseFunctionCallOutputPayloadSchema.transform(
(payload): ParsedRolloutRecord =>
payload.call_id && payload.output
? { kind: "output", callId: payload.call_id, output: payload.output }
: { kind: "ignore" }
),
z.unknown().transform((): ParsedRolloutRecord => ({ kind: "ignore" })),
]);
const RolloutEventRecordSchema = z
.union([
RolloutEventAgentReasoningPayloadSchema.transform(
(payload): ParsedRolloutRecord =>
payload.text
? { kind: "timeline", item: { type: "reasoning", text: payload.text } }
: { kind: "ignore" }
),
RolloutEventAgentMessagePayloadSchema.transform((payload): ParsedRolloutRecord => {
const text = extractEventMessageText(payload.message);
return text.length > 0
? { kind: "timeline", item: { type: "assistant_message", text } }
: { kind: "ignore" };
}),
RolloutEventUserMessagePayloadSchema.transform((payload): ParsedRolloutRecord => {
const text = extractEventMessageText(payload.message);
const shouldEmit = text.length > 0 && !isSyntheticRolloutUserMessage(text);
return shouldEmit
? { kind: "timeline", item: { type: "user_message", text } }
: { kind: "ignore" };
}),
z.unknown().transform((): ParsedRolloutRecord => ({ kind: "ignore" })),
]);
const RolloutRecordSchema = z
.object({
type: z.enum(["response_item", "event_msg"]),
payload: z.unknown().optional(),
item: z.unknown().optional(),
msg: z.unknown().optional(),
})
.transform((entry) =>
entry.type === "response_item"
? RolloutResponseRecordSchema.parse(entry.payload ?? entry.item)
: RolloutEventRecordSchema.parse(entry.payload ?? entry.msg)
);
function parseJsonRolloutTimeline(
parsed: unknown
): AgentTimelineItem[] | null {
if (!parsed || typeof parsed !== "object") {
return null;
}
const items = (parsed as { items?: unknown }).items;
if (!Array.isArray(items)) {
return null;
}
const timeline: AgentTimelineItem[] = [];
for (const entry of items) {
if (!entry || typeof entry !== "object") {
continue;
}
const messagePayloadResult =
RolloutResponseMessagePayloadSchema.safeParse(entry);
if (messagePayloadResult.success) {
const payload = messagePayloadResult.data;
const text = extractMessageText(payload.content);
if (!text) {
continue;
}
if (payload.role === "assistant") {
timeline.push({ type: "assistant_message", text });
} else if (payload.role === "user") {
if (!isSyntheticRolloutUserMessage(text)) {
timeline.push({ type: "user_message", text });
}
}
continue;
}
const reasoningPayloadResult =
RolloutResponseReasoningPayloadSchema.safeParse(entry);
if (reasoningPayloadResult.success) {
const text = extractReasoningText(reasoningPayloadResult.data);
if (text) {
timeline.push({ type: "reasoning", text });
}
continue;
}
}
return timeline;
}
export async function parseRolloutFile(
filePath: string
): Promise<AgentTimelineItem[]> {
const content = await fs.readFile(filePath, "utf8");
const trimmed = content.trim();
if (!trimmed) {
return [];
}
try {
const parsed = JSON.parse(trimmed);
const jsonTimeline = parseJsonRolloutTimeline(parsed);
if (jsonTimeline) {
return jsonTimeline;
}
} catch {
// Fall back to JSONL parsing.
}
const lines = trimmed
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const parsedRecords = lines
.map((line) => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter((record): record is unknown => record !== null)
.map((record) => RolloutRecordSchema.safeParse(record))
.filter((result): result is { success: true; data: ParsedRolloutRecord } => result.success)
.map((result) => result.data);
const outputsByCallId = parsedRecords
.filter((record): record is Extract<ParsedRolloutRecord, { kind: "output" }> => record.kind === "output")
.reduce((map, record) => map.set(record.callId, record.output), new Map<string, string>());
return parsedRecords.flatMap((record): AgentTimelineItem[] =>
record.kind === "timeline"
? [record.item]
: record.kind === "call"
? [
{
type: "tool_call",
name: record.name,
callId: record.callId,
status: "completed",
input: record.input,
output: record.callId ? outputsByCallId.get(record.callId) : undefined,
},
]
: []
);
}
export type CodexPersistedTimelineOptions = {
sessionRoot?: string | null;
rolloutPath?: string | null;
};
export async function loadCodexPersistedTimeline(
sessionId: string,
options?: CodexPersistedTimelineOptions,
logger?: Logger
): Promise<AgentTimelineItem[]> {
const rolloutPath = options?.rolloutPath ?? null;
if (rolloutPath) {
try {
const stat = await fs.stat(rolloutPath);
if (stat.isFile()) {
const timeline = await parseRolloutFile(rolloutPath);
if (timeline.length > 0) {
return timeline;
}
}
} catch {
// Fall back to session root scan.
}
}
try {
const preferredRoot = options?.sessionRoot ?? resolveCodexSessionRoot();
const fallbackRoot = resolveCodexSessionRoot();
let rolloutFile: string | null = null;
if (preferredRoot) {
rolloutFile = await findRolloutFile(sessionId, preferredRoot);
}
if (
!rolloutFile &&
fallbackRoot &&
fallbackRoot !== preferredRoot
) {
rolloutFile = await findRolloutFile(sessionId, fallbackRoot);
}
if (!rolloutFile) {
return [];
}
return await parseRolloutFile(rolloutFile);
} catch (error) {
logger?.warn(
{ err: error, sessionId },
"Failed to load persisted timeline"
);
return [];
}
}
@@ -0,0 +1,35 @@
const TOOL_TOKEN_REGEX = /[a-z0-9]+/g;
const NAMESPACE_SEPARATOR_REGEX = /[.:/]|__/;
export function normalizeToolName(name: string): string {
return name.trim().toLowerCase();
}
export function tokenizeToolName(name: string): string[] {
const normalized = normalizeToolName(name);
return normalized.match(TOOL_TOKEN_REGEX) ?? [];
}
export function getToolLeafName(name: string): string | null {
const tokens = tokenizeToolName(name);
return tokens.length > 0 ? tokens[tokens.length - 1]! : null;
}
export function isSpeakToolName(name: string): boolean {
return getToolLeafName(name) === "speak";
}
export function isLikelyNamespacedToolName(name: string): boolean {
return NAMESPACE_SEPARATOR_REGEX.test(normalizeToolName(name));
}
export function isLikelyExternalToolName(name: string): boolean {
const normalized = normalizeToolName(name);
if (!normalized) {
return false;
}
if (isSpeakToolName(normalized)) {
return true;
}
return isLikelyNamespacedToolName(normalized);
}
@@ -21,6 +21,7 @@ import type {
ListModelsOptions,
} from "../agent/agent-sdk-types.js";
import type { AgentPermissionRequest, AgentPermissionResponse } from "../agent/agent-sdk-types.js";
import { isLikelyExternalToolName } from "../agent/tool-name-normalization.js";
const TEST_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
@@ -656,7 +657,7 @@ class FakeAgentSession implements AgentSession {
return false;
}
if (toolName.startsWith("mcp__")) {
if (isLikelyExternalToolName(toolName)) {
return true;
}
@@ -14,14 +14,20 @@ function buildRequest(partial: Partial<AgentPermissionRequest>): AgentPermission
}
describe("isVoicePermissionAllowed", () => {
test("allows speak tool", () => {
test("allows direct speak tool names across provider conventions", () => {
const result = isVoicePermissionAllowed(
buildRequest({ name: "speak" })
);
expect(result).toBe(true);
expect(
isVoicePermissionAllowed(buildRequest({ name: "paseo_voice.speak" }))
).toBe(true);
expect(
isVoicePermissionAllowed(buildRequest({ name: "mcp__paseo_voice__speak" }))
).toBe(true);
});
test("denies explicit MCP/paseo tool names", () => {
test("denies non-speak tool names", () => {
expect(
isVoicePermissionAllowed(buildRequest({ name: "mcp__paseo__create_agent" }))
).toBe(false);
@@ -37,12 +43,12 @@ describe("isVoicePermissionAllowed", () => {
expect(result).toBe(false);
});
test("allows codextool only when metadata references speak", () => {
test("allows wrapper tools when metadata references speak", () => {
const allowed = isVoicePermissionAllowed(
buildRequest({
name: "codextool",
metadata: {
questions: [{ question: "Allow codextool to call speak for user feedback?" }],
questions: [{ question: "Allow codextool to call paseo_voice.speak for user feedback?" }],
},
})
);
@@ -1,20 +1,22 @@
import type { AgentPermissionRequest } from "./agent/agent-sdk-types.js";
import { isSpeakToolName } from "./agent/tool-name-normalization.js";
const ALLOWED_TOKEN_SET = ["speak"];
const SPEAK_TOKEN_SET = ["speak"];
const DENIED_TOKEN_SET = [
"mcp",
"paseo",
"bash",
"shell",
"terminal",
"command",
"execute",
"apply_patch",
"edit",
"write",
"read",
"fetch",
"http",
"web",
"read_file",
"write_file",
"delete_file",
"web_search",
"fetch_url",
"create_agent",
"list_agents",
"kill_agent",
"wait_for_agent",
];
function containsAny(text: string, tokens: readonly string[]): boolean {
@@ -40,15 +42,14 @@ export function isVoicePermissionAllowed(request: AgentPermissionRequest): boole
return false;
}
if (normalizedName === "speak") {
if (isSpeakToolName(normalizedName)) {
return true;
}
if (normalizedName !== "codextool") {
return false;
}
const metadataText = stringifyMetadata({
name: request.name,
title: request.title ?? null,
description: request.description ?? null,
metadata: request.metadata ?? null,
input: request.input ?? null,
});