mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 04:31:10 +00:00
fix: clarify exec node routing guidance
This commit is contained in:
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) Thanks @eleqtrizit and @pgondhi987.
|
||||
- Auto-reply: unify reply lifecycle ownership across preflight compaction, session rotation, CLI-backed runs, and gateway restart handling so `/stop` and same-session overlap checks target the right active turn and restart-interrupted turns return the restart notice instead of being silently dropped. (#61267) Thanks @dutifulbob.
|
||||
- Exec/remote skills: stop advertising `exec host=node` when the current exec policy cannot route to a node, and clarify blocked exec-host override errors with both the requested host and allowed config path.
|
||||
- Agents/Claude CLI/security: clear inherited Claude Code config-root and plugin-root env overrides like `CLAUDE_CONFIG_DIR` and `CLAUDE_CODE_PLUGIN_*`, so OpenClaw-launched Claude CLI runs cannot be silently pointed at an alternate Claude config/plugin tree with different hooks, plugins, or auth context. Thanks @vincentkoc.
|
||||
- Agents/Claude CLI/security: clear inherited Claude Code provider-routing and managed-auth env overrides, and mark OpenClaw-launched Claude CLI runs as host-managed, so Claude CLI backdoor sessions cannot be silently redirected to proxy, Bedrock, Vertex, Foundry, or parent-managed token contexts. Thanks @vincentkoc.
|
||||
- Agents/Claude CLI/security: force host-managed Claude CLI backdoor runs to `--setting-sources user`, even under custom backend arg overrides, so repo-local `.claude` project/local settings, hooks, and plugin discovery do not silently execute inside non-interactive OpenClaw sessions. Thanks @vincentkoc.
|
||||
|
||||
@@ -65,6 +65,7 @@ import { updateSessionStoreAfterAgentRun } from "./command/session-store.js";
|
||||
import { resolveSession } from "./command/session.js";
|
||||
import type { AgentCommandIngressOpts, AgentCommandOpts } from "./command/types.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import { canExecRequestNode } from "./exec-defaults.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
||||
import { LiveSessionModelSwitchError } from "./live-model-switch.js";
|
||||
import { loadModelCatalog } from "./model-catalog.js";
|
||||
@@ -508,7 +509,16 @@ async function agentCommandInternal(
|
||||
const skillsSnapshot = needsSkillsSnapshot
|
||||
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
config: cfg,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
agentId: sessionAgentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
snapshotVersion: skillsSnapshotVersion,
|
||||
skillFilter,
|
||||
agentId: sessionAgentId,
|
||||
|
||||
@@ -126,7 +126,9 @@ describe("resolveExecTarget", () => {
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: true,
|
||||
}),
|
||||
).toThrow("exec host not allowed");
|
||||
).toThrow(
|
||||
"exec host not allowed (requested gateway; configured host is auto; set tools.exec.host=gateway or auto to allow this override).",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows per-call host=sandbox override when configured host is auto", () => {
|
||||
@@ -153,7 +155,9 @@ describe("resolveExecTarget", () => {
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toThrow("exec host not allowed");
|
||||
).toThrow(
|
||||
"exec host not allowed (requested gateway; configured host is node; set tools.exec.host=gateway or auto to allow this override).",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows explicit auto request when configured host is auto", () => {
|
||||
@@ -180,7 +184,9 @@ describe("resolveExecTarget", () => {
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: true,
|
||||
}),
|
||||
).toThrow("exec host not allowed");
|
||||
).toThrow(
|
||||
"exec host not allowed (requested auto; configured host is gateway; set tools.exec.host=auto to allow this override).",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows exact node matches", () => {
|
||||
|
||||
@@ -259,9 +259,17 @@ export function resolveExecTarget(params: {
|
||||
sandboxAvailable: params.sandboxAvailable,
|
||||
})
|
||||
) {
|
||||
const allowedConfig = Array.from(
|
||||
new Set(
|
||||
requestedTarget === "gateway" && !params.sandboxAvailable
|
||||
? ["gateway", "auto"]
|
||||
: [renderExecTargetLabel(requestedTarget), "auto"],
|
||||
),
|
||||
).join(" or ");
|
||||
throw new Error(
|
||||
`exec host not allowed (requested ${renderExecTargetLabel(requestedTarget)}; ` +
|
||||
`configure tools.exec.host=${renderExecTargetLabel(requestedTarget)} to allow).`,
|
||||
`configured host is ${renderExecTargetLabel(configuredTarget)}; ` +
|
||||
`set tools.exec.host=${allowedConfig} to allow this override).`,
|
||||
);
|
||||
}
|
||||
const selectedTarget = requestedTarget ?? configuredTarget;
|
||||
|
||||
58
src/agents/exec-defaults.test.ts
Normal file
58
src/agents/exec-defaults.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { resolveExecDefaults } from "./exec-defaults.js";
|
||||
|
||||
describe("resolveExecDefaults", () => {
|
||||
it("does not advertise node routing when exec host is pinned to gateway", () => {
|
||||
expect(
|
||||
resolveExecDefaults({
|
||||
cfg: {
|
||||
tools: {
|
||||
exec: {
|
||||
host: "gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxAvailable: false,
|
||||
}).canRequestNode,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps node routing available when exec host is auto", () => {
|
||||
expect(
|
||||
resolveExecDefaults({
|
||||
cfg: {
|
||||
tools: {
|
||||
exec: {
|
||||
host: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxAvailable: true,
|
||||
}),
|
||||
).toMatchObject({
|
||||
host: "auto",
|
||||
effectiveHost: "sandbox",
|
||||
canRequestNode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("honors session-level exec host overrides", () => {
|
||||
const sessionEntry = {
|
||||
execHost: "node",
|
||||
} as SessionEntry;
|
||||
expect(
|
||||
resolveExecDefaults({
|
||||
cfg: {
|
||||
tools: {
|
||||
exec: {
|
||||
host: "gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionEntry,
|
||||
sandboxAvailable: false,
|
||||
}).canRequestNode,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
110
src/agents/exec-defaults.ts
Normal file
110
src/agents/exec-defaults.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import type { ExecAsk, ExecHost, ExecSecurity, ExecTarget } from "../infra/exec-approvals.js";
|
||||
import { resolveAgentConfig, resolveSessionAgentId } from "./agent-scope.js";
|
||||
import { isRequestedExecTargetAllowed, resolveExecTarget } from "./bash-tools.exec-runtime.js";
|
||||
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
|
||||
|
||||
type ResolvedExecConfig = {
|
||||
host?: ExecTarget;
|
||||
security?: ExecSecurity;
|
||||
ask?: ExecAsk;
|
||||
node?: string;
|
||||
};
|
||||
|
||||
function resolveExecConfigState(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
}): {
|
||||
cfg: OpenClawConfig;
|
||||
host: ExecTarget;
|
||||
agentExec?: ResolvedExecConfig;
|
||||
globalExec?: ResolvedExecConfig;
|
||||
} {
|
||||
const cfg = params.cfg ?? {};
|
||||
const resolvedAgentId =
|
||||
params.agentId ??
|
||||
resolveSessionAgentId({
|
||||
sessionKey: params.sessionKey,
|
||||
config: cfg,
|
||||
});
|
||||
const globalExec = cfg.tools?.exec;
|
||||
const agentExec = resolvedAgentId
|
||||
? resolveAgentConfig(cfg, resolvedAgentId)?.tools?.exec
|
||||
: undefined;
|
||||
const host =
|
||||
(params.sessionEntry?.execHost as ExecTarget | undefined) ??
|
||||
(agentExec?.host as ExecTarget | undefined) ??
|
||||
(globalExec?.host as ExecTarget | undefined) ??
|
||||
"auto";
|
||||
return {
|
||||
cfg,
|
||||
host,
|
||||
agentExec,
|
||||
globalExec,
|
||||
};
|
||||
}
|
||||
|
||||
export function canExecRequestNode(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
}): boolean {
|
||||
const { host } = resolveExecConfigState(params);
|
||||
return isRequestedExecTargetAllowed({
|
||||
configuredTarget: host,
|
||||
requestedTarget: "node",
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveExecDefaults(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
sandboxAvailable?: boolean;
|
||||
}): {
|
||||
host: ExecTarget;
|
||||
effectiveHost: ExecHost;
|
||||
security: ExecSecurity;
|
||||
ask: ExecAsk;
|
||||
node?: string;
|
||||
canRequestNode: boolean;
|
||||
} {
|
||||
const { cfg, host, agentExec, globalExec } = resolveExecConfigState(params);
|
||||
const sandboxAvailable =
|
||||
params.sandboxAvailable ??
|
||||
(params.sessionKey
|
||||
? resolveSandboxRuntimeStatus({
|
||||
cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
}).sandboxed
|
||||
: false);
|
||||
const resolved = resolveExecTarget({
|
||||
configuredTarget: host,
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable,
|
||||
});
|
||||
return {
|
||||
host,
|
||||
effectiveHost: resolved.effectiveHost,
|
||||
security:
|
||||
(params.sessionEntry?.execSecurity as ExecSecurity | undefined) ??
|
||||
agentExec?.security ??
|
||||
globalExec?.security ??
|
||||
"deny",
|
||||
ask:
|
||||
(params.sessionEntry?.execAsk as ExecAsk | undefined) ??
|
||||
agentExec?.ask ??
|
||||
globalExec?.ask ??
|
||||
"on-miss",
|
||||
node: params.sessionEntry?.execNode ?? agentExec?.node ?? globalExec?.node,
|
||||
canRequestNode: isRequestedExecTargetAllowed({
|
||||
configuredTarget: host,
|
||||
requestedTarget: "node",
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { DEFAULT_BROWSER_EVALUATE_ENABLED } from "../../plugin-sdk/browser-profiles.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { canExecRequestNode } from "../exec-defaults.js";
|
||||
import { syncSkillsToWorkspace } from "../skills.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
|
||||
import { requireSandboxBackendFactory } from "./backend.js";
|
||||
@@ -58,7 +59,15 @@ async function ensureSandboxWorkspaceLayout(params: {
|
||||
targetWorkspaceDir: sandboxWorkspaceDir,
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: params.config,
|
||||
sessionKey: rawSessionKey,
|
||||
agentId: params.agentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock("../../agents/skills/refresh.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentConfig: vi.fn(() => undefined),
|
||||
resolveSessionAgentIds: vi.fn(() => ({ sessionAgentId: "main" })),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
|
||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||
import { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
||||
import type { EmbeddedContextFile } from "../../agents/pi-embedded-helpers.js";
|
||||
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
||||
@@ -38,12 +39,25 @@ export async function resolveCommandsSystemPromptBundle(
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionEntry?.sessionId,
|
||||
});
|
||||
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
|
||||
});
|
||||
const skillsSnapshot = (() => {
|
||||
try {
|
||||
return buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
config: params.cfg,
|
||||
agentId: sessionAgentId,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: params.cfg,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionKey: params.sessionKey,
|
||||
agentId: sessionAgentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
snapshotVersion: getSkillsSnapshotVersion(workspaceDir),
|
||||
});
|
||||
} catch {
|
||||
@@ -51,10 +65,6 @@ export async function resolveCommandsSystemPromptBundle(
|
||||
}
|
||||
})();
|
||||
const skillsPrompt = skillsSnapshot.prompt ?? "";
|
||||
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
|
||||
});
|
||||
const tools = (() => {
|
||||
try {
|
||||
return createOpenClawCodingTools({
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentDir,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { renderExecTargetLabel, resolveExecTarget } from "../../agents/bash-tools.exec-runtime.js";
|
||||
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { renderExecTargetLabel } from "../../agents/bash-tools.exec-runtime.js";
|
||||
import { resolveExecDefaults } from "../../agents/exec-defaults.js";
|
||||
import { resolveFastModeState } from "../../agents/fast-mode.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
import type { ExecAsk, ExecHost, ExecSecurity, ExecTarget } from "../../infra/exec-approvals.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
@@ -33,49 +28,6 @@ import {
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel } from "./directives.js";
|
||||
import { refreshQueuedFollowupSession } from "./queue.js";
|
||||
|
||||
function resolveExecDefaults(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionEntry?: SessionEntry;
|
||||
agentId?: string;
|
||||
sandboxAvailable: boolean;
|
||||
}): {
|
||||
host: ExecTarget;
|
||||
effectiveHost: ExecHost;
|
||||
security: ExecSecurity;
|
||||
ask: ExecAsk;
|
||||
node?: string;
|
||||
} {
|
||||
const globalExec = params.cfg.tools?.exec;
|
||||
const agentExec = params.agentId
|
||||
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.exec
|
||||
: undefined;
|
||||
const host =
|
||||
(params.sessionEntry?.execHost as ExecTarget | undefined) ??
|
||||
(agentExec?.host as ExecTarget | undefined) ??
|
||||
(globalExec?.host as ExecTarget | undefined) ??
|
||||
"auto";
|
||||
const resolved = resolveExecTarget({
|
||||
configuredTarget: host,
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: params.sandboxAvailable,
|
||||
});
|
||||
return {
|
||||
host,
|
||||
effectiveHost: resolved.effectiveHost,
|
||||
security:
|
||||
(params.sessionEntry?.execSecurity as ExecSecurity | undefined) ??
|
||||
(agentExec?.security as ExecSecurity | undefined) ??
|
||||
(globalExec?.security as ExecSecurity | undefined) ??
|
||||
"deny",
|
||||
ask:
|
||||
(params.sessionEntry?.execAsk as ExecAsk | undefined) ??
|
||||
(agentExec?.ask as ExecAsk | undefined) ??
|
||||
(globalExec?.ask as ExecAsk | undefined) ??
|
||||
"on-miss",
|
||||
node: params.sessionEntry?.execNode ?? agentExec?.node ?? globalExec?.node,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleDirectiveOnly(
|
||||
params: HandleDirectiveOnlyParams,
|
||||
): Promise<ReplyPayload | undefined> {
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
getSkillsSnapshotVersionMock,
|
||||
shouldRefreshSnapshotForVersionMock,
|
||||
getRemoteSkillEligibilityMock,
|
||||
resolveAgentConfigMock,
|
||||
resolveSessionAgentIdMock,
|
||||
resolveAgentIdFromSessionKeyMock,
|
||||
} = vi.hoisted(() => ({
|
||||
@@ -18,11 +19,13 @@ const {
|
||||
hasBin: () => false,
|
||||
hasAnyBin: () => false,
|
||||
})),
|
||||
resolveAgentConfigMock: vi.fn(() => undefined),
|
||||
resolveSessionAgentIdMock: vi.fn(() => "writer"),
|
||||
resolveAgentIdFromSessionKeyMock: vi.fn(() => "main"),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentConfig: resolveAgentConfigMock,
|
||||
resolveSessionAgentId: resolveSessionAgentIdMock,
|
||||
}));
|
||||
|
||||
@@ -63,6 +66,7 @@ describe("ensureSkillSnapshot", () => {
|
||||
hasBin: () => false,
|
||||
hasAnyBin: () => false,
|
||||
});
|
||||
resolveAgentConfigMock.mockReturnValue(undefined);
|
||||
resolveSessionAgentIdMock.mockReturnValue("writer");
|
||||
resolveAgentIdFromSessionKeyMock.mockReturnValue("main");
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { matchesSkillFilter } from "../../agents/skills/filter.js";
|
||||
import {
|
||||
@@ -131,9 +132,16 @@ export async function ensureSkillSnapshot(params: {
|
||||
|
||||
let nextEntry = sessionEntry;
|
||||
let systemSent = sessionEntry?.systemSent ?? false;
|
||||
const remoteEligibility = getRemoteSkillEligibility();
|
||||
const snapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
||||
const sessionAgentId = resolveSessionAgentId({ sessionKey, config: cfg });
|
||||
const remoteEligibility = getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
agentId: sessionAgentId,
|
||||
}),
|
||||
});
|
||||
const snapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
||||
const existingSnapshot = nextEntry?.skillsSnapshot;
|
||||
ensureSkillsWatcher({ workspaceDir, config: cfg });
|
||||
const shouldRefreshSnapshot =
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveAgentSkillsFilter,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { canExecRequestNode } from "../agents/exec-defaults.js";
|
||||
import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agents/skills.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
@@ -24,7 +25,14 @@ export function listSkillCommandsForWorkspace(params: {
|
||||
config: params.cfg,
|
||||
agentId: params.agentId,
|
||||
skillFilter: params.skillFilter,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
reservedNames: listReservedChatSlashCommandNames(),
|
||||
});
|
||||
}
|
||||
@@ -100,7 +108,13 @@ export function listSkillCommandsForAgents(params: {
|
||||
const commands = buildWorkspaceSkillCommandSpecs(workspaceDir, {
|
||||
config: params.cfg,
|
||||
skillFilter,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: params.cfg,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
reservedNames: used,
|
||||
});
|
||||
for (const command of commands) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { canExecRequestNode } from "../agents/exec-defaults.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
@@ -247,7 +248,14 @@ export async function statusAllCommand(
|
||||
try {
|
||||
return buildWorkspaceSkillStatus(defaultWorkspace, {
|
||||
config: cfg,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg,
|
||||
agentId: agentStatus.defaultId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
import type { SkillSnapshot } from "../../agents/skills.js";
|
||||
import { matchesSkillFilter } from "../../agents/skills/filter.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -35,7 +36,14 @@ export function resolveCronSkillsSnapshot(params: {
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
skillFilter,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: params.config,
|
||||
agentId: params.agentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
snapshotVersion,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
import {
|
||||
installSkillFromClawHub,
|
||||
searchSkillsFromClawHub,
|
||||
@@ -92,7 +93,14 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
||||
config: cfg,
|
||||
eligibility: { remote: getRemoteSkillEligibility() },
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg,
|
||||
agentId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
respond(true, report, undefined);
|
||||
},
|
||||
|
||||
@@ -113,4 +113,25 @@ describe("skills-remote", () => {
|
||||
removeRemoteNodeInfo(nodeB);
|
||||
}
|
||||
});
|
||||
|
||||
it("suppresses the exec host=node note when routing is not allowed", () => {
|
||||
const nodeId = `node-${randomUUID()}`;
|
||||
const bin = `bin-${randomUUID()}`;
|
||||
try {
|
||||
recordRemoteNodeInfo({
|
||||
nodeId,
|
||||
displayName: "Mac Studio",
|
||||
platform: "darwin",
|
||||
commands: ["system.run"],
|
||||
});
|
||||
recordRemoteNodeBins(nodeId, [bin]);
|
||||
|
||||
const eligibility = getRemoteSkillEligibility({ advertiseExecNode: false });
|
||||
|
||||
expect(eligibility?.hasBin(bin)).toBe(true);
|
||||
expect(eligibility?.note).toBeUndefined();
|
||||
} finally {
|
||||
removeRemoteNodeInfo(nodeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -316,7 +316,9 @@ export async function refreshRemoteNodeBins(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteSkillEligibility(): SkillEligibilityContext["remote"] | undefined {
|
||||
export function getRemoteSkillEligibility(options?: {
|
||||
advertiseExecNode?: boolean;
|
||||
}): SkillEligibilityContext["remote"] | undefined {
|
||||
const macNodes = [...remoteNodes.values()].filter(
|
||||
(node) => isMacPlatform(node.platform, node.deviceFamily) && supportsSystemRun(node.commands),
|
||||
);
|
||||
@@ -331,14 +333,16 @@ export function getRemoteSkillEligibility(): SkillEligibilityContext["remote"] |
|
||||
}
|
||||
const labels = macNodes.map((node) => node.displayName ?? node.nodeId).filter(Boolean);
|
||||
const note =
|
||||
labels.length > 0
|
||||
? `Remote macOS node available (${labels.join(", ")}). Run macOS-only skills via exec host=node on that node.`
|
||||
: "Remote macOS node available. Run macOS-only skills via exec host=node on that node.";
|
||||
options?.advertiseExecNode === false
|
||||
? undefined
|
||||
: labels.length > 0
|
||||
? `Remote macOS node available (${labels.join(", ")}). Run macOS-only skills via exec host=node on that node.`
|
||||
: "Remote macOS node available. Run macOS-only skills via exec host=node on that node.";
|
||||
return {
|
||||
platforms: ["darwin"],
|
||||
hasBin: (bin) => bins.has(bin),
|
||||
hasAnyBin: (required) => required.some((bin) => bins.has(bin)),
|
||||
note,
|
||||
...(note ? { note } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user