Skip to content

Commit b87f4a7

Browse files
committed
feat(plugins): accept slot owner records
1 parent 26913e6 commit b87f4a7

33 files changed

Lines changed: 450 additions & 75 deletions

docs/gateway/configuration-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,8 @@ restart after changing native plugin config.
355355
- `memory.qmd.*`
356356
- `plugins.entries.memory-core.config.dreaming`
357357
- Enabled Claude bundle plugins can also contribute embedded OpenClaw defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches.
358-
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
359-
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine.
358+
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins. The value may be a string id or an object with `owner`.
359+
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine. The value may be a string id or an object with `owner`.
360360

361361
See [Plugins](/tools/plugin).
362362

docs/plugins/manifest.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,8 +1435,8 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
14351435
- Native manifests are parsed with JSON5, so comments, trailing commas, and unquoted keys are accepted as long as the final value is still an object.
14361436
- Only documented manifest fields are read by the manifest loader. Avoid custom top-level keys.
14371437
- `channels`, `providers`, `cliBackends`, and `skills` can all be omitted when a plugin does not need them.
1438-
- `providerCatalogEntry` must stay lightweight and should not import broad runtime code; use it for static provider catalog metadata or narrow discovery descriptors, not request-time execution.
1439-
- Exclusive plugin kinds are selected through `plugins.slots.*`: `kind: "memory"` via `plugins.slots.memory`, `kind: "context-engine"` via `plugins.slots.contextEngine` (default `legacy`).
1438+
- `providerCatalogEntry` must stay lightweight and should not import broad runtime code; use it for static provider catalog metadata or narrow discovery descriptors, not request-time execution. `providerDiscoveryEntry` is the legacy spelling and still works for existing plugins.
1439+
- Exclusive plugin kinds are selected through `plugins.slots.*`: `kind: "memory"` via `plugins.slots.memory`, `kind: "context-engine"` via `plugins.slots.contextEngine` (default `legacy`). Slot values may be plugin id strings or `{ owner: "<plugin-id>" }` records; plugin authors should read the normalized owner instead of assuming a raw string.
14401440
- Declare exclusive plugin kind in this manifest. Runtime-entry `OpenClawPluginDefinition.kind` is deprecated and remains only as a compatibility fallback for older plugins.
14411441
- Env-var metadata (`setup.providers[].envVars`, deprecated `providerAuthEnvVars`, and `channelEnvVars`) is declarative only. Status, audit, cron delivery validation, and other read-only surfaces still apply plugin trust and effective activation policy before treating an env var as configured.
14421442
- For runtime wizard metadata that requires provider code, see [Provider runtime hooks](/plugins/architecture-internals#provider-runtime-hooks).

docs/tools/plugin.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ Key policy rules:
182182
for that slot by counting as explicit activation; it can load even when it
183183
would otherwise be opt-in. `plugins.deny` and
184184
`plugins.entries.<id>.enabled: false` still block it.
185+
- Slot values can be a plugin id string or an owner record such as
186+
`{ owner: "memory-lancedb", claimed_at: "2026-04-23T21:14:00Z" }`.
187+
OpenClaw reads both forms as the same selected owner. Current CLI install and
188+
repair flows may still write the string form.
185189
- Bundled opt-in plugins can auto-activate when config names one of their owned
186190
surfaces, such as a provider/model ref, channel config, CLI backend, or agent
187191
harness runtime.

src/agents/provider-auth-aliases.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
4040

4141
import {
4242
resetProviderAuthAliasMapCacheForTest,
43+
resolveProviderAuthAliasMap,
4344
resolveProviderIdForAuth,
4445
} from "./provider-auth-aliases.js";
4546

@@ -115,4 +116,30 @@ describe("provider auth aliases", () => {
115116
2,
116117
);
117118
});
119+
120+
it("trusts workspace context-engine provider aliases selected by object-form slot owner", () => {
121+
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
122+
plugins: [
123+
{
124+
id: "workspace-engine",
125+
origin: "workspace",
126+
providerAuthAliases: { "engine-plan": "engine-provider" },
127+
},
128+
],
129+
diagnostics: [],
130+
});
131+
132+
expect(
133+
resolveProviderAuthAliasMap({
134+
config: {
135+
plugins: {
136+
slots: {
137+
contextEngine: { owner: "workspace-engine", claimed_by_version: "0.9.10" },
138+
},
139+
},
140+
},
141+
includeUntrustedWorkspacePlugins: false,
142+
}),
143+
).toEqual({ "engine-plan": "engine-provider" });
144+
});
118145
});

src/agents/provider-auth-aliases.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-
1111
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
1212
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
1313
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
14+
import { resolvePluginSlotOwner } from "../plugins/slots.js";
1415

1516
export type ProviderAuthAliasLookupParams = {
1617
config?: OpenClawConfig;
@@ -73,7 +74,8 @@ function isWorkspacePluginTrustedForAuthAliases(
7374
return isWorkspacePluginAllowedByConfig({
7475
config,
7576
isImplicitlyAllowed: (pluginId) =>
76-
normalizePluginConfigId(config?.plugins?.slots?.contextEngine) === pluginId,
77+
normalizePluginConfigId(resolvePluginSlotOwner(config?.plugins?.slots?.contextEngine)) ===
78+
pluginId,
7779
plugin,
7880
});
7981
}

src/cli/command-secret-gateway.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,16 @@ describe("resolveCommandSecretRefsViaGateway", () => {
108108
return cursor;
109109
}
110110

111+
type ResolveManifestContractOwnerPluginId = NonNullable<
112+
Parameters<
113+
typeof commandSecretGatewayTesting.setDepsForTest
114+
>[0]["resolveManifestContractOwnerPluginId"]
115+
>;
116+
111117
function setSingleSecretTargetDeps(params: {
112118
path: string;
113119
pathSegments: readonly string[];
114-
resolveManifestContractOwnerPluginId?: NonNullable<
115-
Parameters<
116-
typeof commandSecretGatewayTesting.setDepsForTest
117-
>[0]["resolveManifestContractOwnerPluginId"]
118-
>;
120+
resolveManifestContractOwnerPluginId?: ResolveManifestContractOwnerPluginId;
119121
}): () => void {
120122
const deps: Parameters<typeof commandSecretGatewayTesting.setDepsForTest>[0] = {
121123
analyzeCommandSecretAssignmentsFromSnapshot: ({ inactiveRefPaths, resolvedConfig }) => {

src/commands/doctor/shared/codex-route-warnings.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { SessionEntry } from "../../../config/sessions/types.js";
1616
import type { AgentRuntimePolicyConfig } from "../../../config/types.agents-shared.js";
1717
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
1818
import { detectWindowsSpawnCommandInlineArgs } from "../../../plugin-sdk/windows-spawn.js";
19+
import { resolvePluginSlotOwner } from "../../../plugins/slots.js";
1920
import { normalizeAgentId } from "../../../routing/session-key.js";
2021

2122
type CodexRouteHit = {
@@ -1989,7 +1990,7 @@ function maybeMigrateLegacyLosslessCompactionConfig(params: {
19891990
entries[LOSSLESS_CONTEXT_ENGINE_ID] = entry;
19901991
}
19911992
const config = ensureMutablePath(entry, ["config"]);
1992-
if (slots.contextEngine !== LOSSLESS_CONTEXT_ENGINE_ID) {
1993+
if (resolvePluginSlotOwner(slots.contextEngine) !== LOSSLESS_CONTEXT_ENGINE_ID) {
19931994
slots.contextEngine = LOSSLESS_CONTEXT_ENGINE_ID;
19941995
changes.push(
19951996
`Set plugins.slots.contextEngine to "${LOSSLESS_CONTEXT_ENGINE_ID}" for legacy Lossless compaction config.`,

src/commands/doctor/shared/context-engine-host-compat.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function registerEngine(requiredCapabilities: ContextEngineHostCapability[]): st
4545
return id;
4646
}
4747

48-
function configWithEngine(engineId: string, cfg: OpenClawConfig = {}): OpenClawConfig {
48+
function configWithEngine(engineId: unknown, cfg: OpenClawConfig = {}): OpenClawConfig {
4949
return {
5050
...cfg,
5151
plugins: {
@@ -55,7 +55,7 @@ function configWithEngine(engineId: string, cfg: OpenClawConfig = {}): OpenClawC
5555
contextEngine: engineId,
5656
},
5757
},
58-
};
58+
} as OpenClawConfig;
5959
}
6060

6161
describe("doctor context-engine host compatibility", () => {
@@ -120,6 +120,31 @@ describe("doctor context-engine host compatibility", () => {
120120
]);
121121
});
122122

123+
it("resolves object-form context engine slot owners before host compatibility checks", async () => {
124+
const engineId = registerEngine(["assemble-before-prompt"]);
125+
const result = await maybeRepairContextEngineHostCompatibility({
126+
cfg: configWithEngine(
127+
{ owner: engineId, claimed_by_version: "0.9.10" },
128+
{
129+
agents: {
130+
defaults: {
131+
model: "anthropic/claude-sonnet-4-6",
132+
models: {
133+
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } },
134+
},
135+
},
136+
},
137+
},
138+
),
139+
doctorFixCommand: "openclaw doctor --fix",
140+
});
141+
142+
expect(result.config.plugins?.slots?.contextEngine).toBe("legacy");
143+
expect(result.changes).toEqual([
144+
`Set plugins.slots.contextEngine to "legacy" because context engine "${engineId}" is incompatible with every configured agent-run host.`,
145+
]);
146+
});
147+
123148
it("leaves compatible native runtimes unchanged", async () => {
124149
const engineId = registerEngine(["assemble-before-prompt", "runtime-llm-complete"]);
125150
const cfg = configWithEngine(engineId, {

src/commands/doctor/shared/context-engine-host-compat.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { ensureContextEnginesInitialized } from "../../../context-engine/init.js
1818
import { getContextEngineFactory, resolveContextEngine } from "../../../context-engine/registry.js";
1919
import type { ContextEngineInfo } from "../../../context-engine/types.js";
2020
import { ensurePluginRegistryLoaded } from "../../../plugins/runtime/runtime-registry-loader.js";
21-
import { defaultSlotIdForKey } from "../../../plugins/slots.js";
21+
import { defaultSlotIdForKey, resolvePluginSlotOwner } from "../../../plugins/slots.js";
2222
import { isRecord, resolveUserPath } from "../../../utils.js";
2323

2424
export type HostCandidate = {
@@ -233,10 +233,10 @@ export function collectConfiguredContextEngineAgentRunHosts(params: {
233233
}
234234

235235
function selectedContextEngineSlotId(cfg: OpenClawConfig): string {
236-
const slotValue = cfg.plugins?.slots?.contextEngine;
237-
return typeof slotValue === "string" && slotValue.trim()
238-
? slotValue.trim()
239-
: defaultSlotIdForKey("contextEngine");
236+
return (
237+
resolvePluginSlotOwner(cfg.plugins?.slots?.contextEngine) ??
238+
defaultSlotIdForKey("contextEngine")
239+
);
240240
}
241241

242242
async function resolveSelectedContextEngineInfo(params: {

src/commands/doctor/shared/stale-plugin-config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js";
55
import { normalizePluginId } from "../../../plugins/config-state.js";
66
import { loadInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-records.js";
77
import { loadManifestMetadataSnapshot } from "../../../plugins/manifest-contract-eligibility.js";
8-
import { defaultSlotIdForKey, type PluginSlotKey } from "../../../plugins/slots.js";
8+
import {
9+
defaultSlotIdForKey,
10+
resolvePluginSlotOwner,
11+
type PluginSlotKey,
12+
} from "../../../plugins/slots.js";
913
import { asObjectRecord } from "./object.js";
1014

1115
const CHANNEL_CONFIG_META_KEYS = new Set(["defaults", "modelByChannel"]);
@@ -160,7 +164,7 @@ function scanStalePluginConfigWithState(
160164
const slots = asObjectRecord(plugins?.slots);
161165
if (slots) {
162166
for (const slotKey of ["memory", "contextEngine"] as const satisfies readonly PluginSlotKey[]) {
163-
const rawPluginId = slots[slotKey];
167+
const rawPluginId = resolvePluginSlotOwner(slots[slotKey]);
164168
if (typeof rawPluginId !== "string") {
165169
continue;
166170
}

0 commit comments

Comments
 (0)