Files
openclaw/src/plugins/config-contracts.ts
T
Peter Steinberger 1ca60fbc3a refactor(agents): make multi-agent ownership explicit (H2-1 core) (#114388)
* refactor(agents): make roster ownership explicit

* feat(config): materialize legacy agent roles

* fix(cron): migrate legacy owners at startup

* feat(gateway): expose agent selection contracts

* fix(gateway): enforce agent-scoped authorization

* docs(config): document explicit agent ownership

* fix(config): pin retained owner workspace

* fix(gateway): target hook wakes at effective agent

* fix(sessions): preserve fixed-store ownership

* fix: preserve retained agent ownership

* fix: preserve legacy agent ownership across runtime surfaces

* fix: fail closed on ambiguous session ownership

* fix: preserve compatibility owners across dispatch and writes

* fix: preserve retained agent projections

* fix: preserve agent ownership compatibility

* fix: preserve per-agent heartbeat guidance

* fix: preserve compatibility owners in generic paths

* fix: enforce configured ownership in session paths

* fix: defer remote roster selection

* fix: preserve ownership across session and config writes

* fix: fail closed on ambiguous restored ownership

* fix: preserve explicit ACP and legacy ownership

* fix: honor durable fixed-store ownership

* fix: enforce fixed-store owner authority

* fix: preserve ownership evidence boundaries

* fix: honor resolved session ownership

* fix: align compatibility ownership paths

* fix: persist legacy main store ownership

* fix: close ownership fallback gaps

* fix(agents): close retained owner compatibility gaps

* fix(agents): enforce session owner resolution

* fix(agents): complete session owner resolution sweep

* fix(agents): preserve durable session ownership

* fix: complete persisted session owner routing

* fix: thread prepared session owners

* fix: preserve stable session ownership

* fix: enforce session ownership boundaries

* fix: close session ownership delta gaps

* fix: reconcile session ownership after rebase

* fix: reconcile ownership with current main

* fix: align session store path imports

* fix: align session store config path import

* fix: reconcile explicit ownership CI

* fix: reconcile ownership rebase checks

* fix: align ownership ci contracts

* fix: align ownership rebase checks

* fix: preserve compatibility owner during setup

* fix(doctor): migrate ownerless heartbeat monitors

* fix(gateway): preserve explicit session ownership

* test: align ownership fixtures after rebase

* test: complete plugin manifest fixture

* test: align runtime context mocks

* fix(gateway): preserve alias routing for existing sessions

* style: format agent routing update

* fix(gateway): preserve selected owner during alias routing

* style: normalize rebased ownership files

* fix(gateway): preserve owner through global alias routing

* fix(gateway): preserve explicit ownership at HTTP boundaries

* fix(gateway): validate compatibility model ownership

* fix(agents): reconcile strict session ownership

* fix(agents): contain media yield callback failures

* fix(agents): avoid eager bare-key owner resolution

* chore: refresh rebased ownership baselines

* chore: align hosted plugin SDK baseline

* chore: refresh ownership baselines after main sync

* chore: refresh ownership baselines after main sync

* test: align routed event owner fixtures

* chore: retrigger CI after runner startup failure

* chore: refresh ownership SDK budgets after main sync

* fix(tasks): require agent identity for bare owners

* chore: align Linux plugin SDK baseline

* chore: remove release-owned changelog entry
2026-08-12 15:55:16 -07:00

158 lines
6.0 KiB
TypeScript

/** Resolves plugin config contract metadata for scanners and secret/config policy checks. */
import { normalizeSortedUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { findBundledPluginMetadataById } from "./bundled-plugin-metadata.js";
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
import {
loadPluginManifestRegistryCore,
type PluginManifestRegistry,
} from "./manifest-registry.js";
import type { PluginManifestConfigContracts } from "./manifest.js";
import type { PluginOrigin } from "./plugin-origin.types.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
export { collectPluginConfigContractMatches } from "./config-contract-matches.js";
type PluginConfigContractMetadata = {
/** Runtime origin that supplied the contract metadata. */
origin: PluginOrigin;
/** Manifest-declared config contract paths used by secret/security/config scanners. */
configContracts: PluginManifestConfigContracts;
};
/** Resolve config contract metadata for plugin ids through the runtime registry and bundled fallback. */
export function resolvePluginConfigContractsById(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
fallbackToBundledMetadata?: boolean;
fallbackToBundledMetadataForResolvedBundled?: boolean;
fallbackBundledPluginIds?: readonly string[];
pluginIds: readonly string[];
discovery?: PluginDiscoveryResult;
manifestRegistry?: Pick<PluginManifestRegistry, "plugins">;
}): ReadonlyMap<string, PluginConfigContractMetadata> {
const matches = new Map<string, PluginConfigContractMetadata>();
const pluginIds = normalizeSortedUniqueStringEntries(params.pluginIds);
if (pluginIds.length === 0) {
return matches;
}
const fallbackBundledPluginIds = new Set(
normalizeSortedUniqueStringEntries(params.fallbackBundledPluginIds),
);
const bundledContractFallbacks = new Map<string, PluginManifestConfigContracts | undefined>();
const findBundledConfigContracts = (
pluginId: string,
): PluginManifestConfigContracts | undefined => {
if (bundledContractFallbacks.has(pluginId)) {
return bundledContractFallbacks.get(pluginId);
}
const discovery =
params.discovery ??
discoverOpenClawPlugins({
workspaceDir: params.workspaceDir,
env: params.env,
});
const registry = loadPluginManifestRegistryCore({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
candidates: discovery.candidates.filter((candidate) => candidate.origin === "bundled"),
diagnostics: discovery.diagnostics,
});
for (const plugin of registry.plugins) {
bundledContractFallbacks.set(plugin.id, plugin.configContracts);
}
if (bundledContractFallbacks.get(pluginId) === undefined) {
const bundledMetadata = findBundledPluginMetadataById(pluginId, {
includeChannelConfigs: false,
includeSyntheticChannelConfigs: false,
});
if (bundledMetadata?.manifest.configContracts) {
bundledContractFallbacks.set(pluginId, bundledMetadata.manifest.configContracts);
}
}
if (!bundledContractFallbacks.has(pluginId)) {
bundledContractFallbacks.set(pluginId, undefined);
}
return bundledContractFallbacks.get(pluginId);
};
const resolvedPluginOrigins = new Map<string, PluginOrigin>();
const registry =
params.manifestRegistry ??
loadPluginManifestRegistryForPluginRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
includeDisabled: true,
});
for (const plugin of registry.plugins) {
if (!pluginIds.includes(plugin.id)) {
continue;
}
resolvedPluginOrigins.set(plugin.id, plugin.origin);
if (!plugin.configContracts) {
continue;
}
matches.set(plugin.id, {
origin: plugin.origin,
configContracts: plugin.configContracts,
});
}
if (params.fallbackToBundledMetadata ?? true) {
for (const pluginId of pluginIds) {
const existing = matches.get(pluginId);
const shouldHydrateBundledMatch =
existing &&
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
(!params.manifestRegistry && fallbackBundledPluginIds.has(pluginId)));
if (shouldHydrateBundledMatch) {
const bundledConfigContracts = findBundledConfigContracts(pluginId);
if (bundledConfigContracts) {
// Bundled metadata can carry richer contract declarations than installed registry entries;
// installed declarations still win except for bundled secret input coverage.
matches.set(pluginId, {
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
configContracts: {
...bundledConfigContracts,
...existing.configContracts,
...(bundledConfigContracts.secretInputs
? { secretInputs: bundledConfigContracts.secretInputs }
: {}),
},
});
}
continue;
}
if (matches.has(pluginId)) {
continue;
}
const resolvedOrigin = resolvedPluginOrigins.get(pluginId);
if (
resolvedOrigin &&
!(params.fallbackToBundledMetadataForResolvedBundled && resolvedOrigin === "bundled") &&
!fallbackBundledPluginIds.has(pluginId)
) {
continue;
}
if (params.manifestRegistry && resolvedOrigin && resolvedOrigin !== "bundled") {
continue;
}
if (params.manifestRegistry && !fallbackBundledPluginIds.has(pluginId)) {
continue;
}
const bundledConfigContracts = findBundledConfigContracts(pluginId);
if (!bundledConfigContracts) {
continue;
}
matches.set(pluginId, {
origin: "bundled",
configContracts: bundledConfigContracts,
});
}
}
return matches;
}