fix: resolve ClickClack token secret refs

This commit is contained in:
Shakker
2026-07-15 13:05:10 +01:00
committed by Shakker
parent b09c99dfee
commit d599ac9133
5 changed files with 120 additions and 1 deletions
@@ -0,0 +1,6 @@
// ClickClack API module exposes the plugin secret contract.
export {
channelSecrets,
collectRuntimeConfigAssignments,
secretTargetRegistryEntries,
} from "./src/secret-contract.js";
+4
View File
@@ -7,4 +7,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./setup-plugin-api.js",
exportName: "clickClackSetupPlugin",
},
secrets: {
specifier: "./secret-contract-api.js",
exportName: "channelSecrets",
},
});
+5 -1
View File
@@ -16,7 +16,6 @@ import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import { resolveClickClackAccount } from "./accounts.js";
import {
CLICKCLACK_CHANNEL_ID,
clickClackConfigAdapter,
@@ -30,6 +29,7 @@ import {
sendClickClackMedia,
sendClickClackText,
} from "./outbound.js";
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
import { clickClackSetupAdapter } from "./setup-core.js";
import { clickClackSetupWizard } from "./setup-surface.js";
import {
@@ -129,6 +129,10 @@ export const clickClackPlugin: ChannelPlugin<ResolvedClickClackAccount> = create
config: clickClackConfigAdapter,
setup: clickClackSetupAdapter,
setupWizard: clickClackSetupWizard,
secrets: {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
},
messaging: {
targetPrefixes: ["clickclack", "cc"],
normalizeTarget: normalizeClickClackTarget,
@@ -0,0 +1,46 @@
// ClickClack tests cover token secret contract behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { createResolverContext } from "openclaw/plugin-sdk/secret-ref-runtime";
import { describe, expect, it } from "vitest";
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
describe("ClickClack secret contract", () => {
it("publishes ClickClack token targets", () => {
expect(secretTargetRegistryEntries.map((entry) => entry.id)).toEqual([
"channels.clickclack.accounts.*.token",
"channels.clickclack.token",
]);
});
it("collects an account file SecretRef even when the default uses tokenFile", () => {
const sourceConfig = {
channels: {
clickclack: {
enabled: true,
baseUrl: "https://clickclack.example",
workspace: "default",
tokenFile: "/run/secrets/default-clickclack-token",
accounts: {
work: {
token: { source: "file", provider: "vault", id: "/clickclack/work" },
},
},
},
},
} satisfies OpenClawConfig;
const context = createResolverContext({ sourceConfig, env: {} });
collectRuntimeConfigAssignments({
config: structuredClone(sourceConfig),
defaults: undefined,
context,
});
expect(context.assignments).toHaveLength(1);
expect(context.assignments[0]).toMatchObject({
path: "channels.clickclack.accounts.work.token",
ref: { source: "file", provider: "vault", id: "/clickclack/work" },
});
expect(context.warnings).toStrictEqual([]);
});
});
@@ -0,0 +1,59 @@
// ClickClack plugin module implements token secret contract behavior.
import {
collectConditionalChannelFieldAssignments,
createChannelSecretTargetRegistryEntries,
getChannelSurface,
hasConfiguredSecretInputValue,
type ResolverContext,
type SecretDefaults,
} from "openclaw/plugin-sdk/channel-secret-basic-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
export const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
channelKey: "clickclack",
account: ["token"],
channel: ["token"],
});
export function collectRuntimeConfigAssignments(params: {
config: { channels?: Record<string, unknown> };
defaults?: SecretDefaults;
context: ResolverContext;
}): void {
const resolved = getChannelSurface(params.config, "clickclack");
if (!resolved) {
return;
}
const { channel: clickclack, surface } = resolved;
const baseTokenFile = normalizeOptionalString(clickclack.tokenFile) ?? "";
const accountTokenFile = (account: Record<string, unknown>) =>
normalizeOptionalString(account.tokenFile) ?? "";
const hasImplicitDefault =
Boolean(normalizeOptionalString(clickclack.baseUrl)) &&
Boolean(normalizeOptionalString(clickclack.workspace));
collectConditionalChannelFieldAssignments({
channelKey: "clickclack",
field: "token",
channel: clickclack,
surface,
defaults: params.defaults,
context: params.context,
topLevelActiveWithoutAccounts: baseTokenFile.length === 0,
topLevelInheritedAccountActive: ({ account, enabled }) =>
(hasImplicitDefault && baseTokenFile.length === 0) ||
(enabled &&
baseTokenFile.length === 0 &&
accountTokenFile(account).length === 0 &&
!hasConfiguredSecretInputValue(account.token, params.defaults)),
accountActive: ({ account, enabled }) => enabled && accountTokenFile(account).length === 0,
topInactiveReason:
"no enabled ClickClack account inherits this top-level token (tokenFile is configured).",
accountInactiveReason: "ClickClack account is disabled or tokenFile is configured.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};