mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
Support SecretRef for voice-call credentials and bundled plugin SecretInputs (#72607)
* fix: support voice-call secretrefs * test: classify plugin secretref targets * docs: credit voice-call secretref change * fix: keep plugin secret target discovery lightweight
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
|
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
|
||||||
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
|
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
|
||||||
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
|
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
|
||||||
|
- Voice Call: allow SecretRef-backed Twilio auth tokens and call-specific OpenAI/ElevenLabs TTS API keys through the plugin config surface. Fixes #68690. Thanks @joshavant.
|
||||||
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
|
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
|
||||||
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
|
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
|
||||||
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.
|
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Scope intent:
|
|||||||
- `talk.providers.*.apiKey`
|
- `talk.providers.*.apiKey`
|
||||||
- `messages.tts.providers.*.apiKey`
|
- `messages.tts.providers.*.apiKey`
|
||||||
- `tools.web.fetch.firecrawl.apiKey`
|
- `tools.web.fetch.firecrawl.apiKey`
|
||||||
|
- `plugins.entries.acpx.config.mcpServers.*.env.*`
|
||||||
- `plugins.entries.brave.config.webSearch.apiKey`
|
- `plugins.entries.brave.config.webSearch.apiKey`
|
||||||
- `plugins.entries.exa.config.webSearch.apiKey`
|
- `plugins.entries.exa.config.webSearch.apiKey`
|
||||||
- `plugins.entries.google.config.webSearch.apiKey`
|
- `plugins.entries.google.config.webSearch.apiKey`
|
||||||
@@ -49,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.tts.providers.*.apiKey`
|
||||||
|
- `plugins.entries.voice-call.config.twilio.authToken`
|
||||||
- `tools.web.search.apiKey`
|
- `tools.web.search.apiKey`
|
||||||
- `gateway.auth.password`
|
- `gateway.auth.password`
|
||||||
- `gateway.auth.token`
|
- `gateway.auth.token`
|
||||||
|
|||||||
@@ -526,6 +526,13 @@
|
|||||||
"secretShape": "secret_input",
|
"secretShape": "secret_input",
|
||||||
"optIn": true
|
"optIn": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins.entries.acpx.config.mcpServers.*.env.*",
|
||||||
|
"configFile": "openclaw.json",
|
||||||
|
"path": "plugins.entries.acpx.config.mcpServers.*.env.*",
|
||||||
|
"secretShape": "secret_input",
|
||||||
|
"optIn": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "plugins.entries.brave.config.webSearch.apiKey",
|
"id": "plugins.entries.brave.config.webSearch.apiKey",
|
||||||
"configFile": "openclaw.json",
|
"configFile": "openclaw.json",
|
||||||
@@ -582,6 +589,20 @@
|
|||||||
"secretShape": "secret_input",
|
"secretShape": "secret_input",
|
||||||
"optIn": true
|
"optIn": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
|
||||||
|
"configFile": "openclaw.json",
|
||||||
|
"path": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
|
||||||
|
"secretShape": "secret_input",
|
||||||
|
"optIn": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins.entries.voice-call.config.twilio.authToken",
|
||||||
|
"configFile": "openclaw.json",
|
||||||
|
"path": "plugins.entries.voice-call.config.twilio.authToken",
|
||||||
|
"secretShape": "secret_input",
|
||||||
|
"optIn": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "plugins.entries.xai.config.webSearch.apiKey",
|
"id": "plugins.entries.xai.config.webSearch.apiKey",
|
||||||
"configFile": "openclaw.json",
|
"configFile": "openclaw.json",
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"authToken": {
|
"authToken": {
|
||||||
"type": "string"
|
"type": ["string", "object"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -521,7 +521,7 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"type": "string"
|
"type": ["string", "object"]
|
||||||
},
|
},
|
||||||
"baseUrl": {
|
"baseUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"type": "string"
|
"type": ["string", "object"]
|
||||||
},
|
},
|
||||||
"baseUrl": {
|
"baseUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
"type": "string"
|
"type": ["string", "object"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
@@ -718,6 +718,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"configContracts": {
|
"configContracts": {
|
||||||
"compatibilityMigrationPaths": ["plugins.entries.voice-call.config"]
|
"compatibilityMigrationPaths": ["plugins.entries.voice-call.config"],
|
||||||
|
"secretInputs": {
|
||||||
|
"paths": [
|
||||||
|
{ "path": "twilio.authToken", "expected": "string" },
|
||||||
|
{ "path": "tts.providers.*.apiKey", "expected": "string" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,6 +477,38 @@ describe("config plugin validation", () => {
|
|||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts voice-call SecretRef credentials declared by the plugin schema", async () => {
|
||||||
|
const res = validateInSuite({
|
||||||
|
agents: { list: [{ id: "pi" }] },
|
||||||
|
plugins: {
|
||||||
|
enabled: true,
|
||||||
|
load: { paths: [voiceCallSchemaPluginDir] },
|
||||||
|
entries: {
|
||||||
|
"voice-call-schema-fixture": {
|
||||||
|
config: {
|
||||||
|
provider: "twilio",
|
||||||
|
twilio: {
|
||||||
|
accountSid: "twilio-account-sid-placeholder",
|
||||||
|
authToken: { source: "env", provider: "default", id: "TWILIO_AUTH_TOKEN" },
|
||||||
|
},
|
||||||
|
tts: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||||
|
},
|
||||||
|
elevenlabs: {
|
||||||
|
apiKey: { source: "env", provider: "default", id: "ELEVENLABS_API_KEY" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects out-of-range voice-call OpenAI TTS speed values", async () => {
|
it("rejects out-of-range voice-call OpenAI TTS speed values", async () => {
|
||||||
const res = validateInSuite({
|
const res = validateInSuite({
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ vi.mock("./plugin-registry.js", () => ({
|
|||||||
|
|
||||||
import { resolvePluginConfigContractsById } from "./config-contracts.js";
|
import { resolvePluginConfigContractsById } from "./config-contracts.js";
|
||||||
|
|
||||||
|
type PluginManifestRecord = PluginManifestRegistry["plugins"][number];
|
||||||
|
|
||||||
function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManifestRegistry {
|
function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManifestRegistry {
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
@@ -33,6 +35,46 @@ function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManif
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPluginRecord(
|
||||||
|
overrides: Pick<PluginManifestRecord, "id" | "origin"> & Partial<PluginManifestRecord>,
|
||||||
|
): PluginManifestRecord {
|
||||||
|
return {
|
||||||
|
rootDir: `/tmp/${overrides.id}`,
|
||||||
|
manifestPath: `/tmp/${overrides.id}/openclaw.plugin.json`,
|
||||||
|
channelConfigs: undefined,
|
||||||
|
providerAuthEnvVars: undefined,
|
||||||
|
configUiHints: undefined,
|
||||||
|
configSchema: undefined,
|
||||||
|
configContracts: undefined,
|
||||||
|
contracts: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
version: undefined,
|
||||||
|
enabledByDefault: undefined,
|
||||||
|
autoEnableWhenConfiguredProviders: undefined,
|
||||||
|
legacyPluginIds: undefined,
|
||||||
|
format: undefined,
|
||||||
|
bundleFormat: undefined,
|
||||||
|
bundleCapabilities: undefined,
|
||||||
|
kind: undefined,
|
||||||
|
channels: [],
|
||||||
|
providers: [],
|
||||||
|
modelSupport: undefined,
|
||||||
|
cliBackends: [],
|
||||||
|
channelEnvVars: undefined,
|
||||||
|
providerAuthAliases: undefined,
|
||||||
|
providerAuthChoices: undefined,
|
||||||
|
skills: [],
|
||||||
|
settingsFiles: undefined,
|
||||||
|
hooks: [],
|
||||||
|
source: `/tmp/${overrides.id}/openclaw.plugin.json`,
|
||||||
|
setupSource: undefined,
|
||||||
|
startupDeferConfiguredChannelFullLoadUntilAfterListen: undefined,
|
||||||
|
channelCatalogMeta: undefined,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("resolvePluginConfigContractsById", () => {
|
describe("resolvePluginConfigContractsById", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.findBundledPluginMetadataById.mockReset();
|
mocks.findBundledPluginMetadataById.mockReset();
|
||||||
@@ -45,42 +87,10 @@ describe("resolvePluginConfigContractsById", () => {
|
|||||||
it("does not fall back to bundled metadata when registry already resolved a plugin without config contracts", () => {
|
it("does not fall back to bundled metadata when registry already resolved a plugin without config contracts", () => {
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
||||||
createRegistry([
|
createRegistry([
|
||||||
{
|
createPluginRecord({
|
||||||
id: "brave",
|
id: "brave",
|
||||||
origin: "bundled",
|
origin: "bundled",
|
||||||
rootDir: "/tmp/brave",
|
}),
|
||||||
manifestPath: "/tmp/brave/openclaw.plugin.json",
|
|
||||||
channelConfigs: undefined,
|
|
||||||
providerAuthEnvVars: undefined,
|
|
||||||
configUiHints: undefined,
|
|
||||||
configSchema: undefined,
|
|
||||||
configContracts: undefined,
|
|
||||||
contracts: undefined,
|
|
||||||
name: undefined,
|
|
||||||
description: undefined,
|
|
||||||
version: undefined,
|
|
||||||
enabledByDefault: undefined,
|
|
||||||
autoEnableWhenConfiguredProviders: undefined,
|
|
||||||
legacyPluginIds: undefined,
|
|
||||||
format: undefined,
|
|
||||||
bundleFormat: undefined,
|
|
||||||
bundleCapabilities: undefined,
|
|
||||||
kind: undefined,
|
|
||||||
channels: [],
|
|
||||||
providers: [],
|
|
||||||
modelSupport: undefined,
|
|
||||||
cliBackends: [],
|
|
||||||
channelEnvVars: undefined,
|
|
||||||
providerAuthAliases: undefined,
|
|
||||||
providerAuthChoices: undefined,
|
|
||||||
skills: [],
|
|
||||||
settingsFiles: undefined,
|
|
||||||
hooks: [],
|
|
||||||
source: "/tmp/brave/openclaw.plugin.json",
|
|
||||||
setupSource: undefined,
|
|
||||||
startupDeferConfiguredChannelFullLoadUntilAfterListen: undefined,
|
|
||||||
channelCatalogMeta: undefined,
|
|
||||||
},
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -92,6 +102,92 @@ describe("resolvePluginConfigContractsById", () => {
|
|||||||
expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled();
|
expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can hydrate missing contracts from bundled metadata for resolved bundled plugins", () => {
|
||||||
|
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
||||||
|
createRegistry([
|
||||||
|
createPluginRecord({
|
||||||
|
id: "voice-call",
|
||||||
|
origin: "bundled",
|
||||||
|
configContracts: {
|
||||||
|
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocks.findBundledPluginMetadataById.mockReturnValue({
|
||||||
|
manifest: {
|
||||||
|
configContracts: {
|
||||||
|
secretInputs: {
|
||||||
|
paths: [{ path: "twilio.authToken", 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" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can hydrate missing contracts for plugin ids known to be bundled by runtime discovery", () => {
|
||||||
|
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
|
||||||
|
createRegistry([
|
||||||
|
createPluginRecord({
|
||||||
|
id: "voice-call",
|
||||||
|
origin: "config",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocks.findBundledPluginMetadataById.mockReturnValue({
|
||||||
|
manifest: {
|
||||||
|
configContracts: {
|
||||||
|
secretInputs: {
|
||||||
|
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePluginConfigContractsById({
|
||||||
|
pluginIds: ["voice-call"],
|
||||||
|
fallbackBundledPluginIds: ["voice-call"],
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"voice-call",
|
||||||
|
{
|
||||||
|
origin: "bundled",
|
||||||
|
configContracts: {
|
||||||
|
secretInputs: {
|
||||||
|
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("can skip bundled metadata fallback for registry-scoped callers", () => {
|
it("can skip bundled metadata fallback for registry-scoped callers", () => {
|
||||||
expect(
|
expect(
|
||||||
resolvePluginConfigContractsById({
|
resolvePluginConfigContractsById({
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export function resolvePluginConfigContractsById(params: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
cache?: boolean;
|
cache?: boolean;
|
||||||
fallbackToBundledMetadata?: boolean;
|
fallbackToBundledMetadata?: boolean;
|
||||||
|
fallbackToBundledMetadataForResolvedBundled?: boolean;
|
||||||
|
fallbackBundledPluginIds?: readonly string[];
|
||||||
pluginIds: readonly string[];
|
pluginIds: readonly string[];
|
||||||
}): ReadonlyMap<string, PluginConfigContractMetadata> {
|
}): ReadonlyMap<string, PluginConfigContractMetadata> {
|
||||||
const matches = new Map<string, PluginConfigContractMetadata>();
|
const matches = new Map<string, PluginConfigContractMetadata>();
|
||||||
@@ -112,8 +114,11 @@ export function resolvePluginConfigContractsById(params: {
|
|||||||
if (pluginIds.length === 0) {
|
if (pluginIds.length === 0) {
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
const fallbackBundledPluginIds = new Set(
|
||||||
|
(params.fallbackBundledPluginIds ?? []).map((pluginId) => pluginId.trim()).filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
const resolvedPluginIds = new Set<string>();
|
const resolvedPluginOrigins = new Map<string, PluginOrigin>();
|
||||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
@@ -125,7 +130,7 @@ export function resolvePluginConfigContractsById(params: {
|
|||||||
if (!pluginIds.includes(plugin.id)) {
|
if (!pluginIds.includes(plugin.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
resolvedPluginIds.add(plugin.id);
|
resolvedPluginOrigins.set(plugin.id, plugin.origin);
|
||||||
if (!plugin.configContracts) {
|
if (!plugin.configContracts) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -137,7 +142,35 @@ export function resolvePluginConfigContractsById(params: {
|
|||||||
|
|
||||||
if (params.fallbackToBundledMetadata ?? true) {
|
if (params.fallbackToBundledMetadata ?? true) {
|
||||||
for (const pluginId of pluginIds) {
|
for (const pluginId of pluginIds) {
|
||||||
if (matches.has(pluginId) || resolvedPluginIds.has(pluginId)) {
|
const existing = matches.get(pluginId);
|
||||||
|
const shouldHydrateBundledMatch =
|
||||||
|
existing &&
|
||||||
|
!existing.configContracts.secretInputs &&
|
||||||
|
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
|
||||||
|
fallbackBundledPluginIds.has(pluginId));
|
||||||
|
if (shouldHydrateBundledMatch) {
|
||||||
|
const bundled = findBundledPluginMetadataById(pluginId);
|
||||||
|
if (bundled?.manifest.configContracts?.secretInputs) {
|
||||||
|
matches.set(pluginId, {
|
||||||
|
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
|
||||||
|
configContracts: {
|
||||||
|
...bundled.manifest.configContracts,
|
||||||
|
...existing.configContracts,
|
||||||
|
secretInputs: bundled.manifest.configContracts.secretInputs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches.has(pluginId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const resolvedOrigin = resolvedPluginOrigins.get(pluginId);
|
||||||
|
if (
|
||||||
|
resolvedOrigin &&
|
||||||
|
!(params.fallbackToBundledMetadataForResolvedBundled && resolvedOrigin === "bundled") &&
|
||||||
|
!fallbackBundledPluginIds.has(pluginId)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bundled = findBundledPluginMetadataById(pluginId);
|
const bundled = findBundledPluginMetadataById(pluginId);
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ describe("exec SecretRef id parity", () => {
|
|||||||
if (canonicalId.startsWith("tools.web.search.")) {
|
if (canonicalId.startsWith("tools.web.search.")) {
|
||||||
return "tools.web.search";
|
return "tools.web.search";
|
||||||
}
|
}
|
||||||
|
if (canonicalId.startsWith("plugins.entries.")) {
|
||||||
|
return "plugins.config";
|
||||||
|
}
|
||||||
return "unclassified";
|
return "unclassified";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { findBundledPluginMetadataById } from "../plugins/bundled-plugin-metadata.js";
|
||||||
|
import { resolvePluginConfigContractsById } from "../plugins/config-contracts.js";
|
||||||
|
import { collectPluginConfigAssignments } from "./runtime-config-collectors-plugins.js";
|
||||||
|
import { createResolverContext } from "./runtime-shared.js";
|
||||||
|
|
||||||
|
function envRef(id: string) {
|
||||||
|
return { source: "env" as const, provider: "default", id };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("collectPluginConfigAssignments bundled plugin manifests", () => {
|
||||||
|
it("collects voice-call SecretRef assignments from bundled manifest contracts", () => {
|
||||||
|
expect(
|
||||||
|
findBundledPluginMetadataById("voice-call")?.manifest.configContracts?.secretInputs?.paths,
|
||||||
|
).toEqual([
|
||||||
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "tts.providers.*.apiKey", expected: "string" },
|
||||||
|
]);
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
"voice-call": {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
twilio: {
|
||||||
|
authToken: envRef("TWILIO_AUTH_TOKEN"),
|
||||||
|
},
|
||||||
|
tts: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
apiKey: envRef("OPENAI_API_KEY"),
|
||||||
|
},
|
||||||
|
elevenlabs: {
|
||||||
|
apiKey: envRef("ELEVENLABS_API_KEY"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
expect(
|
||||||
|
resolvePluginConfigContractsById({
|
||||||
|
config,
|
||||||
|
workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)),
|
||||||
|
env: {},
|
||||||
|
cache: true,
|
||||||
|
fallbackToBundledMetadata: true,
|
||||||
|
fallbackToBundledMetadataForResolvedBundled: true,
|
||||||
|
pluginIds: ["voice-call"],
|
||||||
|
fallbackBundledPluginIds: ["voice-call"],
|
||||||
|
}).get("voice-call")?.configContracts.secretInputs?.paths,
|
||||||
|
).toEqual([
|
||||||
|
{ path: "twilio.authToken", expected: "string" },
|
||||||
|
{ path: "tts.providers.*.apiKey", expected: "string" },
|
||||||
|
]);
|
||||||
|
const context = createResolverContext({
|
||||||
|
sourceConfig: config,
|
||||||
|
env: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
collectPluginConfigAssignments({
|
||||||
|
config,
|
||||||
|
defaults: undefined,
|
||||||
|
context,
|
||||||
|
loadablePluginOrigins: new Map([["voice-call", "bundled"]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect({
|
||||||
|
assignments: context.assignments.map((assignment) => assignment.path).toSorted(),
|
||||||
|
warnings: context.warnings,
|
||||||
|
}).toEqual({
|
||||||
|
assignments: [
|
||||||
|
"plugins.entries.voice-call.config.tts.providers.elevenlabs.apiKey",
|
||||||
|
"plugins.entries.voice-call.config.tts.providers.openai.apiKey",
|
||||||
|
"plugins.entries.voice-call.config.twilio.authToken",
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,6 +40,9 @@ export function collectPluginConfigAssignments(params: {
|
|||||||
params.config,
|
params.config,
|
||||||
resolveDefaultAgentId(params.config),
|
resolveDefaultAgentId(params.config),
|
||||||
);
|
);
|
||||||
|
const bundledLoadablePluginIds = [...(params.loadablePluginOrigins?.entries() ?? [])]
|
||||||
|
.filter(([, origin]) => origin === "bundled")
|
||||||
|
.map(([pluginId]) => pluginId);
|
||||||
const pluginSecretInputs = new Map(
|
const pluginSecretInputs = new Map(
|
||||||
[
|
[
|
||||||
...resolvePluginConfigContractsById({
|
...resolvePluginConfigContractsById({
|
||||||
@@ -47,7 +50,9 @@ export function collectPluginConfigAssignments(params: {
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
env: params.context.env,
|
env: params.context.env,
|
||||||
cache: true,
|
cache: true,
|
||||||
fallbackToBundledMetadata: false,
|
fallbackToBundledMetadata: true,
|
||||||
|
fallbackToBundledMetadataForResolvedBundled: true,
|
||||||
|
fallbackBundledPluginIds: bundledLoadablePluginIds,
|
||||||
pluginIds: Object.keys(entries),
|
pluginIds: Object.keys(entries),
|
||||||
}).entries(),
|
}).entries(),
|
||||||
].flatMap(([pluginId, metadata]) => {
|
].flatMap(([pluginId, metadata]) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||||
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
|
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
|
||||||
@@ -66,6 +67,30 @@ function listBundledWebProviderSecretTargetRegistryEntries(): SecretTargetRegist
|
|||||||
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
|
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listBundledPluginConfigSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
||||||
|
const entries: SecretTargetRegistryEntry[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const record of listBundledPluginMetadata({
|
||||||
|
includeChannelConfigs: false,
|
||||||
|
includeSyntheticChannelConfigs: false,
|
||||||
|
})) {
|
||||||
|
const secretInputs = record.manifest.configContracts?.secretInputs?.paths ?? [];
|
||||||
|
for (const secretInput of secretInputs) {
|
||||||
|
const entry = createPluginOpenClawConfigSecretTargetEntry(
|
||||||
|
record.manifest.id,
|
||||||
|
secretInput.path,
|
||||||
|
);
|
||||||
|
const key = `${entry.configFile}:${entry.pathPattern}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
|
||||||
|
}
|
||||||
|
|
||||||
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
||||||
const entries: SecretTargetRegistryEntry[] = [];
|
const entries: SecretTargetRegistryEntry[] = [];
|
||||||
|
|
||||||
@@ -436,6 +461,7 @@ export function getSecretTargetRegistry(): SecretTargetRegistryEntry[] {
|
|||||||
cachedSecretTargetRegistry = [
|
cachedSecretTargetRegistry = [
|
||||||
...CORE_SECRET_TARGET_REGISTRY,
|
...CORE_SECRET_TARGET_REGISTRY,
|
||||||
...listBundledWebProviderSecretTargetRegistryEntries(),
|
...listBundledWebProviderSecretTargetRegistryEntries(),
|
||||||
|
...listBundledPluginConfigSecretTargetRegistryEntries(),
|
||||||
...listChannelSecretTargetRegistryEntries(),
|
...listChannelSecretTargetRegistryEntries(),
|
||||||
];
|
];
|
||||||
return cachedSecretTargetRegistry;
|
return cachedSecretTargetRegistry;
|
||||||
|
|||||||
@@ -72,4 +72,23 @@ describe("secret target registry", () => {
|
|||||||
expect(fetchTarget).not.toBeNull();
|
expect(fetchTarget).not.toBeNull();
|
||||||
expect(fetchTarget?.entry?.id).toBe("plugins.entries.firecrawl.config.webFetch.apiKey");
|
expect(fetchTarget?.entry?.id).toBe("plugins.entries.firecrawl.config.webFetch.apiKey");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives bundled plugin SecretInput contract target paths from plugin manifests", () => {
|
||||||
|
const coreTargetIds = new Set(getCoreSecretTargetRegistry().map((entry) => entry.id));
|
||||||
|
expect(coreTargetIds.has("plugins.entries.voice-call.config.twilio.authToken")).toBe(false);
|
||||||
|
|
||||||
|
const target = resolveConfigSecretTargetByPath([
|
||||||
|
"plugins",
|
||||||
|
"entries",
|
||||||
|
"voice-call",
|
||||||
|
"config",
|
||||||
|
"tts",
|
||||||
|
"providers",
|
||||||
|
"elevenlabs",
|
||||||
|
"apiKey",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(target).not.toBeNull();
|
||||||
|
expect(target?.entry?.id).toBe("plugins.entries.voice-call.config.tts.providers.*.apiKey");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user