mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(talk): preserve caller tool authority in consults (#125392)
This commit is contained in:
@@ -60,6 +60,7 @@ type StartChatDispatchParams = {
|
||||
attachments: PreparedChatSendAttachments;
|
||||
client: GatewayRequestHandlerOptions["client"];
|
||||
context: GatewayRequestHandlerOptions["context"];
|
||||
toolsAllow?: string[];
|
||||
cronCreatorAuthority: ReturnType<ChatSendExternalAuthorityAdmission["resolve"]>;
|
||||
externalAuthorityAdmission: ChatSendExternalAuthorityAdmission | undefined;
|
||||
injection: {
|
||||
@@ -89,6 +90,7 @@ export function startChatDispatch(params: StartChatDispatchParams): void {
|
||||
attachments,
|
||||
client,
|
||||
context,
|
||||
toolsAllow,
|
||||
cronCreatorAuthority,
|
||||
externalAuthorityAdmission,
|
||||
injection,
|
||||
@@ -248,6 +250,7 @@ export function startChatDispatch(params: StartChatDispatchParams): void {
|
||||
dispatchInboundMessageWithProjectedDispatcher({
|
||||
ctx,
|
||||
cfg,
|
||||
toolsAllow,
|
||||
dispatcherOptions: replyDispatch.dispatcherOptions,
|
||||
onSessionMetadataChanges: (changes) =>
|
||||
changes.forEach((change) => emitSessionsChanged(context, change)),
|
||||
|
||||
@@ -31,7 +31,7 @@ async function handleChatSendWithOptions(
|
||||
{ params, respond, context, client }: GatewayRequestHandlerOptions,
|
||||
onAdmissionOwned?: () => Promise<boolean>,
|
||||
externalAuthorityAdmission?: ChatSendExternalAuthorityAdmission,
|
||||
options?: { trustedSystemInput?: boolean },
|
||||
options?: { trustedSystemInput?: boolean; toolsAllow?: string[] },
|
||||
): Promise<void> {
|
||||
const setup = await prepareAndAdmitChatSend(
|
||||
{ params, respond, context, client },
|
||||
@@ -273,6 +273,7 @@ async function handleChatSendWithOptions(
|
||||
attachments: preparedAttachments.value,
|
||||
client,
|
||||
context,
|
||||
toolsAllow: options?.toolsAllow,
|
||||
cronCreatorAuthority,
|
||||
externalAuthorityAdmission,
|
||||
injection: {
|
||||
@@ -311,6 +312,14 @@ export async function handleChatSend(
|
||||
await handleChatSendWithOptions(options, onAdmissionOwned, externalAuthorityAdmission);
|
||||
}
|
||||
|
||||
/** Dispatches an internally delegated turn within its caller-owned tool boundary. */
|
||||
export async function handleChatSendWithRuntimeTools(
|
||||
options: GatewayRequestHandlerOptions,
|
||||
toolsAllow: string[],
|
||||
): Promise<void> {
|
||||
await handleChatSendWithOptions(options, undefined, undefined, { toolsAllow });
|
||||
}
|
||||
|
||||
/** Dispatches Gateway-authored system input without widening the public chat-send contract. */
|
||||
export async function handleTrustedInternalChatSend(
|
||||
options: GatewayRequestHandlerOptions,
|
||||
|
||||
@@ -61,7 +61,7 @@ import { normalizeSessionDeliveryState } from "../../utils/delivery-context.shar
|
||||
import { consumeCronCreatorAuthorityGrant } from "../cron-creator-authority-grant.js";
|
||||
import { createChatRunState } from "../server-chat-state.js";
|
||||
import { STALE_WORKER_BUILD_REASON } from "../worker-environments/admission.js";
|
||||
import { handleChatSend } from "./chat-send-handler.js";
|
||||
import { handleChatSend, handleChatSendWithRuntimeTools } from "./chat-send-handler.js";
|
||||
import type { GatewayRequestContext, RespondFn } from "./types.js";
|
||||
|
||||
type ProjectedDispatchParams = Parameters<
|
||||
@@ -1161,6 +1161,7 @@ async function runNonStreamingChatSend(params: {
|
||||
waitForCompletion?: boolean;
|
||||
waitForDedupe?: boolean;
|
||||
waitFor?: NonStreamingChatSendWaitFor;
|
||||
runtimeToolsAllow?: string[];
|
||||
}): Promise<Record<string, any> | undefined> {
|
||||
const sendParams: {
|
||||
sessionKey: string;
|
||||
@@ -1179,7 +1180,7 @@ async function runNonStreamingChatSend(params: {
|
||||
params.directExternal === false
|
||||
? handleChatSend
|
||||
: expectDefined(chatHandlers["chat.send"], 'chatHandlers["chat.send"] test invariant');
|
||||
await handler({
|
||||
const handlerOptions = {
|
||||
params: {
|
||||
...sendParams,
|
||||
...params.requestParams,
|
||||
@@ -1189,7 +1190,12 @@ async function runNonStreamingChatSend(params: {
|
||||
client: (params.client ?? null) as never,
|
||||
isWebchatConnect: () => false,
|
||||
context: params.context,
|
||||
});
|
||||
};
|
||||
if (params.runtimeToolsAllow) {
|
||||
await handleChatSendWithRuntimeTools(handlerOptions, params.runtimeToolsAllow);
|
||||
} else {
|
||||
await handler(handlerOptions);
|
||||
}
|
||||
|
||||
const waitFor =
|
||||
params.waitFor ??
|
||||
@@ -1360,6 +1366,22 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
mockState.cronAuthorityProbe = undefined;
|
||||
});
|
||||
|
||||
it("carries an internal runtime tool cap into agent dispatch", async () => {
|
||||
await createGatewayUserTurnSqliteFixture("openclaw-chat-send-runtime-tools-");
|
||||
const { send } = createChatRequestFixture();
|
||||
|
||||
await send({
|
||||
idempotencyKey: "idem-runtime-tools",
|
||||
directExternal: false,
|
||||
runtimeToolsAllow: ["read"],
|
||||
waitFor: "dedupe",
|
||||
});
|
||||
|
||||
expect(dispatchInboundMessageMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ toolsAllow: ["read"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["stale", "previous-leaf"],
|
||||
["empty", null],
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
closeTalkClientGatewayControlSession,
|
||||
createTalkClientAgentConsultRunner,
|
||||
createTalkClientGatewayControlOwner,
|
||||
resolveTalkAgentConsultAuthority,
|
||||
} from "../talk-client-gateway-control.js";
|
||||
import {
|
||||
ensureTalkRealtimeRelayVoiceSession,
|
||||
@@ -299,6 +300,7 @@ export const talkClientHandlers: GatewayRequestHandlers = {
|
||||
agentId,
|
||||
sessionKey,
|
||||
...(ownerConnId ? { ownerConnId } : {}),
|
||||
authority: resolveTalkAgentConsultAuthority(client?.connect?.scopes),
|
||||
getVoiceSessionId: () => activeVoiceSessionId,
|
||||
initialItems,
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-reso
|
||||
import { ADMIN_SCOPE } from "../operator-scopes.js";
|
||||
import { resolveRequestedSessionAgentId } from "../session-request-agent.js";
|
||||
import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js";
|
||||
import { resolveTalkAgentConsultAuthority } from "../talk-client-gateway-control.js";
|
||||
import { createTalkHandoff, getTalkHandoff, revokeTalkHandoff } from "../talk-handoff.js";
|
||||
import {
|
||||
cancelTalkRealtimeRelayTurn,
|
||||
@@ -297,6 +298,7 @@ export const talkSessionHandlers: GatewayRequestHandlers = {
|
||||
context,
|
||||
connId,
|
||||
cfg: runtimeConfig,
|
||||
consultAuthority: resolveTalkAgentConsultAuthority(client?.connect?.scopes),
|
||||
provider: resolution.provider,
|
||||
providerConfig: relayLaunch.providerConfig,
|
||||
instructions: buildRealtimeInstructions(realtimeContext.instructions),
|
||||
|
||||
@@ -185,6 +185,7 @@ vi.mock("../../talk/client-voice-session.js", async (importOriginal) => {
|
||||
|
||||
vi.mock("./chat-send-handler.js", () => ({
|
||||
handleChatSend: mocks.chatSend,
|
||||
handleChatSendWithRuntimeTools: mocks.chatSend,
|
||||
}));
|
||||
|
||||
vi.mock("../sessions-resolve.js", () => ({
|
||||
@@ -1653,6 +1654,7 @@ describe("talk.session unified handlers", () => {
|
||||
language: "de",
|
||||
},
|
||||
respond: createRespond,
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.talk"] } },
|
||||
context: {
|
||||
getRuntimeConfig: () =>
|
||||
({
|
||||
@@ -1687,7 +1689,15 @@ describe("talk.session unified handlers", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expectRecordFields(relayCreateInput, { connId: "conn-1", provider, language: "de" });
|
||||
expectRecordFields(relayCreateInput, {
|
||||
connId: "conn-1",
|
||||
provider,
|
||||
language: "de",
|
||||
consultAuthority: {
|
||||
senderIsOwner: false,
|
||||
toolsAllow: ["read", "web_search", "web_fetch", "x_search", "memory_search", "memory_get"],
|
||||
},
|
||||
});
|
||||
expectRecordFields(relayCreateInput.providerConfig, {
|
||||
apiKey: "openai-key",
|
||||
model: "gpt-realtime",
|
||||
@@ -2495,6 +2505,7 @@ describe("talk.client.toolCall handler", () => {
|
||||
args: { question: "What is in this repo?", responseStyle: "one sentence" },
|
||||
},
|
||||
respond,
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.talk"] } },
|
||||
context: {
|
||||
getRuntimeConfig: () => ({}) as OpenClawConfig,
|
||||
},
|
||||
@@ -2508,6 +2519,14 @@ describe("talk.client.toolCall handler", () => {
|
||||
expectRecordFields(chatInput.params, { sessionKey: "main" });
|
||||
expect(chatInput.params?.message).toContain("What is in this repo?");
|
||||
expect(chatInput.params?.idempotencyKey).toMatch(/^talk-call-1-/);
|
||||
expect(mockCallArg(mocks.chatSend, 0, 1)).toEqual([
|
||||
"read",
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
"x_search",
|
||||
"memory_search",
|
||||
"memory_get",
|
||||
]);
|
||||
const response = expectRespondOk(respond, { runId: "run-voice-1" }) as Record<string, unknown>;
|
||||
expect(response.idempotencyKey).toMatch(/^talk-call-1-/);
|
||||
});
|
||||
@@ -2554,6 +2573,7 @@ describe("talk.client.toolCall handler", () => {
|
||||
args: { question: "Are the basement lights off?" },
|
||||
},
|
||||
respond,
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.write"] } },
|
||||
context: {
|
||||
getRuntimeConfig: () =>
|
||||
({
|
||||
@@ -2570,6 +2590,7 @@ describe("talk.client.toolCall handler", () => {
|
||||
thinking: "low",
|
||||
fastMode: true,
|
||||
});
|
||||
expect(mockCallArg(mocks.chatSend, 0, 1)).toBeUndefined();
|
||||
expectRespondOk(respond, { runId: "run-voice-1" });
|
||||
});
|
||||
|
||||
@@ -2835,6 +2856,7 @@ describe("talk.client.create handler", () => {
|
||||
reasoningEffort: "low",
|
||||
},
|
||||
respond,
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.talk"] } },
|
||||
context: {
|
||||
getRuntimeConfig: () =>
|
||||
({
|
||||
@@ -2903,6 +2925,8 @@ describe("talk.client.create handler", () => {
|
||||
],
|
||||
surface: "a browser Talk session",
|
||||
abortSignal: consultSignal,
|
||||
senderIsOwner: false,
|
||||
toolsAllow: ["read", "web_search", "web_fetch", "x_search", "memory_search", "memory_get"],
|
||||
}),
|
||||
);
|
||||
expect(createInput).not.toHaveProperty("provider");
|
||||
@@ -2959,6 +2983,7 @@ describe("talk.client.create handler", () => {
|
||||
await callTalkHandler("talk.client.create", {
|
||||
params: { sessionKey: "main", model: "gpt-live-1" },
|
||||
respond,
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.write"] } },
|
||||
context: {
|
||||
getRuntimeConfig: () =>
|
||||
({
|
||||
@@ -2983,6 +3008,12 @@ describe("talk.client.create handler", () => {
|
||||
model: "gpt-live-1",
|
||||
runAgentConsult: expect.any(Function),
|
||||
});
|
||||
await (
|
||||
createInput.runAgentConsult as (params: { prompt: string }) => Promise<{ text: string }>
|
||||
)({ prompt: "Check the repository" });
|
||||
const consultInput = mockCallArg(mocks.consultRealtimeVoiceAgent) as Record<string, unknown>;
|
||||
expect(consultInput.senderIsOwner).toBe(false);
|
||||
expect(consultInput).not.toHaveProperty("toolsAllow");
|
||||
expect(createInput).not.toHaveProperty("tools");
|
||||
expectRespondOk(respond, { provider: "openai", transport: "webrtc" });
|
||||
});
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
import { normalizeTalkSection } from "../config/talk.js";
|
||||
import { buildRealtimeVoiceAgentConsultChatMessage } from "../talk/agent-consult-tool.js";
|
||||
import { abortChatRunById } from "./chat-abort.js";
|
||||
import { handleChatSend } from "./server-methods/chat-send-handler.js";
|
||||
import {
|
||||
handleChatSend,
|
||||
handleChatSendWithRuntimeTools,
|
||||
} from "./server-methods/chat-send-handler.js";
|
||||
import type {
|
||||
GatewayClient,
|
||||
GatewayRequestContext,
|
||||
GatewayRequestHandlerOptions,
|
||||
} from "./server-methods/shared-types.js";
|
||||
import { resolveTalkAgentConsultAuthority } from "./talk-client-gateway-control.js";
|
||||
import { registerTalkRealtimeRelayAgentRun } from "./talk-realtime-relay.js";
|
||||
import { formatForLog } from "./ws-log.js";
|
||||
|
||||
@@ -78,12 +82,13 @@ export async function startTalkRealtimeAgentConsult(params: {
|
||||
}
|
||||
const idempotencyKey = `talk-${params.callId}-${randomUUID()}`;
|
||||
const normalizedTalk = normalizeTalkSection(params.context.getRuntimeConfig().talk);
|
||||
const authority = resolveTalkAgentConsultAuthority(params.client?.connect?.scopes);
|
||||
let acknowledgedRunId: string | undefined;
|
||||
const chatResponse = await new Promise<
|
||||
{ ok: true; result: unknown } | { ok: false; error: ErrorShape } | undefined
|
||||
>((resolve) => {
|
||||
let acknowledged = false;
|
||||
const chatSendResult = handleChatSend({
|
||||
const chatSendOptions = {
|
||||
req: {
|
||||
type: "req",
|
||||
id: `${params.requestId}:talk-tool-call`,
|
||||
@@ -146,7 +151,13 @@ export async function startTalkRealtimeAgentConsult(params: {
|
||||
},
|
||||
);
|
||||
},
|
||||
} as GatewayRequestHandlerOptions);
|
||||
} as GatewayRequestHandlerOptions;
|
||||
// talk.client.toolCall enters below the normal chat.send scope gate, so its
|
||||
// delegated run must carry the Talk caller's already-resolved tool boundary.
|
||||
const chatSendResult =
|
||||
authority.toolsAllow !== undefined
|
||||
? handleChatSendWithRuntimeTools(chatSendOptions, authority.toolsAllow)
|
||||
: handleChatSend(chatSendOptions);
|
||||
void Promise.resolve(chatSendResult).then(
|
||||
() => {
|
||||
if (!acknowledged) {
|
||||
|
||||
@@ -36,7 +36,10 @@ vi.mock("../talk/agent-consult-runtime.js", () => ({
|
||||
consultRealtimeVoiceAgent: mocks.consultRealtimeVoiceAgent,
|
||||
}));
|
||||
|
||||
import { createTalkClientAgentConsultRunner } from "./talk-client-gateway-control.js";
|
||||
import {
|
||||
createTalkClientAgentConsultRunner,
|
||||
type TalkAgentConsultAuthority,
|
||||
} from "./talk-client-gateway-control.js";
|
||||
|
||||
const config = {} as OpenClawConfig;
|
||||
const coreParams = {
|
||||
@@ -54,12 +57,16 @@ const coreParams = {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
} as Parameters<PluginRuntime["agent"]["runEmbeddedAgent"]>[0];
|
||||
|
||||
function createRunner(registerRun = vi.fn()) {
|
||||
function createRunner(
|
||||
registerRun = vi.fn(),
|
||||
authority: TalkAgentConsultAuthority = { senderIsOwner: false, toolsAllow: ["read"] },
|
||||
) {
|
||||
return createTalkClientAgentConsultRunner({
|
||||
config,
|
||||
context: { chatAbortControllers: new Map(), logGateway: { warn: vi.fn() } } as never,
|
||||
agentId: "researcher",
|
||||
sessionKey: "agent:researcher:talk",
|
||||
authority,
|
||||
getVoiceSessionId: () => "voice-session",
|
||||
initialItems: [],
|
||||
registerRun,
|
||||
@@ -107,9 +114,23 @@ describe("Talk client agent consult admission", () => {
|
||||
preparedRunAdmission: expect.objectContaining({ close: mocks.close }),
|
||||
}),
|
||||
);
|
||||
expect(mocks.consultRealtimeVoiceAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ senderIsOwner: false, toolsAllow: ["read"] }),
|
||||
);
|
||||
expect(mocks.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves full agent authority for administrator consults", async () => {
|
||||
await expect(
|
||||
createRunner(vi.fn(), { senderIsOwner: true }).runPrompt({ prompt: "check" }),
|
||||
).resolves.toEqual({ text: "done" });
|
||||
|
||||
expect(mocks.consultRealtimeVoiceAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ senderIsOwner: true }),
|
||||
);
|
||||
expect(mocks.consultRealtimeVoiceAgent.mock.calls[0]?.[0]).not.toHaveProperty("toolsAllow");
|
||||
});
|
||||
|
||||
it("closes the Talk admission when core execution fails", async () => {
|
||||
mocks.runEmbeddedAgentCore.mockRejectedValueOnce(new Error("core failed"));
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { consultRealtimeVoiceAgent } from "../talk/agent-consult-runtime.js";
|
||||
import {
|
||||
REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
|
||||
parseRealtimeVoiceAgentConsultArgs,
|
||||
resolveRealtimeVoiceAgentConsultToolsAllow,
|
||||
} from "../talk/agent-consult-tool.js";
|
||||
import {
|
||||
buildRealtimeVoiceAgentCancelProviderResult,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
} from "../talk/realtime-session-harness.js";
|
||||
import type { TalkEvent } from "../talk/talk-events.js";
|
||||
import { registerChatAbortController } from "./chat-abort.js";
|
||||
import { ADMIN_SCOPE, WRITE_SCOPE } from "./operator-scopes.js";
|
||||
import type { GatewayRequestContext } from "./server-methods/shared-types.js";
|
||||
import { formatError } from "./server-utils.js";
|
||||
import { registerTalkConnectionCleanup } from "./talk-session-registry.js";
|
||||
@@ -54,6 +56,25 @@ const owners = new Map<string, GatewayControlOwner>();
|
||||
|
||||
const REALTIME_VOICE_CONTEXT_MAX_UTF8_BYTES = 8_000;
|
||||
const REALTIME_CONTROL_MAX_PENDING = 8;
|
||||
|
||||
export type TalkAgentConsultAuthority = {
|
||||
senderIsOwner: boolean;
|
||||
toolsAllow?: string[];
|
||||
};
|
||||
|
||||
export function resolveTalkAgentConsultAuthority(
|
||||
scopes: readonly string[] | undefined,
|
||||
): TalkAgentConsultAuthority {
|
||||
const senderIsOwner = scopes?.includes(ADMIN_SCOPE) === true;
|
||||
if (senderIsOwner || scopes?.includes(WRITE_SCOPE) === true) {
|
||||
return { senderIsOwner };
|
||||
}
|
||||
return {
|
||||
senderIsOwner: false,
|
||||
toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow("safe-read-only"),
|
||||
};
|
||||
}
|
||||
|
||||
const loadTalkAgentExecution = createLazyRuntimeModule(async () => {
|
||||
const [embeddedAgent, admission] = await Promise.all([
|
||||
import("../agents/embedded-agent.js"),
|
||||
@@ -213,12 +234,14 @@ export function createTalkClientAgentConsultRunner(params: {
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
ownerConnId?: string;
|
||||
authority?: TalkAgentConsultAuthority;
|
||||
getVoiceSessionId: () => string | undefined;
|
||||
initialItems: Array<{ role: "user" | "assistant"; text: string }>;
|
||||
runIdPrefix?: string;
|
||||
surface?: string;
|
||||
registerRun?: (params: { runId: string }) => void;
|
||||
}) {
|
||||
const authority = params.authority ?? resolveTalkAgentConsultAuthority(undefined);
|
||||
let agentRuntime: ReturnType<typeof createPluginRuntime>["agent"] | undefined;
|
||||
const runArgs = async (args: unknown, signal?: AbortSignal) => {
|
||||
const parsedArgs = parseRealtimeVoiceAgentConsultArgs(args);
|
||||
@@ -255,6 +278,7 @@ export function createTalkClientAgentConsultRunner(params: {
|
||||
questionSourceLabel: "user",
|
||||
thinkLevel: talkConfig?.consultThinkingLevel,
|
||||
fastMode: talkConfig?.consultFastMode,
|
||||
...authority,
|
||||
abortSignal: signal,
|
||||
onRunStarted: ({ runId, sessionId, timeoutMs }) => {
|
||||
if (params.registerRun) {
|
||||
|
||||
@@ -135,6 +135,7 @@ export function createTalkRealtimeRelaySession(
|
||||
),
|
||||
sessionKey: relaySessionKey,
|
||||
ownerConnId: params.connId,
|
||||
authority: params.consultAuthority,
|
||||
getVoiceSessionId: () => relaySessionId,
|
||||
initialItems: [],
|
||||
runIdPrefix: "talk-realtime-relay-consult",
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { RealtimeVoiceSessionHarness } from "../talk/realtime-session-harne
|
||||
import type { RealtimeVoiceBridgeSession } from "../talk/session-runtime.js";
|
||||
import type { TalkEvent } from "../talk/talk-session-controller.js";
|
||||
import type { GatewayRequestContext } from "./server-methods/shared-types.js";
|
||||
import type { TalkAgentConsultAuthority } from "./talk-client-gateway-control.js";
|
||||
import type { RelayToolCallLedger } from "./talk-realtime-relay-tool-call-ledger.js";
|
||||
|
||||
export const RELAY_SESSION_TTL_MS = 30 * 60 * 1000;
|
||||
@@ -122,6 +123,7 @@ export type CreateTalkRealtimeRelaySessionParams = {
|
||||
context: GatewayRequestContext;
|
||||
connId: string;
|
||||
cfg?: OpenClawConfig;
|
||||
consultAuthority?: TalkAgentConsultAuthority;
|
||||
provider: RealtimeVoiceProviderPlugin;
|
||||
providerConfig: RealtimeVoiceProviderConfig;
|
||||
instructions: string;
|
||||
|
||||
Reference in New Issue
Block a user