mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
fix(agentmail): allow official ClawHub channel to load (#155097)
* fix(agentmail): allow official ClawHub channel to load * docs(agentmail): keep setup guide vendor-owned * fix(agentmail): require API key for configured state * fix(plugin): verify pinned digest before legacy trust
This commit is contained in:
parent
2a7642919e
commit
f9358275ab
8 changed files with 256 additions and 17 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"entries": [
|
||||
{"name":"@agentmail/agentmail","description":"Official AgentMail email channel and CLI-backed skill.","source":"external","kind":"channel","openclaw":{"plugin":{"id":"agentmail","label":"AgentMail"},"channel":{"id":"agentmail","label":"AgentMail","selectionLabel":"AgentMail (Email)","detailLabel":"AgentMail Email","docsPath":"https://www.agentmail.to/docs/integrations/openclaw","docsLabel":"agentmail","exposure":{"docs":false},"blurb":"Durable, allowlisted, reply-only email conversations through AgentMail.","order":89,"quickstartAllowFrom":true,"configuredState":{"env":{"anyOf":["AGENTMAIL_API_KEY"]}}},"channelConfigs":{"agentmail":{"label":"AgentMail","description":"AgentMail email channel accounts and sender allowlist.","schema":{"type":"object","additionalProperties":true}}},"install":{"clawhubSpec":"clawhub:@agentmail/agentmail@0.2.1","expectedIntegrity":"sha256:155221cec38673a39bc27629f9f6ec87567ce4e37b7fa619ec4b1f7ca3d28730","defaultChoice":"clawhub","minHostVersion":">=2026.8.1-beta.2","allowInvalidConfigRecovery":true}}},
|
||||
{"name":"@openclaw/buzz","version":"2026.9.6","description":"Connect OpenClaw agents to Buzz rooms","source":"official","kind":"channel","openclaw":{"channelConfigs":{"buzz":{"label":"Buzz","description":"Connect OpenClaw agents to Buzz team rooms."}},"channel":{"id":"buzz","configuredState":{"env":{"allOf":["BUZZ_RELAY_URL","BUZZ_PRIVATE_KEY"]}},"label":"Buzz","selectionLabel":"Buzz","docsPath":"/channels/buzz","docsLabel":"buzz","blurb":"Connect OpenClaw agents to Buzz team rooms.","markdownCapable":true,"order":56,"setup":{"fields":[{"key":"relayUrl","kind":"string","cli":{"flags":"--relay-url <url>","description":"Buzz relay WebSocket URL"}},{"key":"privateKey","kind":"string","sensitive":true,"cli":{"flags":"--private-key <key>","description":"Buzz bot Nostr private key"}},{"key":"useEnv","kind":"boolean","cli":{"flags":"--use-env","description":"Use BUZZ_PRIVATE_KEY with the supplied relay URL"},"envVars":["BUZZ_PRIVATE_KEY"]}]}},"install":{"clawhubSpec":"clawhub:@openclaw/buzz","npmSpec":"@openclaw/buzz","defaultChoice":"npm","minHostVersion":">=2026.7.2"}}},
|
||||
{"name":"@openclaw/clickclack","version":"2026.9.6","description":"OpenClaw ClickClack channel plugin","source":"official","kind":"channel","openclaw":{"contracts":{"tools":["discussion"]},"channelConfigs":{"clickclack":{"label":"ClickClack","description":"ClickClack channel accounts and group activation policy."}},"channel":{"id":"clickclack","configuredState":{"env":{"anyOf":["CLICKCLACK_BOT_TOKEN"]}},"label":"ClickClack","selectionLabel":"ClickClack","detailLabel":"ClickClack Bot","docsPath":"/channels/clickclack","docsLabel":"clickclack","blurb":"self-hosted chat via first-class ClickClack bot tokens.","systemImage":"bubble.left.and.bubble.right","markdownCapable":true,"preferSessionLookupForAnnounceTarget":true,"order":85,"commands":{"nativeCommandsAutoEnabled":false,"nativeSkillsAutoEnabled":false},"setup":{"fields":[{"key":"code","kind":"string","sensitive":true,"cli":{"flags":"--code <code>","description":"ClickClack one-time setup code or setup URL"}},{"key":"token","kind":"string","sensitive":true,"cli":{"flags":"--token <token>","description":"ClickClack bot token"}},{"key":"tokenFile","kind":"string","sensitive":true,"cli":{"flags":"--token-file <path>","description":"ClickClack bot token file"}},{"key":"baseUrl","kind":"string","cli":{"flags":"--base-url <url>","description":"ClickClack API base URL"}},{"key":"workspace","kind":"string","cli":{"flags":"--workspace <workspace>","description":"ClickClack workspace id, slug, or name"}},{"key":"defaultTo","kind":"string","cli":{"flags":"--default-to <target>","description":"Default ClickClack target"}},{"key":"allowFrom","kind":"string-list","cli":{"flags":"--allow-from <ids>","description":"Allowed ClickClack senders"}},{"key":"agentActivity","kind":"boolean","cli":{"flags":"--agent-activity","description":"Enable ClickClack agent activity"}},{"key":"useEnv","kind":"boolean","cli":{"flags":"--use-env","description":"Use CLICKCLACK_BOT_TOKEN"},"envVars":["CLICKCLACK_BOT_TOKEN"]}]}},"install":{"clawhubSpec":"clawhub:@openclaw/clickclack","npmSpec":"@openclaw/clickclack","defaultChoice":"npm","minHostVersion":">=2026.6.9","allowInvalidConfigRecovery":true}}},
|
||||
{"name":"@openclaw/discord","version":"2026.9.6","description":"OpenClaw Discord channel plugin for channels, DMs, commands, and app events.","source":"official","kind":"channel","openclaw":{"contracts":{"transcriptSourceProviders":["discord-voice"]},"channel":{"id":"discord","configuredState":{"env":{"anyOf":["DISCORD_BOT_TOKEN"]}},"approvalFlags":["native"],"label":"Discord","selectionLabel":"Discord (Bot API)","detailLabel":"Discord Bot","docsPath":"/channels/discord","docsLabel":"discord","blurb":"very well supported right now.","systemImage":"bubble.left.and.bubble.right","markdownCapable":true,"preferSessionLookupForAnnounceTarget":true,"setup":{"fields":[{"key":"token","kind":"string","sensitive":true,"cli":{"flags":"--token <token>","description":"Discord bot token"}},{"key":"useEnv","kind":"boolean","cli":{"flags":"--use-env","description":"Use DISCORD_BOT_TOKEN"},"envVars":["DISCORD_BOT_TOKEN"]}]},"commands":{"nativeCommandsAutoEnabled":true,"nativeSkillsAutoEnabled":true},"doctorCapabilities":{"dmAllowFromMode":"topOnly","groupModel":"route","groupAllowFromFallbackToAllowFrom":false,"warnOnEmptyGroupSenderAllowlist":false}},"install":{"clawhubSpec":"clawhub:@openclaw/discord","npmSpec":"@openclaw/discord","defaultChoice":"npm","minHostVersion":">=2026.5.26","allowInvalidConfigRecovery":true}}},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,43 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "@agentmail/agentmail",
|
||||
"description": "Official AgentMail email channel and CLI-backed skill.",
|
||||
"source": "external",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"plugin": { "id": "agentmail", "label": "AgentMail" },
|
||||
"channel": {
|
||||
"id": "agentmail",
|
||||
"label": "AgentMail",
|
||||
"selectionLabel": "AgentMail (Email)",
|
||||
"detailLabel": "AgentMail Email",
|
||||
"docsPath": "https://www.agentmail.to/docs/integrations/openclaw",
|
||||
"docsLabel": "agentmail",
|
||||
"exposure": { "docs": false },
|
||||
"blurb": "Durable, allowlisted, reply-only email conversations through AgentMail.",
|
||||
"order": 89,
|
||||
"quickstartAllowFrom": true,
|
||||
"configuredState": {
|
||||
"env": { "anyOf": ["AGENTMAIL_API_KEY"] }
|
||||
}
|
||||
},
|
||||
"channelConfigs": {
|
||||
"agentmail": {
|
||||
"label": "AgentMail",
|
||||
"description": "AgentMail email channel accounts and sender allowlist.",
|
||||
"schema": { "type": "object", "additionalProperties": true }
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
"clawhubSpec": "clawhub:@agentmail/agentmail@0.2.1",
|
||||
"expectedIntegrity": "sha256:155221cec38673a39bc27629f9f6ec87567ce4e37b7fa619ec4b1f7ca3d28730",
|
||||
"defaultChoice": "clawhub",
|
||||
"minHostVersion": ">=2026.8.1-beta.2",
|
||||
"allowInvalidConfigRecovery": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@wecom/wecom-openclaw-plugin",
|
||||
"description": "OpenClaw WeCom channel plugin by the Tencent WeCom team.",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,58 @@ describe("describePluginInstallSource", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"ab".repeat(32),
|
||||
`sha256:${"ab".repeat(32)}`,
|
||||
`sha256-${Buffer.alloc(32, 0xab).toString("base64")}`,
|
||||
])("accepts ClawHub-only integrity metadata %s", (expectedIntegrity) => {
|
||||
const source = describePluginInstallSource({
|
||||
clawhubSpec: "clawhub:@vendor/demo@1.2.3",
|
||||
expectedIntegrity,
|
||||
defaultChoice: "clawhub",
|
||||
});
|
||||
expect(source.clawhub).toMatchObject({ packageName: "@vendor/demo", exactVersion: true });
|
||||
expect(source.npm).toBeUndefined();
|
||||
expect(source.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not let ClawHub conceal integrity on an invalid declared npm source", () => {
|
||||
expect(
|
||||
describePluginInstallSource({
|
||||
clawhubSpec: "clawhub:@vendor/demo@1.2.3",
|
||||
npmSpec: "github:vendor/demo",
|
||||
expectedIntegrity: `sha256:${"ab".repeat(32)}`,
|
||||
}).warnings,
|
||||
).toEqual(["invalid-npm-spec", "npm-integrity-without-source"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{},
|
||||
{ localPath: "extensions/demo" },
|
||||
{ clawhubSpec: "clawhub:@vendor/demo@1.2.3", expectedIntegrity: "not-a-hash" },
|
||||
])("preserves warnings for unusable integrity metadata %j", (install) => {
|
||||
expect(
|
||||
describePluginInstallSource({
|
||||
expectedIntegrity: `sha256:${"ab".repeat(32)}`,
|
||||
...install,
|
||||
}).warnings,
|
||||
).toEqual(["npm-integrity-without-source"]);
|
||||
});
|
||||
|
||||
it("keeps npm integrity ownership when both sources are declared", () => {
|
||||
const source = describePluginInstallSource({
|
||||
clawhubSpec: "clawhub:@vendor/demo@1.2.3",
|
||||
npmSpec: "@vendor/demo@1.2.3",
|
||||
expectedIntegrity: "sha512-demo",
|
||||
defaultChoice: "clawhub",
|
||||
});
|
||||
expect(source.npm).toMatchObject({
|
||||
expectedIntegrity: "sha512-demo",
|
||||
pinState: "exact-with-integrity",
|
||||
});
|
||||
expect(source.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("marks exact npm specs with integrity as fully pinned", () => {
|
||||
expect(
|
||||
describePluginInstallSource({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/** Describes package-authored plugin install source metadata and pinning warnings. */
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { normalizeClawHubSha256Integrity } from "../infra/clawhub-integrity.js";
|
||||
import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
|
||||
import { isExactSemverVersion, parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import type {
|
||||
|
|
@ -111,7 +112,10 @@ export function describePluginInstallSource(
|
|||
) {
|
||||
warnings.push("default-choice-missing-source");
|
||||
}
|
||||
if (expectedIntegrity && !npm) {
|
||||
// Integrity belongs to npm when declared, otherwise to a ClawHub-only source.
|
||||
const hasClawHubIntegrity =
|
||||
!npmSpec && clawhub && expectedIntegrity && normalizeClawHubSha256Integrity(expectedIntegrity);
|
||||
if (expectedIntegrity && !npm && !hasClawHubIntegrity) {
|
||||
warnings.push("npm-integrity-without-source");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import path from "node:path";
|
|||
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
||||
import { maybeRepairPluginRegistryState } from "../commands/doctor-plugin-registry.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { normalizeClawHubSha256Integrity } from "../infra/clawhub-integrity.js";
|
||||
import { resetPluginStateStoreForTests } from "../plugin-state/plugin-state-store.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { refreshPersistedInstalledPluginIndex } from "./installed-plugin-index-store-write.js";
|
||||
|
|
@ -17,8 +18,14 @@ import {
|
|||
} from "./loader.test-fixtures.js";
|
||||
import { buildPluginInspectReport, buildPluginSnapshotReport } from "./status.js";
|
||||
|
||||
const pluginId = "diagnostics-otel";
|
||||
const packageName = `@openclaw/${pluginId}`;
|
||||
const defaultPluginId = "diagnostics-otel";
|
||||
const defaultPackageName = `@openclaw/${defaultPluginId}`;
|
||||
const agentMailIntegrity = normalizeClawHubSha256Integrity(
|
||||
"sha256:155221cec38673a39bc27629f9f6ec87567ce4e37b7fa619ec4b1f7ca3d28730",
|
||||
);
|
||||
if (!agentMailIntegrity) {
|
||||
throw new Error("Expected a valid AgentMail catalog integrity");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginStateStoreForTests();
|
||||
|
|
@ -31,7 +38,7 @@ describe("recorded plugin trust diagnostics", () => {
|
|||
{ name: "legacy npm spec", override: {}, reason: "trusted-official", trusted: true },
|
||||
{
|
||||
name: "legacy ClawHub spec",
|
||||
override: { source: "clawhub", spec: `clawhub:${packageName}@2026.8.2` },
|
||||
override: { source: "clawhub", spec: `clawhub:${defaultPackageName}@2026.8.2` },
|
||||
reason: "provenance-missing",
|
||||
trusted: false,
|
||||
repair: true,
|
||||
|
|
@ -56,37 +63,113 @@ describe("recorded plugin trust diagnostics", () => {
|
|||
reason: "origin-path",
|
||||
trusted: false,
|
||||
},
|
||||
{
|
||||
name: "official AgentMail ClawHub install",
|
||||
pluginId: "agentmail",
|
||||
packageName: "@agentmail/agentmail",
|
||||
version: "0.2.1",
|
||||
override: {
|
||||
source: "clawhub",
|
||||
spec: "clawhub:@agentmail/agentmail@0.2.1",
|
||||
clawhubPackage: "@agentmail/agentmail",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubChannel: "official",
|
||||
},
|
||||
reason: "trusted-official",
|
||||
trusted: true,
|
||||
},
|
||||
{
|
||||
name: "legacy AgentMail ClawHub install",
|
||||
pluginId: "agentmail",
|
||||
packageName: "@agentmail/agentmail",
|
||||
version: "0.2.1",
|
||||
override: { source: "clawhub", spec: "clawhub:@agentmail/agentmail@0.2.1" },
|
||||
reason: "provenance-missing",
|
||||
trusted: false,
|
||||
repair: true,
|
||||
repairTrusted: false,
|
||||
},
|
||||
{
|
||||
name: "legacy AgentMail ClawHub install with matching integrity",
|
||||
pluginId: "agentmail",
|
||||
packageName: "@agentmail/agentmail",
|
||||
version: "0.2.1",
|
||||
override: {
|
||||
source: "clawhub",
|
||||
spec: "clawhub:@agentmail/agentmail@0.2.1",
|
||||
integrity: agentMailIntegrity,
|
||||
},
|
||||
reason: "provenance-missing",
|
||||
trusted: false,
|
||||
repair: true,
|
||||
repairTrusted: true,
|
||||
},
|
||||
{
|
||||
name: "unendorsed AgentMail npm namesake",
|
||||
pluginId: "agentmail",
|
||||
packageName: "@agentmail/agentmail",
|
||||
version: "0.2.1",
|
||||
reason: "provenance-invalid",
|
||||
trusted: false,
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
pluginId?: string;
|
||||
packageName?: string;
|
||||
version?: string;
|
||||
override?: Partial<PluginInstallRecord>;
|
||||
missing?: boolean;
|
||||
reason: string;
|
||||
trusted: boolean;
|
||||
repair?: boolean;
|
||||
repairTrusted?: boolean;
|
||||
}>)(
|
||||
"inspection and registration agree for $name",
|
||||
async ({ override, missing, reason, trusted, repair }) => {
|
||||
async ({
|
||||
override,
|
||||
missing,
|
||||
reason,
|
||||
trusted,
|
||||
repair,
|
||||
repairTrusted,
|
||||
pluginId = defaultPluginId,
|
||||
packageName = defaultPackageName,
|
||||
version = "2026.8.2",
|
||||
}) => {
|
||||
useNoBundledPlugins();
|
||||
const stateDir = fs.realpathSync(makePluginLoaderTempDir());
|
||||
const plugin = writePlugin({
|
||||
id: pluginId,
|
||||
dir: path.join(stateDir, "extensions", pluginId),
|
||||
filename: "index.cjs",
|
||||
body: `module.exports = { id: ${JSON.stringify(pluginId)}, register(api) { api.runtime.state.openKeyedStore({ namespace: "proof", maxEntries: 2 }); } };`,
|
||||
body: `module.exports = { id: ${JSON.stringify(pluginId)}, register(api) {
|
||||
const blocked = [];
|
||||
try {
|
||||
api.runtime.state.openKeyedStore({ namespace: "proof", maxEntries: 2 });
|
||||
} catch (error) {
|
||||
blocked.push("openKeyedStore: " + String(error));
|
||||
}
|
||||
try {
|
||||
api.runtime.state.openChannelIngressQueue({ accountId: "default" });
|
||||
} catch (error) {
|
||||
blocked.push("openChannelIngressQueue: " + String(error));
|
||||
}
|
||||
if (blocked.length) throw new Error(blocked.join("; "));
|
||||
} };`,
|
||||
});
|
||||
writePluginMetadata({
|
||||
dir: plugin.dir,
|
||||
id: plugin.id,
|
||||
packageJson: {
|
||||
name: packageName,
|
||||
version: "2026.8.2",
|
||||
version,
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
});
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
|
||||
const install: PluginInstallRecord = {
|
||||
source: "npm",
|
||||
spec: `${packageName}@2026.8.2`,
|
||||
spec: `${packageName}@${version}`,
|
||||
installPath: plugin.dir,
|
||||
...override,
|
||||
};
|
||||
|
|
@ -119,6 +202,12 @@ describe("recorded plugin trust diagnostics", () => {
|
|||
});
|
||||
expect(loaded.status).toBe(trusted ? "loaded" : "error");
|
||||
if (!trusted) {
|
||||
expect(loaded.error).toContain(
|
||||
"openKeyedStore is only available for trusted plugins in this release.",
|
||||
);
|
||||
expect(loaded.error).toContain(
|
||||
"openChannelIngressQueue is only available for trusted plugins in this release.",
|
||||
);
|
||||
expect(loaded.error).toContain(`loaded from ${JSON.stringify(plugin.file)}`);
|
||||
expect(loaded.error).toContain(`reason=${reason}`);
|
||||
expect(loaded.error).toContain(
|
||||
|
|
@ -144,10 +233,10 @@ describe("recorded plugin trust diagnostics", () => {
|
|||
(entry) => entry.id === pluginId,
|
||||
)!;
|
||||
expect(repaired).toMatchObject({
|
||||
status: "loaded",
|
||||
trustedOfficialInstall: true,
|
||||
trust: { reason: "trusted-official" },
|
||||
status: repairTrusted === false ? "error" : "loaded",
|
||||
trust: { reason: repairTrusted === false ? reason : "trusted-official" },
|
||||
});
|
||||
expect(repaired.trustedOfficialInstall === true).toBe(repairTrusted !== false);
|
||||
expect(inspectedAfter.trust).toEqual(repaired.trust);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Defines official external install records for plugins.
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { normalizeClawHubSha256Integrity } from "../infra/clawhub-integrity.js";
|
||||
import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import {
|
||||
|
|
@ -148,6 +149,7 @@ export function isTrustedOfficialPluginInstallRecord(params: {
|
|||
function hasTrustedClawHubSourceAuthority(
|
||||
record: PluginInstallRecord,
|
||||
officialClawHubSpec: string | undefined,
|
||||
officialExpectedIntegrity: string | undefined,
|
||||
): boolean {
|
||||
const hasAuthorityMetadata =
|
||||
record.clawhubUrl !== undefined || record.clawhubChannel !== undefined;
|
||||
|
|
@ -155,7 +157,19 @@ function hasTrustedClawHubSourceAuthority(
|
|||
return isOfficialClawHubInstallRecord(record);
|
||||
}
|
||||
// Older official installs persisted only their catalog-backed ClawHub spec.
|
||||
// Preserve that shipped shape, but do not let package-only records claim it.
|
||||
// For a pinned catalog artifact, require the recorded digest before restoring
|
||||
// authority; a matching package name alone cannot prove which code was installed.
|
||||
if (officialExpectedIntegrity) {
|
||||
const expectedIntegrity = normalizeClawHubSha256Integrity(officialExpectedIntegrity);
|
||||
const recordedIntegrity = record.integrity
|
||||
? normalizeClawHubSha256Integrity(record.integrity)
|
||||
: null;
|
||||
if (!expectedIntegrity || recordedIntegrity !== expectedIntegrity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Preserve the old spec-only shape for unpinned entries, but do not let
|
||||
// package-only records claim official provenance.
|
||||
return Boolean(
|
||||
officialClawHubSpec &&
|
||||
record.spec &&
|
||||
|
|
@ -374,7 +388,11 @@ export function resolveTrustedSourceLinkedOfficialClawHubInstall(params: {
|
|||
}
|
||||
const recordedPackageNames = resolveRecordedClawHubPackageNames(params.record);
|
||||
if (
|
||||
!hasTrustedClawHubSourceAuthority(params.record, officialClawHubSpec) ||
|
||||
!hasTrustedClawHubSourceAuthority(
|
||||
params.record,
|
||||
officialClawHubSpec,
|
||||
install?.expectedIntegrity,
|
||||
) ||
|
||||
!recordedPackageNames ||
|
||||
recordedPackageNames.length === 0 ||
|
||||
!recordedPackageNames.every((name) => officialNames.includes(name))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getOfficialExternalPluginCatalogEntry,
|
||||
getOfficialExternalPluginCatalogEntryForPackage,
|
||||
resolveOfficialExternalPluginInstall,
|
||||
} from "./official-external-plugin-catalog.js";
|
||||
|
||||
describe("official AgentMail catalog entry", () => {
|
||||
it("discovers AgentMail through its official ClawHub package without endorsing an npm namesake", () => {
|
||||
const entry = getOfficialExternalPluginCatalogEntry("agentmail");
|
||||
if (!entry) {
|
||||
throw new Error("Expected AgentMail in the official external channel catalog");
|
||||
}
|
||||
expect(getOfficialExternalPluginCatalogEntryForPackage("@agentmail/agentmail")).toBe(entry);
|
||||
expect(entry.kind).toBe("channel");
|
||||
expect(entry.openclaw?.channel).toMatchObject({
|
||||
configuredState: { env: { anyOf: ["AGENTMAIL_API_KEY"] } },
|
||||
docsPath: "https://www.agentmail.to/docs/integrations/openclaw",
|
||||
exposure: { docs: false },
|
||||
});
|
||||
expect(resolveOfficialExternalPluginInstall(entry)).toEqual({
|
||||
clawhubSpec: "clawhub:@agentmail/agentmail@0.2.1",
|
||||
expectedIntegrity: "sha256:155221cec38673a39bc27629f9f6ec87567ce4e37b7fa619ec4b1f7ca3d28730",
|
||||
defaultChoice: "clawhub",
|
||||
minHostVersion: ">=2026.8.1-beta.2",
|
||||
allowInvalidConfigRecovery: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
writeOfficialChannelDocsIndex,
|
||||
writeOfficialChannelCatalogSource,
|
||||
} from "../scripts/write-official-channel-catalog.mts";
|
||||
import { normalizeClawHubSha256Integrity } from "../src/infra/clawhub-integrity.js";
|
||||
import { describePluginInstallSource } from "../src/plugins/install-source-info.js";
|
||||
import { cleanupTempDirs, makeTempDir as makeTempRepoRoot } from "./helpers/temp-dir.js";
|
||||
import { writeJsonFile } from "./helpers/temp-repo.js";
|
||||
|
|
@ -88,7 +89,7 @@ function writeExternalChannelDocs(repoRoot: string): void {
|
|||
};
|
||||
for (const entry of seed.entries) {
|
||||
const channel = entry.openclaw?.channel;
|
||||
if (!channel?.docsPath || !channel.label) {
|
||||
if (!channel?.docsPath?.startsWith("/") || !channel.label) {
|
||||
continue;
|
||||
}
|
||||
const title = channel.id === "openclaw-weixin" ? "WeChat" : channel.label;
|
||||
|
|
@ -683,7 +684,7 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps third-party official external catalog npm sources pinned unless they track latest", () => {
|
||||
it("keeps third-party official external catalog install sources pinned", () => {
|
||||
const repoRoot = makeRepoRoot("openclaw-official-channel-catalog-policy-");
|
||||
const entries = buildOfficialChannelCatalog({ repoRoot }).entries.filter(
|
||||
(entry) => entry.source === "external" && !entry.name?.startsWith("@openclaw/"),
|
||||
|
|
@ -691,9 +692,16 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
for (const entry of entries) {
|
||||
const installSource = describePluginInstallSource(requireInstall(entry));
|
||||
const install = requireInstall(entry);
|
||||
const installSource = describePluginInstallSource(install);
|
||||
expect(installSource.warnings).toStrictEqual([]);
|
||||
expect(requireNpmInstallSource(installSource).pinState).toBe("exact-with-integrity");
|
||||
if (install.npmSpec) {
|
||||
expect(requireNpmInstallSource(installSource).pinState).toBe("exact-with-integrity");
|
||||
} else {
|
||||
expect(installSource.npm).toBeUndefined();
|
||||
expect(installSource.clawhub?.exactVersion).toBe(true);
|
||||
expect(normalizeClawHubSha256Integrity(install.expectedIntegrity ?? "")).not.toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue