mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 11:41:34 +00:00
fix(plugins): keep feed trust within env budget (#113364)
This commit is contained in:
committed by
GitHub
parent
8029b3bbb8
commit
f41e28202f
@@ -563,11 +563,9 @@ refreshes fail if OpenClaw cannot persist the validated snapshot.
|
||||
|
||||
The built-in `clawhub-public` profile expects payload identity
|
||||
`clawhub-official`. OpenClaw will bundle ClawHub's production public key after
|
||||
ClawHub generates and hands off that key. Until then, staging and private
|
||||
deployments can inject both `OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID` and
|
||||
`OPENCLAW_CLAWHUB_FEED_TRUSTED_PUBLIC_KEY`; setting only one fails closed before
|
||||
fetch. Public keys must come from a trusted release or operator channel, not
|
||||
from a key endpoint on the feed host.
|
||||
ClawHub generates and hands off that key. Until then, the built-in profile does
|
||||
not grant signed-feed install authority. Public keys must come from a trusted
|
||||
release or operator channel, not from a key endpoint on the feed host.
|
||||
|
||||
OpenClaw verifies the DSSE envelope and, when a profile declares `feedId`,
|
||||
requires the decoded payload ID to match it. The built-in `clawhub-public`
|
||||
|
||||
@@ -389,9 +389,17 @@ describe("official external plugin catalog", () => {
|
||||
});
|
||||
|
||||
const result = await loadHostedCatalog({
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "acme-root",
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_PUBLIC_KEY: signed.publicKeyPem,
|
||||
catalogConfig: {
|
||||
feeds: {
|
||||
"clawhub-public": {
|
||||
url: "https://clawhub.ai/v1/feeds/plugins",
|
||||
feedId: "clawhub-official",
|
||||
verification: {
|
||||
mode: "signed",
|
||||
keys: [{ keyId: "acme-root", publicKey: signed.publicKeyPem }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ifModifiedSince: "Mon, 22 Jun 2026 00:00:00 GMT",
|
||||
fetchImpl,
|
||||
@@ -404,7 +412,7 @@ describe("official external plugin catalog", () => {
|
||||
expect(result.trust).toMatchObject({ mode: "signed", signedBy: "acme-root" });
|
||||
});
|
||||
|
||||
it("preserves default ClawHub trust when the built-in profile is customized", async () => {
|
||||
it("uses configured ClawHub trust when the built-in profile is customized", async () => {
|
||||
const feed = {
|
||||
...hostedCatalogFeed({ sequence: 12, pluginName: "@openclaw/default-configured" }),
|
||||
id: "clawhub-official",
|
||||
@@ -416,13 +424,13 @@ describe("official external plugin catalog", () => {
|
||||
"clawhub-public": {
|
||||
url: "https://clawhub.ai/v1/feeds/plugins",
|
||||
feedId: "clawhub-official",
|
||||
verification: {
|
||||
mode: "signed",
|
||||
keys: [{ keyId: "acme-root", publicKey: signed.publicKeyPem }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "acme-root",
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_PUBLIC_KEY: signed.publicKeyPem,
|
||||
},
|
||||
fetchImpl: vi.fn(async () => dsseResponse(signed.body, { status: 200 })),
|
||||
snapshotStore: null,
|
||||
});
|
||||
@@ -446,9 +454,6 @@ describe("official external plugin catalog", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "incomplete",
|
||||
},
|
||||
fetchImpl: vi.fn(async () => new Response(body, { status: 200 })),
|
||||
snapshotStore: null,
|
||||
});
|
||||
@@ -463,9 +468,17 @@ describe("official external plugin catalog", () => {
|
||||
});
|
||||
|
||||
const result = await loadHostedCatalog({
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "acme-root",
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_PUBLIC_KEY: signed.publicKeyPem,
|
||||
catalogConfig: {
|
||||
feeds: {
|
||||
"clawhub-public": {
|
||||
url: "https://clawhub.ai/v1/feeds/plugins",
|
||||
feedId: "clawhub-official",
|
||||
verification: {
|
||||
mode: "signed",
|
||||
keys: [{ keyId: "acme-root", publicKey: signed.publicKeyPem }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fetchImpl: vi.fn(async () => dsseResponse(signed.body, { status: 200 })),
|
||||
snapshotStore: null,
|
||||
@@ -478,39 +491,6 @@ describe("official external plugin catalog", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed before fetch when default ClawHub trust env is incomplete", async () => {
|
||||
const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 }));
|
||||
const result = await loadHostedCatalog({
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "acme-root",
|
||||
},
|
||||
fetchImpl,
|
||||
snapshotStore: null,
|
||||
});
|
||||
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
expectBundledFallback(result);
|
||||
expect(result.entries).toEqual([]);
|
||||
expect(result.error).toContain("requires both key id and public key env vars");
|
||||
});
|
||||
|
||||
it("preserves default ClawHub trust for an explicit default feed URL", async () => {
|
||||
const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 }));
|
||||
const result = await loadHostedCatalog({
|
||||
feedUrl: "https://clawhub.ai/v1/feeds/plugins",
|
||||
env: {
|
||||
OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID: "acme-root",
|
||||
},
|
||||
fetchImpl,
|
||||
snapshotStore: null,
|
||||
});
|
||||
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
expectBundledFallback(result);
|
||||
expect(result.entries).toEqual([]);
|
||||
expect(result.error).toContain("requires both key id and public key env vars");
|
||||
});
|
||||
|
||||
it("loads schema-v2 marketplace entries and gates installs by state and trust", async () => {
|
||||
const body = JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
|
||||
@@ -27,13 +27,6 @@ class HostedCatalogSnapshotWriteError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
class HostedCatalogTrustConfigurationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "HostedCatalogTrustConfigurationError";
|
||||
}
|
||||
}
|
||||
|
||||
export type OfficialExternalProviderAuthChoice = {
|
||||
method?: string;
|
||||
choiceId?: string;
|
||||
@@ -268,10 +261,6 @@ const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE = "clawhub-public";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_ID = "clawhub-official";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF = "public-clawhub";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF = "public-npm";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_KEY_ID_ENV =
|
||||
"OPENCLAW_CLAWHUB_FEED_TRUSTED_KEY_ID";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_PUBLIC_KEY_ENV =
|
||||
"OPENCLAW_CLAWHUB_FEED_TRUSTED_PUBLIC_KEY";
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_KEYS: readonly OfficialExternalPluginCatalogFeedSigningKey[] =
|
||||
[];
|
||||
const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG: OfficialExternalPluginCatalogProfileConfig =
|
||||
@@ -405,38 +394,11 @@ function resolveHostedCatalogFeedUrl(raw: string): URL {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveDefaultClawHubFeedVerificationFromEnv(
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): OfficialExternalPluginCatalogFeedVerification | undefined {
|
||||
const keyId = normalizeOptionalString(
|
||||
env?.[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_KEY_ID_ENV],
|
||||
);
|
||||
const publicKey = normalizeOptionalString(
|
||||
env?.[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_PUBLIC_KEY_ENV],
|
||||
);
|
||||
if (!keyId && !publicKey) {
|
||||
return undefined;
|
||||
}
|
||||
if (!keyId || !publicKey) {
|
||||
throw new HostedCatalogTrustConfigurationError(
|
||||
"default ClawHub feed trust requires both key id and public key env vars",
|
||||
);
|
||||
}
|
||||
return {
|
||||
mode: "signed",
|
||||
keys: [{ keyId, publicKey }],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOfficialExternalPluginCatalogProfileConfig(
|
||||
config?: OfficialExternalPluginCatalogProfileConfig,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
): Required<OfficialExternalPluginCatalogProfileConfig> {
|
||||
const configuredDefaultFeed =
|
||||
config?.feeds?.[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE];
|
||||
const envVerification = configuredDefaultFeed?.verification
|
||||
? undefined
|
||||
: resolveDefaultClawHubFeedVerificationFromEnv(env);
|
||||
const bundledVerification =
|
||||
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_KEYS.length > 0
|
||||
? {
|
||||
@@ -444,7 +406,6 @@ function resolveOfficialExternalPluginCatalogProfileConfig(
|
||||
keys: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_TRUSTED_KEYS,
|
||||
}
|
||||
: undefined;
|
||||
const defaultVerification = envVerification ?? bundledVerification;
|
||||
const defaultFeed = DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG.feeds?.[
|
||||
DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE
|
||||
] ?? {
|
||||
@@ -456,7 +417,7 @@ function resolveOfficialExternalPluginCatalogProfileConfig(
|
||||
...config?.feeds,
|
||||
[DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE]: {
|
||||
...defaultFeed,
|
||||
...(defaultVerification ? { verification: defaultVerification } : {}),
|
||||
...(bundledVerification ? { verification: bundledVerification } : {}),
|
||||
...configuredDefaultFeed,
|
||||
},
|
||||
},
|
||||
@@ -471,7 +432,6 @@ function resolveHostedCatalogFeedSource(params: {
|
||||
feedUrl?: string;
|
||||
feedProfile?: string;
|
||||
catalogConfig?: OfficialExternalPluginCatalogProfileConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): {
|
||||
url: URL;
|
||||
hostnameAllowlist: string[];
|
||||
@@ -499,12 +459,7 @@ function resolveHostedCatalogFeedSource(params: {
|
||||
const profileConfig =
|
||||
profileName === undefined
|
||||
? undefined
|
||||
: resolveOfficialExternalPluginCatalogProfileConfig(
|
||||
params.catalogConfig,
|
||||
profileName === DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE
|
||||
? params.env
|
||||
: undefined,
|
||||
);
|
||||
: resolveOfficialExternalPluginCatalogProfileConfig(params.catalogConfig);
|
||||
const profile = profileName === undefined ? undefined : profileConfig?.feeds[profileName];
|
||||
if (profileName !== undefined && !profile) {
|
||||
throw new Error(`hosted catalog feed profile "${profileName}" is not configured`);
|
||||
@@ -517,10 +472,7 @@ function resolveHostedCatalogFeedSource(params: {
|
||||
};
|
||||
}
|
||||
const profileName = explicitProfileName ?? DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE;
|
||||
const profileConfig = resolveOfficialExternalPluginCatalogProfileConfig(
|
||||
params.catalogConfig,
|
||||
profileName === DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE ? params.env : undefined,
|
||||
);
|
||||
const profileConfig = resolveOfficialExternalPluginCatalogProfileConfig(params.catalogConfig);
|
||||
const profile = profileConfig.feeds[profileName];
|
||||
if (!profile) {
|
||||
throw new Error(`hosted catalog feed profile "${profileName}" is not configured`);
|
||||
@@ -1109,12 +1061,9 @@ async function loadHostedOfficialExternalPluginCatalogEntries(params?: {
|
||||
feedUrl: params?.feedUrl,
|
||||
feedProfile: params?.feedProfile,
|
||||
catalogConfig: params?.catalogConfig,
|
||||
env: params?.env ?? process.env,
|
||||
});
|
||||
} catch (err) {
|
||||
return err instanceof HostedCatalogTrustConfigurationError
|
||||
? emptyBundledFallbackResult(err)
|
||||
: bundledFallbackResult(err);
|
||||
return bundledFallbackResult(err);
|
||||
}
|
||||
const { url } = source;
|
||||
const snapshotStore = await resolveHostedCatalogSnapshotStore({
|
||||
|
||||
Reference in New Issue
Block a user