mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:00:54 +00:00
fix: resolve voice-call SecretRef inputs (#73632)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -111,6 +111,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Voice Call: resolve SecretRef-backed Twilio auth tokens and realtime/streaming provider API keys before initializing call providers, so SecretRef-backed voice-call credentials reach runtime as strings. (#73632) Thanks @VACInc.
|
||||||
- Security/outbound: strip re-formed HTML tags during plain-text sanitization so nested tag fragments cannot leave a CodeQL-detected `<script>` sequence behind. Thanks @vincentkoc.
|
- Security/outbound: strip re-formed HTML tags during plain-text sanitization so nested tag fragments cannot leave a CodeQL-detected `<script>` sequence behind. Thanks @vincentkoc.
|
||||||
- Security/secrets: compare credential bytes with padded timing-safe buffers instead of hashing candidate passwords before equality checks. Thanks @vincentkoc.
|
- Security/secrets: compare credential bytes with padded timing-safe buffers instead of hashing candidate passwords before equality checks. Thanks @vincentkoc.
|
||||||
- Security/QQBot: sanitize debug log arguments before writing to `console.*`, so gateway payload fields cannot forge extra log lines when debug logging is enabled. Thanks @vincentkoc.
|
- Security/QQBot: sanitize debug log arguments before writing to `console.*`, so gateway payload fields cannot forge extra log lines when debug logging is enabled. Thanks @vincentkoc.
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ skips starting the runtime. Commands, RPC calls, and agent tools still
|
|||||||
return the exact missing provider configuration when used.
|
return the exact missing provider configuration when used.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Voice-call credentials accept SecretRefs. `plugins.entries.voice-call.config.twilio.authToken` and `plugins.entries.voice-call.config.tts.providers.*.apiKey` resolve through the standard SecretRef surface; see [SecretRef credential surface](/reference/secretref-credential-surface).
|
Voice-call credentials accept SecretRefs. `plugins.entries.voice-call.config.twilio.authToken`, `plugins.entries.voice-call.config.realtime.providers.*.apiKey`, `plugins.entries.voice-call.config.streaming.providers.*.apiKey`, and `plugins.entries.voice-call.config.tts.providers.*.apiKey` resolve through the standard SecretRef surface; see [SecretRef credential surface](/reference/secretref-credential-surface).
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ Scope intent:
|
|||||||
- `plugins.entries.firecrawl.config.webSearch.apiKey`
|
- `plugins.entries.firecrawl.config.webSearch.apiKey`
|
||||||
- `plugins.entries.minimax.config.webSearch.apiKey`
|
- `plugins.entries.minimax.config.webSearch.apiKey`
|
||||||
- `plugins.entries.tavily.config.webSearch.apiKey`
|
- `plugins.entries.tavily.config.webSearch.apiKey`
|
||||||
|
- `plugins.entries.voice-call.config.realtime.providers.*.apiKey`
|
||||||
|
- `plugins.entries.voice-call.config.streaming.providers.*.apiKey`
|
||||||
- `plugins.entries.voice-call.config.tts.providers.*.apiKey`
|
- `plugins.entries.voice-call.config.tts.providers.*.apiKey`
|
||||||
- `plugins.entries.voice-call.config.twilio.authToken`
|
- `plugins.entries.voice-call.config.twilio.authToken`
|
||||||
- `tools.web.search.apiKey`
|
- `tools.web.search.apiKey`
|
||||||
|
|||||||
@@ -589,6 +589,20 @@
|
|||||||
"secretShape": "secret_input",
|
"secretShape": "secret_input",
|
||||||
"optIn": true
|
"optIn": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins.entries.voice-call.config.realtime.providers.*.apiKey",
|
||||||
|
"configFile": "openclaw.json",
|
||||||
|
"path": "plugins.entries.voice-call.config.realtime.providers.*.apiKey",
|
||||||
|
"secretShape": "secret_input",
|
||||||
|
"optIn": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins.entries.voice-call.config.streaming.providers.*.apiKey",
|
||||||
|
"configFile": "openclaw.json",
|
||||||
|
"path": "plugins.entries.voice-call.config.streaming.providers.*.apiKey",
|
||||||
|
"secretShape": "secret_input",
|
||||||
|
"optIn": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
|
"id": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
|
||||||
"configFile": "openclaw.json",
|
"configFile": "openclaw.json",
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ function setup(config: Record<string, unknown>): Registered {
|
|||||||
return { methods, tools, service };
|
return { methods, tools, service };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envRef(id: string) {
|
||||||
|
return { source: "env" as const, provider: "default", id };
|
||||||
|
}
|
||||||
|
|
||||||
async function registerVoiceCallCli(
|
async function registerVoiceCallCli(
|
||||||
program: Command,
|
program: Command,
|
||||||
pluginConfig: Record<string, unknown> = { provider: "mock" },
|
pluginConfig: Record<string, unknown> = { provider: "mock" },
|
||||||
@@ -275,6 +279,26 @@ describe("voice-call plugin", () => {
|
|||||||
expect(noopLogger.warn).toHaveBeenCalledWith(expect.stringContaining("TWILIO_ACCOUNT_SID"));
|
expect(noopLogger.warn).toHaveBeenCalledWith(expect.stringContaining("TWILIO_ACCOUNT_SID"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("registers Twilio configs with SecretRef auth tokens", async () => {
|
||||||
|
const authToken = envRef("TWILIO_AUTH_TOKEN");
|
||||||
|
const { service } = setup({
|
||||||
|
enabled: true,
|
||||||
|
provider: "twilio",
|
||||||
|
fromNumber: "+15550001234",
|
||||||
|
twilio: {
|
||||||
|
accountSid: "AC123",
|
||||||
|
authToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await service?.start(createServiceContext());
|
||||||
|
|
||||||
|
expect(createVoiceCallRuntime).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vi.mocked(createVoiceCallRuntime).mock.calls[0]?.[0]?.config.twilio?.authToken).toEqual(
|
||||||
|
authToken,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("still reports missing provider setup when a command needs the runtime", async () => {
|
it("still reports missing provider setup when a command needs the runtime", async () => {
|
||||||
vi.stubEnv("TWILIO_ACCOUNT_SID", "");
|
vi.stubEnv("TWILIO_ACCOUNT_SID", "");
|
||||||
vi.stubEnv("TWILIO_AUTH_TOKEN", "");
|
vi.stubEnv("TWILIO_AUTH_TOKEN", "");
|
||||||
|
|||||||
@@ -727,6 +727,8 @@
|
|||||||
"secretInputs": {
|
"secretInputs": {
|
||||||
"paths": [
|
"paths": [
|
||||||
{ "path": "twilio.authToken", "expected": "string" },
|
{ "path": "twilio.authToken", "expected": "string" },
|
||||||
|
{ "path": "realtime.providers.*.apiKey", "expected": "string" },
|
||||||
|
{ "path": "streaming.providers.*.apiKey", "expected": "string" },
|
||||||
{ "path": "tts.providers.*.apiKey", "expected": "string" }
|
{ "path": "tts.providers.*.apiKey", "expected": "string" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
VoiceCallConfigSchema,
|
||||||
|
resolveTwilioAuthToken,
|
||||||
validateProviderConfig,
|
validateProviderConfig,
|
||||||
normalizeVoiceCallConfig,
|
normalizeVoiceCallConfig,
|
||||||
resolveVoiceCallConfig,
|
resolveVoiceCallConfig,
|
||||||
@@ -11,6 +13,10 @@ function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): Voi
|
|||||||
return createVoiceCallBaseConfig({ provider });
|
return createVoiceCallBaseConfig({ provider });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envRef(id: string) {
|
||||||
|
return { source: "env" as const, provider: "default", id };
|
||||||
|
}
|
||||||
|
|
||||||
function requireElevenLabsTtsConfig(config: Pick<VoiceCallConfig, "tts">) {
|
function requireElevenLabsTtsConfig(config: Pick<VoiceCallConfig, "tts">) {
|
||||||
const tts = config.tts;
|
const tts = config.tts;
|
||||||
const elevenlabs = tts?.providers?.elevenlabs;
|
const elevenlabs = tts?.providers?.elevenlabs;
|
||||||
@@ -80,6 +86,24 @@ describe("validateProviderConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("twilio provider", () => {
|
describe("twilio provider", () => {
|
||||||
|
it("accepts SecretRef-backed auth tokens before runtime resolution", () => {
|
||||||
|
const config = VoiceCallConfigSchema.parse({
|
||||||
|
enabled: true,
|
||||||
|
provider: "twilio",
|
||||||
|
fromNumber: "+15550001234",
|
||||||
|
twilio: {
|
||||||
|
accountSid: "AC123",
|
||||||
|
authToken: envRef("TWILIO_AUTH_TOKEN"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.twilio?.authToken).toEqual(envRef("TWILIO_AUTH_TOKEN"));
|
||||||
|
expect(validateProviderConfig(config)).toMatchObject({ valid: true, errors: [] });
|
||||||
|
expect(() => resolveTwilioAuthToken(config)).toThrow(
|
||||||
|
'plugins.entries.voice-call.config.twilio.authToken: unresolved SecretRef "env:default:TWILIO_AUTH_TOKEN"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("passes validation with mixed config and env vars", () => {
|
it("passes validation with mixed config and env vars", () => {
|
||||||
process.env.TWILIO_AUTH_TOKEN = "secret";
|
process.env.TWILIO_AUTH_TOKEN = "secret";
|
||||||
let config = createBaseConfig("twilio");
|
let config = createBaseConfig("twilio");
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import {
|
|||||||
REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES,
|
REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES,
|
||||||
type RealtimeVoiceAgentConsultToolPolicy,
|
type RealtimeVoiceAgentConsultToolPolicy,
|
||||||
} from "openclaw/plugin-sdk/realtime-voice";
|
} from "openclaw/plugin-sdk/realtime-voice";
|
||||||
|
import {
|
||||||
|
buildSecretInputSchema,
|
||||||
|
hasConfiguredSecretInput,
|
||||||
|
normalizeResolvedSecretInputString,
|
||||||
|
type SecretInput,
|
||||||
|
} from "openclaw/plugin-sdk/secret-input";
|
||||||
import { z } from "openclaw/plugin-sdk/zod";
|
import { z } from "openclaw/plugin-sdk/zod";
|
||||||
import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema } from "../api.js";
|
import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema } from "../api.js";
|
||||||
import { deepMergeDefined } from "./deep-merge.js";
|
import { deepMergeDefined } from "./deep-merge.js";
|
||||||
@@ -39,6 +45,8 @@ export type InboundPolicy = z.infer<typeof InboundPolicySchema>;
|
|||||||
// Provider-Specific Configuration
|
// Provider-Specific Configuration
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SecretInputSchema = buildSecretInputSchema();
|
||||||
|
|
||||||
export const TelnyxConfigSchema = z
|
export const TelnyxConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
/** Telnyx API v2 key */
|
/** Telnyx API v2 key */
|
||||||
@@ -56,10 +64,12 @@ export const TwilioConfigSchema = z
|
|||||||
/** Twilio Account SID */
|
/** Twilio Account SID */
|
||||||
accountSid: z.string().min(1).optional(),
|
accountSid: z.string().min(1).optional(),
|
||||||
/** Twilio Auth Token */
|
/** Twilio Auth Token */
|
||||||
authToken: z.string().min(1).optional(),
|
authToken: SecretInputSchema.optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
|
export type TwilioConfig = Omit<z.infer<typeof TwilioConfigSchema>, "authToken"> & {
|
||||||
|
authToken?: SecretInput;
|
||||||
|
};
|
||||||
|
|
||||||
export const PlivoConfigSchema = z
|
export const PlivoConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -393,13 +403,15 @@ export const VoiceCallConfigSchema = z
|
|||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
|
export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
|
||||||
type DeepPartial<T> =
|
type DeepPartial<T> = T extends SecretInput
|
||||||
T extends Array<infer U>
|
? T
|
||||||
|
: T extends Array<infer U>
|
||||||
? DeepPartial<U>[]
|
? DeepPartial<U>[]
|
||||||
: T extends object
|
: T extends object
|
||||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
: T;
|
: T;
|
||||||
export type VoiceCallConfigInput = DeepPartial<VoiceCallConfig>;
|
export type VoiceCallConfigInput = DeepPartial<VoiceCallConfig>;
|
||||||
|
const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Configuration Helpers
|
// Configuration Helpers
|
||||||
@@ -458,6 +470,15 @@ function sanitizeVoiceCallProviderConfigs(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveTwilioAuthToken(
|
||||||
|
config: Pick<VoiceCallConfig, "twilio">,
|
||||||
|
): string | undefined {
|
||||||
|
return normalizeResolvedSecretInputString({
|
||||||
|
value: config.twilio?.authToken,
|
||||||
|
path: TWILIO_AUTH_TOKEN_PATH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallConfig {
|
export function normalizeVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallConfig {
|
||||||
const defaults = cloneDefaultVoiceCallConfig();
|
const defaults = cloneDefaultVoiceCallConfig();
|
||||||
const serve = { ...defaults.serve, ...config.serve };
|
const serve = { ...defaults.serve, ...config.serve };
|
||||||
@@ -608,7 +629,7 @@ export function validateProviderConfig(config: VoiceCallConfig): {
|
|||||||
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!config.twilio?.authToken) {
|
if (!hasConfiguredSecretInput(config.twilio?.authToken)) {
|
||||||
errors.push(
|
errors.push(
|
||||||
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import crypto from "node:crypto";
|
|||||||
import { setTimeout as sleep } from "node:timers/promises";
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
||||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||||
import type { TwilioConfig } from "../config.js";
|
|
||||||
import { getHeader } from "../http-headers.js";
|
import { getHeader } from "../http-headers.js";
|
||||||
import type { MediaStreamHandler } from "../media-stream.js";
|
import type { MediaStreamHandler } from "../media-stream.js";
|
||||||
import { chunkAudio } from "../telephony-audio.js";
|
import { chunkAudio } from "../telephony-audio.js";
|
||||||
@@ -69,6 +68,11 @@ type StreamSendResult = {
|
|||||||
sent: boolean;
|
sent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TwilioProviderConfig = {
|
||||||
|
accountSid?: string;
|
||||||
|
authToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class TwilioProvider implements VoiceCallProvider {
|
export class TwilioProvider implements VoiceCallProvider {
|
||||||
readonly name = "twilio" as const;
|
readonly name = "twilio" as const;
|
||||||
|
|
||||||
@@ -129,7 +133,7 @@ export class TwilioProvider implements VoiceCallProvider {
|
|||||||
this.streamAuthTokens.delete(providerCallId);
|
this.streamAuthTokens.delete(providerCallId);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config: TwilioConfig, options: TwilioProviderOptions = {}) {
|
constructor(config: TwilioProviderConfig, options: TwilioProviderOptions = {}) {
|
||||||
if (!config.accountSid) {
|
if (!config.accountSid) {
|
||||||
throw new Error("Twilio Account SID is required");
|
throw new Error("Twilio Account SID is required");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createVoiceCallBaseConfig } from "./test-fixtures.js";
|
|||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
resolveVoiceCallConfig: vi.fn(),
|
resolveVoiceCallConfig: vi.fn(),
|
||||||
|
resolveTwilioAuthToken: vi.fn(),
|
||||||
validateProviderConfig: vi.fn(),
|
validateProviderConfig: vi.fn(),
|
||||||
managerInitialize: vi.fn(),
|
managerInitialize: vi.fn(),
|
||||||
managerGetCall: vi.fn(),
|
managerGetCall: vi.fn(),
|
||||||
@@ -26,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
|
|
||||||
vi.mock("./config.js", () => ({
|
vi.mock("./config.js", () => ({
|
||||||
resolveVoiceCallConfig: mocks.resolveVoiceCallConfig,
|
resolveVoiceCallConfig: mocks.resolveVoiceCallConfig,
|
||||||
|
resolveTwilioAuthToken: mocks.resolveTwilioAuthToken,
|
||||||
validateProviderConfig: mocks.validateProviderConfig,
|
validateProviderConfig: mocks.validateProviderConfig,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -109,6 +111,9 @@ describe("createVoiceCallRuntime lifecycle", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.resolveVoiceCallConfig.mockImplementation((cfg: VoiceCallConfig) => cfg);
|
mocks.resolveVoiceCallConfig.mockImplementation((cfg: VoiceCallConfig) => cfg);
|
||||||
|
mocks.resolveTwilioAuthToken.mockImplementation(
|
||||||
|
(cfg: VoiceCallConfig) => cfg.twilio?.authToken,
|
||||||
|
);
|
||||||
mocks.validateProviderConfig.mockReturnValue({ valid: true, errors: [] });
|
mocks.validateProviderConfig.mockReturnValue({ valid: true, errors: [] });
|
||||||
mocks.managerInitialize.mockResolvedValue(undefined);
|
mocks.managerInitialize.mockResolvedValue(undefined);
|
||||||
mocks.managerGetCall.mockReset();
|
mocks.managerGetCall.mockReset();
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
type ResolvedRealtimeVoiceProvider,
|
type ResolvedRealtimeVoiceProvider,
|
||||||
} from "openclaw/plugin-sdk/realtime-voice";
|
} from "openclaw/plugin-sdk/realtime-voice";
|
||||||
import type { VoiceCallConfig } from "./config.js";
|
import type { VoiceCallConfig } from "./config.js";
|
||||||
import { resolveVoiceCallConfig, validateProviderConfig } from "./config.js";
|
import {
|
||||||
|
resolveTwilioAuthToken,
|
||||||
|
resolveVoiceCallConfig,
|
||||||
|
validateProviderConfig,
|
||||||
|
} from "./config.js";
|
||||||
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
||||||
import { CallManager } from "./manager.js";
|
import { CallManager } from "./manager.js";
|
||||||
import type { VoiceCallProvider } from "./providers/base.js";
|
import type { VoiceCallProvider } from "./providers/base.js";
|
||||||
@@ -195,7 +199,7 @@ async function resolveProvider(config: VoiceCallConfig): Promise<VoiceCallProvid
|
|||||||
return new TwilioProvider(
|
return new TwilioProvider(
|
||||||
{
|
{
|
||||||
accountSid: config.twilio?.accountSid,
|
accountSid: config.twilio?.accountSid,
|
||||||
authToken: config.twilio?.authToken,
|
authToken: resolveTwilioAuthToken(config),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
allowNgrokFreeTierLoopbackBypass,
|
allowNgrokFreeTierLoopbackBypass,
|
||||||
|
|||||||
@@ -147,6 +147,60 @@ describe("resolvePluginConfigContractsById", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refreshes stale bundled SecretInput contracts from bundled metadata", () => {
|
||||||
|
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
||||||
|
createRegistry([
|
||||||
|
createPluginRecord({
|
||||||
|
id: "voice-call",
|
||||||
|
origin: "bundled",
|
||||||
|
configContracts: {
|
||||||
|
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
|
||||||
|
secretInputs: {
|
||||||
|
paths: [{ path: "twilio.authToken", expected: "string" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocks.findBundledPluginMetadataById.mockReturnValue({
|
||||||
|
manifest: {
|
||||||
|
configContracts: {
|
||||||
|
secretInputs: {
|
||||||
|
paths: [
|
||||||
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "realtime.providers.*.apiKey", expected: "string" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePluginConfigContractsById({
|
||||||
|
pluginIds: ["voice-call"],
|
||||||
|
fallbackToBundledMetadataForResolvedBundled: true,
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"voice-call",
|
||||||
|
{
|
||||||
|
origin: "bundled",
|
||||||
|
configContracts: {
|
||||||
|
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
|
||||||
|
secretInputs: {
|
||||||
|
paths: [
|
||||||
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "realtime.providers.*.apiKey", expected: "string" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("can hydrate missing contracts for plugin ids known to be bundled by runtime discovery", () => {
|
it("can hydrate missing contracts for plugin ids known to be bundled by runtime discovery", () => {
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
||||||
createRegistry([
|
createRegistry([
|
||||||
|
|||||||
@@ -143,18 +143,19 @@ export function resolvePluginConfigContractsById(params: {
|
|||||||
const existing = matches.get(pluginId);
|
const existing = matches.get(pluginId);
|
||||||
const shouldHydrateBundledMatch =
|
const shouldHydrateBundledMatch =
|
||||||
existing &&
|
existing &&
|
||||||
!existing.configContracts.secretInputs &&
|
|
||||||
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
|
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
|
||||||
fallbackBundledPluginIds.has(pluginId));
|
fallbackBundledPluginIds.has(pluginId));
|
||||||
if (shouldHydrateBundledMatch) {
|
if (shouldHydrateBundledMatch) {
|
||||||
const bundled = findBundledPluginMetadataById(pluginId);
|
const bundled = findBundledPluginMetadataById(pluginId);
|
||||||
if (bundled?.manifest.configContracts?.secretInputs) {
|
if (bundled?.manifest.configContracts) {
|
||||||
matches.set(pluginId, {
|
matches.set(pluginId, {
|
||||||
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
|
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
|
||||||
configContracts: {
|
configContracts: {
|
||||||
...bundled.manifest.configContracts,
|
...bundled.manifest.configContracts,
|
||||||
...existing.configContracts,
|
...existing.configContracts,
|
||||||
secretInputs: bundled.manifest.configContracts.secretInputs,
|
...(bundled.manifest.configContracts.secretInputs
|
||||||
|
? { secretInputs: bundled.manifest.configContracts.secretInputs }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ describe("collectPluginConfigAssignments bundled plugin manifests", () => {
|
|||||||
findBundledPluginMetadataById("voice-call")?.manifest.configContracts?.secretInputs?.paths,
|
findBundledPluginMetadataById("voice-call")?.manifest.configContracts?.secretInputs?.paths,
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{ path: "twilio.authToken", expected: "string" },
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "realtime.providers.*.apiKey", expected: "string" },
|
||||||
|
{ path: "streaming.providers.*.apiKey", expected: "string" },
|
||||||
{ path: "tts.providers.*.apiKey", expected: "string" },
|
{ path: "tts.providers.*.apiKey", expected: "string" },
|
||||||
]);
|
]);
|
||||||
const config = {
|
const config = {
|
||||||
@@ -27,6 +29,20 @@ describe("collectPluginConfigAssignments bundled plugin manifests", () => {
|
|||||||
twilio: {
|
twilio: {
|
||||||
authToken: envRef("TWILIO_AUTH_TOKEN"),
|
authToken: envRef("TWILIO_AUTH_TOKEN"),
|
||||||
},
|
},
|
||||||
|
realtime: {
|
||||||
|
providers: {
|
||||||
|
google: {
|
||||||
|
apiKey: envRef("GEMINI_API_KEY"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streaming: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
apiKey: envRef("OPENAI_API_KEY"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
tts: {
|
tts: {
|
||||||
providers: {
|
providers: {
|
||||||
openai: {
|
openai: {
|
||||||
@@ -54,6 +70,8 @@ describe("collectPluginConfigAssignments bundled plugin manifests", () => {
|
|||||||
}).get("voice-call")?.configContracts.secretInputs?.paths,
|
}).get("voice-call")?.configContracts.secretInputs?.paths,
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{ path: "twilio.authToken", expected: "string" },
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "realtime.providers.*.apiKey", expected: "string" },
|
||||||
|
{ path: "streaming.providers.*.apiKey", expected: "string" },
|
||||||
{ path: "tts.providers.*.apiKey", expected: "string" },
|
{ path: "tts.providers.*.apiKey", expected: "string" },
|
||||||
]);
|
]);
|
||||||
const context = createResolverContext({
|
const context = createResolverContext({
|
||||||
@@ -73,6 +91,8 @@ describe("collectPluginConfigAssignments bundled plugin manifests", () => {
|
|||||||
warnings: context.warnings,
|
warnings: context.warnings,
|
||||||
}).toEqual({
|
}).toEqual({
|
||||||
assignments: [
|
assignments: [
|
||||||
|
"plugins.entries.voice-call.config.realtime.providers.google.apiKey",
|
||||||
|
"plugins.entries.voice-call.config.streaming.providers.openai.apiKey",
|
||||||
"plugins.entries.voice-call.config.tts.providers.elevenlabs.apiKey",
|
"plugins.entries.voice-call.config.tts.providers.elevenlabs.apiKey",
|
||||||
"plugins.entries.voice-call.config.tts.providers.openai.apiKey",
|
"plugins.entries.voice-call.config.tts.providers.openai.apiKey",
|
||||||
"plugins.entries.voice-call.config.twilio.authToken",
|
"plugins.entries.voice-call.config.twilio.authToken",
|
||||||
|
|||||||
Reference in New Issue
Block a user