mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:05:01 +00:00
fix(skills): scope skill-command APIs to respect agent allowlists (#32155)
* refactor(skills): use explicit skill-command scope APIs * test(skills): cover scoped listing and telegram allowlist * fix(skills): add mergeSkillFilters edge-case tests and simplify dead code Cover unrestricted-co-tenant and empty-allowlist merge paths in skill-commands tests. Remove dead ternary in bot-handlers pagination. Add clarifying comments on undefined vs [] filter semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(skills): collapse scope functions into single listSkillCommandsForAgents Replace listSkillCommandsForAgentIds, listSkillCommandsForAllAgents, and the deprecated listSkillCommandsForAgents with a single function that accepts optional agentIds and falls back to all agents when omitted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(skills): harden realpathSync race and add missing test coverage - Wrap fs.realpathSync in try-catch to gracefully skip workspaces that disappear between existsSync and realpathSync (TOCTOU race). - Log verbose diagnostics for missing/unresolvable workspace paths. - Add test for overlapping allowlists deduplication on shared workspaces. - Add test for graceful skip of missing workspaces. - Add test for pagination callback without agent suffix (default agent). - Clean up temp directories in skill-commands tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(telegram): warn when nativeSkillsEnabled but no agent route is bound Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use runtime.log instead of nonexistent runtime.warn Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import { listAgentIds, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import {
|
||||
listAgentIds,
|
||||
resolveAgentSkillsFilter,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agents/skills.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||
import { listChatCommands } from "./commands-registry.js";
|
||||
|
||||
@@ -45,25 +50,57 @@ export function listSkillCommandsForAgents(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentIds?: string[];
|
||||
}): SkillCommandSpec[] {
|
||||
const mergeSkillFilters = (existing?: string[], incoming?: string[]): string[] | undefined => {
|
||||
// undefined = no allowlist (unrestricted); [] = explicit empty allowlist (no skills).
|
||||
// If any agent is unrestricted for this workspace, keep command discovery unrestricted.
|
||||
if (existing === undefined || incoming === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// An empty allowlist contributes no skills but does not widen the merge to unrestricted.
|
||||
if (existing.length === 0) {
|
||||
return Array.from(new Set(incoming));
|
||||
}
|
||||
if (incoming.length === 0) {
|
||||
return Array.from(new Set(existing));
|
||||
}
|
||||
return Array.from(new Set([...existing, ...incoming]));
|
||||
};
|
||||
|
||||
const agentIds = params.agentIds ?? listAgentIds(params.cfg);
|
||||
const used = listReservedChatSlashCommandNames();
|
||||
const entries: SkillCommandSpec[] = [];
|
||||
const agentIds = params.agentIds ?? listAgentIds(params.cfg);
|
||||
// Track visited workspace dirs to avoid registering duplicate commands
|
||||
// when multiple agents share the same workspace directory (#5717).
|
||||
const visitedDirs = new Set<string>();
|
||||
// Group by canonical workspace to avoid duplicate registration when multiple
|
||||
// agents share the same directory (#5717), while still honoring per-agent filters.
|
||||
const workspaceFilters = new Map<string, { workspaceDir: string; skillFilter?: string[] }>();
|
||||
for (const agentId of agentIds) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
|
||||
if (!fs.existsSync(workspaceDir)) {
|
||||
logVerbose(`Skipping agent "${agentId}": workspace does not exist: ${workspaceDir}`);
|
||||
continue;
|
||||
}
|
||||
// Resolve to canonical path to handle symlinks and relative paths
|
||||
const canonicalDir = fs.realpathSync(workspaceDir);
|
||||
if (visitedDirs.has(canonicalDir)) {
|
||||
let canonicalDir: string;
|
||||
try {
|
||||
canonicalDir = fs.realpathSync(workspaceDir);
|
||||
} catch {
|
||||
logVerbose(`Skipping agent "${agentId}": cannot resolve workspace: ${workspaceDir}`);
|
||||
continue;
|
||||
}
|
||||
visitedDirs.add(canonicalDir);
|
||||
const skillFilter = resolveAgentSkillsFilter(params.cfg, agentId);
|
||||
const existing = workspaceFilters.get(canonicalDir);
|
||||
if (existing) {
|
||||
existing.skillFilter = mergeSkillFilters(existing.skillFilter, skillFilter);
|
||||
continue;
|
||||
}
|
||||
workspaceFilters.set(canonicalDir, {
|
||||
workspaceDir,
|
||||
skillFilter,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { workspaceDir, skillFilter } of workspaceFilters.values()) {
|
||||
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
|
||||
config: params.cfg,
|
||||
skillFilter,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
reservedNames: used,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user