mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-28 03:13:36 +00:00
fix(gateway): preserve owner MCP tools for agent RPC
This commit is contained in:
@@ -10,6 +10,12 @@ import {
|
||||
resolveClaudeCliExecutionArgs,
|
||||
} from "./cli-shared.js";
|
||||
|
||||
function expectDefaultDisallowedTools(args: readonly string[] | undefined) {
|
||||
const disallowedIndex = args?.indexOf("--disallowedTools") ?? -1;
|
||||
expect(disallowedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(args?.[disallowedIndex + 1]).toBe("ScheduleWakeup,CronCreate");
|
||||
}
|
||||
|
||||
describe("normalizeClaudePermissionArgs", () => {
|
||||
it("leaves args alone when they omit permission flags", () => {
|
||||
expect(
|
||||
@@ -356,8 +362,10 @@ describe("normalizeClaudeBackendConfig", () => {
|
||||
expect(backend.config.input).toBe("stdin");
|
||||
expect(backend.config.args).toContain("--setting-sources");
|
||||
expect(backend.config.args).toContain("user");
|
||||
expectDefaultDisallowedTools(backend.config.args);
|
||||
expect(backend.config.resumeArgs).toContain("--setting-sources");
|
||||
expect(backend.config.resumeArgs).toContain("user");
|
||||
expectDefaultDisallowedTools(backend.config.resumeArgs);
|
||||
expect(backend.config.clearEnv).toEqual([...CLAUDE_CLI_CLEAR_ENV]);
|
||||
expect(backend.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
|
||||
expect(backend.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Claude CLI argument helpers for OpenClaw-managed bundle MCP config.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
/** Find an existing Claude `--mcp-config` argument value. */
|
||||
@@ -43,3 +44,45 @@ export function injectClaudeMcpConfigArgs(
|
||||
next.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
|
||||
return next;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Writes the active per-attempt capture token into OpenClaw's generated Claude MCP config. */
|
||||
export async function writeClaudeMcpCaptureConfig(params: {
|
||||
mcpConfigPath: string;
|
||||
captureKey: string;
|
||||
}): Promise<void> {
|
||||
const raw = JSON.parse(await fs.readFile(params.mcpConfigPath, "utf-8")) as unknown;
|
||||
if (!isRecord(raw)) {
|
||||
throw new Error("Claude MCP capture requires an object config");
|
||||
}
|
||||
const mcpServers = isRecord(raw.mcpServers) ? raw.mcpServers : {};
|
||||
const openclaw = isRecord(mcpServers.openclaw) ? mcpServers.openclaw : undefined;
|
||||
if (!openclaw) {
|
||||
throw new Error("Claude MCP capture requires an openclaw server config");
|
||||
}
|
||||
const headers = isRecord(openclaw.headers) ? openclaw.headers : {};
|
||||
await fs.writeFile(
|
||||
params.mcpConfigPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
...raw,
|
||||
mcpServers: {
|
||||
...mcpServers,
|
||||
openclaw: {
|
||||
...openclaw,
|
||||
headers: {
|
||||
...headers,
|
||||
"x-openclaw-cli-capture-key": params.captureKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ function createEnabledBundleProbeConfig(): OpenClawConfig {
|
||||
|
||||
export async function prepareBundleProbeCliConfig(params?: {
|
||||
additionalConfig?: Parameters<typeof prepareCliBundleMcpConfig>[0]["additionalConfig"];
|
||||
env?: Parameters<typeof prepareCliBundleMcpConfig>[0]["env"];
|
||||
}) {
|
||||
// Bundle discovery reads HOME for per-user plugin roots.
|
||||
return await withEnvAsync({ HOME: bundleProbeHomeDir }, async () => {
|
||||
@@ -82,6 +83,7 @@ export async function prepareBundleProbeCliConfig(params?: {
|
||||
workspaceDir: bundleProbeWorkspaceDir,
|
||||
config: createEnabledBundleProbeConfig(),
|
||||
additionalConfig: params?.additionalConfig,
|
||||
env: params?.env,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { writeClaudeBundleManifest } from "../../plugins/bundle-mcp.test-support.js";
|
||||
import { prepareCliBundleMcpConfig } from "./bundle-mcp.js";
|
||||
import { prepareCliBundleMcpCaptureAttempt, prepareCliBundleMcpConfig } from "./bundle-mcp.js";
|
||||
import {
|
||||
cliBundleMcpHarness,
|
||||
prepareBundleProbeCliConfig,
|
||||
@@ -116,18 +116,31 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
});
|
||||
|
||||
it("merges loopback overlay config with bundle MCP servers", async () => {
|
||||
const prepared = await prepareBundleProbeCliConfig({
|
||||
additionalConfig: {
|
||||
mcpServers: {
|
||||
openclaw: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:23119/mcp",
|
||||
headers: {
|
||||
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
},
|
||||
const additionalConfig = {
|
||||
mcpServers: {
|
||||
openclaw: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:23119/mcp",
|
||||
headers: {
|
||||
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
"x-openclaw-cli-capture-key": "${OPENCLAW_MCP_CLI_CAPTURE_KEY}",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const prepared = await prepareBundleProbeCliConfig({
|
||||
additionalConfig,
|
||||
env: {
|
||||
OPENCLAW_MCP_TOKEN: "loopback-token-123",
|
||||
OPENCLAW_MCP_CLI_CAPTURE_KEY: "",
|
||||
},
|
||||
});
|
||||
const otherEnvPrepared = await prepareBundleProbeCliConfig({
|
||||
additionalConfig,
|
||||
env: {
|
||||
OPENCLAW_MCP_TOKEN: "other-loopback-token",
|
||||
OPENCLAW_MCP_CLI_CAPTURE_KEY: "",
|
||||
},
|
||||
});
|
||||
|
||||
const generatedConfigPath = requireMcpConfigPath(prepared.backend.args);
|
||||
@@ -136,9 +149,28 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
};
|
||||
expect(Object.keys(raw.mcpServers ?? {}).toSorted()).toEqual(["bundleProbe", "openclaw"]);
|
||||
expect(raw.mcpServers?.openclaw?.url).toBe("http://127.0.0.1:23119/mcp");
|
||||
expect(raw.mcpServers?.openclaw?.headers?.Authorization).toBe("Bearer ${OPENCLAW_MCP_TOKEN}");
|
||||
expect(raw.mcpServers?.openclaw?.headers?.Authorization).toBe("Bearer loopback-token-123");
|
||||
expect(raw.mcpServers?.openclaw?.headers?.["x-openclaw-cli-capture-key"]).toBe("");
|
||||
await prepareCliBundleMcpCaptureAttempt({
|
||||
mode: "claude-config-file",
|
||||
backend: prepared.backend,
|
||||
env: prepared.env,
|
||||
captureKey: "attempt-123",
|
||||
});
|
||||
const attemptRaw = JSON.parse(await fs.readFile(generatedConfigPath, "utf-8")) as {
|
||||
mcpServers?: Record<string, { url?: string; headers?: Record<string, string> }>;
|
||||
};
|
||||
expect(attemptRaw.mcpServers?.openclaw?.headers?.Authorization).toBe(
|
||||
"Bearer loopback-token-123",
|
||||
);
|
||||
expect(attemptRaw.mcpServers?.openclaw?.headers?.["x-openclaw-cli-capture-key"]).toBe(
|
||||
"attempt-123",
|
||||
);
|
||||
expect(prepared.mcpConfigHash).toBe(otherEnvPrepared.mcpConfigHash);
|
||||
expect(prepared.mcpResumeHash).toBe(otherEnvPrepared.mcpResumeHash);
|
||||
|
||||
await prepared.cleanup?.();
|
||||
await otherEnvPrepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("preserves extra env values alongside generated MCP config", async () => {
|
||||
|
||||
@@ -13,7 +13,11 @@ import { extractMcpServerMap, type BundleMcpConfig } from "../../plugins/bundle-
|
||||
import type { CliBundleMcpMode } from "../../plugins/types.js";
|
||||
import { loadMergedBundleMcpConfig, toCliBundleMcpServerConfig } from "../bundle-mcp-config.js";
|
||||
import { isRecord } from "./bundle-mcp-adapter-shared.js";
|
||||
import { findClaudeMcpConfigPath, injectClaudeMcpConfigArgs } from "./bundle-mcp-claude.js";
|
||||
import {
|
||||
findClaudeMcpConfigPath,
|
||||
injectClaudeMcpConfigArgs,
|
||||
writeClaudeMcpCaptureConfig,
|
||||
} from "./bundle-mcp-claude.js";
|
||||
import { injectCodexMcpConfigArgs } from "./bundle-mcp-codex.js";
|
||||
import { writeGeminiMcpCaptureSettings, writeGeminiSystemSettings } from "./bundle-mcp-gemini.js";
|
||||
|
||||
@@ -78,6 +82,28 @@ function canonicalizeBundleMcpConfigForResume(config: BundleMcpConfig): BundleMc
|
||||
};
|
||||
}
|
||||
|
||||
const OPENCLAW_MCP_ENV_TEMPLATE_PATTERN = /\$\{(OPENCLAW_MCP_[A-Z0-9_]+)\}/g;
|
||||
|
||||
function resolveOpenClawMcpEnvTemplates(value: unknown, env?: Record<string, string>): unknown {
|
||||
if (!env) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value.replace(OPENCLAW_MCP_ENV_TEMPLATE_PATTERN, (match, name: string) => {
|
||||
return Object.hasOwn(env, name) ? env[name] : match;
|
||||
});
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => resolveOpenClawMcpEnvTemplates(entry, env));
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entry]) => [key, resolveOpenClawMcpEnvTemplates(entry, env)]),
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareModeSpecificBundleMcpConfig(params: {
|
||||
mode: CliBundleMcpMode;
|
||||
backend: CliBackendConfig;
|
||||
@@ -122,7 +148,11 @@ async function prepareModeSpecificBundleMcpConfig(params: {
|
||||
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-mcp-"));
|
||||
const mcpConfigPath = path.join(tempDir, "mcp.json");
|
||||
await fs.writeFile(mcpConfigPath, serializedConfig, "utf-8");
|
||||
const runtimeConfig = resolveOpenClawMcpEnvTemplates(
|
||||
params.mergedConfig,
|
||||
params.env,
|
||||
) as BundleMcpConfig;
|
||||
await fs.writeFile(mcpConfigPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, "utf-8");
|
||||
return {
|
||||
backend: {
|
||||
...params.backend,
|
||||
@@ -201,6 +231,7 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
/** Prepares a per-attempt capture token without changing resume compatibility hashes. */
|
||||
export async function prepareCliBundleMcpCaptureAttempt(params: {
|
||||
mode?: CliBundleMcpMode;
|
||||
backend?: CliBackendConfig;
|
||||
env?: Record<string, string>;
|
||||
captureKey?: string;
|
||||
}): Promise<{ env?: Record<string, string>; cleanup?: () => Promise<void> }> {
|
||||
@@ -213,6 +244,17 @@ export async function prepareCliBundleMcpCaptureAttempt(params: {
|
||||
captureKey: params.captureKey,
|
||||
});
|
||||
}
|
||||
if (resolveBundleMcpMode(params.mode) === "claude-config-file") {
|
||||
const mcpConfigPath =
|
||||
findClaudeMcpConfigPath(params.backend?.args) ??
|
||||
findClaudeMcpConfigPath(params.backend?.resumeArgs);
|
||||
if (mcpConfigPath) {
|
||||
await writeClaudeMcpCaptureConfig({
|
||||
mcpConfigPath,
|
||||
captureKey: params.captureKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
env: {
|
||||
...params.env,
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "../cli-output.js";
|
||||
import { classifyFailoverReason } from "../embedded-agent-helpers.js";
|
||||
import { FailoverError, resolveFailoverStatus } from "../failover-error.js";
|
||||
import { prepareCliBundleMcpCaptureAttempt } from "./bundle-mcp.js";
|
||||
import { buildClaudeOwnerKey } from "./helpers.js";
|
||||
import { cliBackendLog, formatCliBackendOutputDigest } from "./log.js";
|
||||
import type { PreparedCliRunContext } from "./types.js";
|
||||
@@ -1063,39 +1064,49 @@ async function createClaudeLiveSession(params: {
|
||||
cleanup: () => Promise<void>;
|
||||
}): Promise<ClaudeLiveSession> {
|
||||
let session: ClaudeLiveSession | null = null;
|
||||
const managedRun = await params.supervisor.spawn({
|
||||
sessionId: params.context.params.sessionId,
|
||||
backendId: params.context.backendResolved.id,
|
||||
scopeKey: `claude-live:${params.key}`,
|
||||
replaceExistingScope: true,
|
||||
mode: "child",
|
||||
argv: params.argv,
|
||||
cwd: params.context.cwd ?? params.context.workspaceDir,
|
||||
env: params.mcpCaptureKey
|
||||
? { ...params.env, OPENCLAW_MCP_CLI_CAPTURE_KEY: params.mcpCaptureKey }
|
||||
: params.env,
|
||||
stdinMode: "pipe-open",
|
||||
captureOutput: false,
|
||||
onStdout: (chunk) => {
|
||||
if (session) {
|
||||
handleClaudeStdout(session, chunk);
|
||||
}
|
||||
},
|
||||
onStderr: (chunk) => {
|
||||
if (session) {
|
||||
session.stderr += chunk;
|
||||
if (session.stderr.length > CLAUDE_LIVE_MAX_STDERR_CHARS) {
|
||||
closeLiveSession(
|
||||
session,
|
||||
"abort",
|
||||
createOutputLimitError(session, "Claude CLI stderr exceeded limit."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resetNoOutputTimer(session);
|
||||
}
|
||||
},
|
||||
const mcpCaptureAttempt = await prepareCliBundleMcpCaptureAttempt({
|
||||
mode: params.context.backendResolved.bundleMcpMode,
|
||||
backend: params.context.preparedBackend.backend,
|
||||
env: params.env,
|
||||
captureKey: params.mcpCaptureKey,
|
||||
});
|
||||
let managedRun: ManagedRun;
|
||||
try {
|
||||
managedRun = await params.supervisor.spawn({
|
||||
sessionId: params.context.params.sessionId,
|
||||
backendId: params.context.backendResolved.id,
|
||||
scopeKey: `claude-live:${params.key}`,
|
||||
replaceExistingScope: true,
|
||||
mode: "child",
|
||||
argv: params.argv,
|
||||
cwd: params.context.cwd ?? params.context.workspaceDir,
|
||||
env: mcpCaptureAttempt.env ?? params.env,
|
||||
stdinMode: "pipe-open",
|
||||
captureOutput: false,
|
||||
onStdout: (chunk) => {
|
||||
if (session) {
|
||||
handleClaudeStdout(session, chunk);
|
||||
}
|
||||
},
|
||||
onStderr: (chunk) => {
|
||||
if (session) {
|
||||
session.stderr += chunk;
|
||||
if (session.stderr.length > CLAUDE_LIVE_MAX_STDERR_CHARS) {
|
||||
closeLiveSession(
|
||||
session,
|
||||
"abort",
|
||||
createOutputLimitError(session, "Claude CLI stderr exceeded limit."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resetNoOutputTimer(session);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await mcpCaptureAttempt.cleanup?.();
|
||||
throw error;
|
||||
}
|
||||
session = {
|
||||
key: params.key,
|
||||
fingerprint: params.fingerprint,
|
||||
@@ -1109,7 +1120,10 @@ async function createClaudeLiveSession(params: {
|
||||
drainTimer: null,
|
||||
drainingAbortedTurn: false,
|
||||
idleTimer: null,
|
||||
cleanup: params.cleanup,
|
||||
cleanup: async () => {
|
||||
await mcpCaptureAttempt.cleanup?.();
|
||||
await params.cleanup();
|
||||
},
|
||||
cleanupPromise: null,
|
||||
closing: false,
|
||||
mcpCaptureKey: params.mcpCaptureKey,
|
||||
|
||||
@@ -677,6 +677,7 @@ export async function executePreparedCliRun(
|
||||
: buildCliMcpCaptureKey(context);
|
||||
const mcpCaptureAttempt = await prepareCliBundleMcpCaptureAttempt({
|
||||
mode: context.backendResolved.bundleMcpMode,
|
||||
backend,
|
||||
env: context.preparedBackend.env,
|
||||
captureKey: initialGatewayCaptureKey,
|
||||
});
|
||||
|
||||
@@ -112,6 +112,7 @@ function createTestMcpLoopbackServerConfig(port: number) {
|
||||
openclaw: {
|
||||
type: "http",
|
||||
url: `http://127.0.0.1:${port}/mcp`,
|
||||
alwaysLoad: true,
|
||||
headers: {
|
||||
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
"x-session-key": "${OPENCLAW_MCP_SESSION_KEY}",
|
||||
|
||||
@@ -44,14 +44,19 @@ describe("live-agent-probes", () => {
|
||||
agentId: "codex",
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
});
|
||||
expect(
|
||||
buildLiveCronProbeMessage({
|
||||
agent: "claude-cli",
|
||||
argsJson: spec.argsJson,
|
||||
attempt: 1,
|
||||
exactReply: spec.name,
|
||||
}),
|
||||
).toContain("Preserve job.sessionTarget and job.sessionKey exactly as provided.");
|
||||
const claudeRetryPrompt = buildLiveCronProbeMessage({
|
||||
agent: "claude-cli",
|
||||
argsJson: spec.argsJson,
|
||||
attempt: 1,
|
||||
exactReply: spec.name,
|
||||
});
|
||||
expect(claudeRetryPrompt).toContain(
|
||||
"Preserve job.sessionTarget and job.sessionKey exactly as provided.",
|
||||
);
|
||||
expect(claudeRetryPrompt).toContain("search/load MCP tools for `openclaw cron` or `cron`");
|
||||
expect(claudeRetryPrompt).toContain("mcp__openclaw__cron");
|
||||
expect(claudeRetryPrompt).toContain("Do not use Claude native `CronCreate`");
|
||||
expect(claudeRetryPrompt).not.toContain("openclaw-tools");
|
||||
expect(
|
||||
buildLiveCronProbeMessage({
|
||||
agent: "future-agent",
|
||||
|
||||
@@ -103,8 +103,10 @@ export function buildLiveCronProbeMessage(params: {
|
||||
const claudeLike = isClaudeLikeLiveAgent(params.agent);
|
||||
if (params.attempt === 0) {
|
||||
return (
|
||||
"Use the OpenClaw MCP tool `openclaw-tools/cron` (server `openclaw-tools`, tool `cron`). " +
|
||||
"If the harness shows Claude-style MCP names, use `mcp__openclaw-tools__cron` or `mcp__openclaw_tools__cron`. " +
|
||||
"Use the OpenClaw MCP cron tool from server `openclaw`. " +
|
||||
"If it is not already visible, search/load MCP tools for `openclaw cron` or `cron`, " +
|
||||
"then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__cron`. " +
|
||||
"Do not use Claude native `CronCreate`, `CronList`, or `CronDelete`; those are not OpenClaw proof. " +
|
||||
`Call it with JSON arguments ${params.argsJson}. ` +
|
||||
"Preserve the JSON exactly, including job.sessionTarget and job.sessionKey; do not omit, rename, or flatten those fields. " +
|
||||
"Do the actual tool call; I will verify externally with the OpenClaw cron CLI. " +
|
||||
@@ -113,8 +115,10 @@ export function buildLiveCronProbeMessage(params: {
|
||||
}
|
||||
if (claudeLike) {
|
||||
return (
|
||||
"Retry the OpenClaw MCP tool `openclaw-tools/cron` now. " +
|
||||
"If the harness shows Claude-style MCP names, use `mcp__openclaw-tools__cron` or `mcp__openclaw_tools__cron`. " +
|
||||
"Retry the OpenClaw MCP cron tool from server `openclaw` now. " +
|
||||
"If it is not already visible, search/load MCP tools for `openclaw cron` or `cron`, " +
|
||||
"then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__cron`. " +
|
||||
"Do not use Claude native `CronCreate`, `CronList`, or `CronDelete`; those are not OpenClaw proof. " +
|
||||
`Use these exact JSON arguments: ${params.argsJson}. ` +
|
||||
"Preserve job.sessionTarget and job.sessionKey exactly as provided. " +
|
||||
`If the cron job is created, reply exactly: ${params.exactReply}. ` +
|
||||
@@ -125,8 +129,8 @@ export function buildLiveCronProbeMessage(params: {
|
||||
}
|
||||
return (
|
||||
"Your previous OpenClaw cron MCP tool call was cancelled before the job was created. " +
|
||||
"Retry the OpenClaw MCP tool `openclaw-tools/cron` now. " +
|
||||
"If the harness shows Claude-style MCP names, use `mcp__openclaw-tools__cron` or `mcp__openclaw_tools__cron`. " +
|
||||
"Retry the OpenClaw MCP cron tool from server `openclaw` now. " +
|
||||
"If the harness shows Claude-style MCP names, use `mcp__openclaw__cron`. " +
|
||||
`Use these exact JSON arguments: ${params.argsJson}. ` +
|
||||
"Preserve job.sessionTarget and job.sessionKey exactly as provided. " +
|
||||
`If the cron job is created, reply exactly: ${params.exactReply}. ` +
|
||||
|
||||
@@ -373,6 +373,7 @@ export function createMcpLoopbackServerConfig(port: number) {
|
||||
openclaw: {
|
||||
type: "http",
|
||||
url: `http://127.0.0.1:${port}/mcp`,
|
||||
alwaysLoad: true,
|
||||
headers: {
|
||||
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
"x-session-key": "${OPENCLAW_MCP_SESSION_KEY}",
|
||||
|
||||
@@ -1688,9 +1688,13 @@ describe("mcp loopback server", () => {
|
||||
describe("createMcpLoopbackServerConfig", () => {
|
||||
it("builds a server entry with env-driven headers", () => {
|
||||
const config = createMcpLoopbackServerConfig(23119) as {
|
||||
mcpServers?: Record<string, { url?: string; headers?: Record<string, string> }>;
|
||||
mcpServers?: Record<
|
||||
string,
|
||||
{ alwaysLoad?: boolean; url?: string; headers?: Record<string, string> }
|
||||
>;
|
||||
};
|
||||
expect(config.mcpServers?.openclaw?.url).toBe("http://127.0.0.1:23119/mcp");
|
||||
expect(config.mcpServers?.openclaw?.alwaysLoad).toBe(true);
|
||||
expect(config.mcpServers?.openclaw?.headers?.Authorization).toBe(
|
||||
"Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
);
|
||||
|
||||
@@ -2233,6 +2233,44 @@ describe("gateway agent handler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards admin caller ownership to ingress agent runs", async () => {
|
||||
primeMainAgentRun({ cfg: mocks.loadConfigReturn });
|
||||
mocks.agentCommand.mockClear();
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "owner tool check",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: "test-admin-sender-owner",
|
||||
},
|
||||
{
|
||||
reqId: "admin-sender-owner",
|
||||
client: { connect: { scopes: ["operator.admin"] } } as AgentHandlerArgs["client"],
|
||||
},
|
||||
);
|
||||
|
||||
expect((await waitForAgentCommandCall<{ senderIsOwner?: boolean }>()).senderIsOwner).toBe(true);
|
||||
|
||||
mocks.agentCommand.mockClear();
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "non-owner tool check",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: "test-write-sender-owner",
|
||||
},
|
||||
{
|
||||
reqId: "write-sender-owner",
|
||||
client: backendGatewayClient(),
|
||||
},
|
||||
);
|
||||
|
||||
expect((await waitForAgentCommandCall<{ senderIsOwner?: boolean }>()).senderIsOwner).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects public transcriptMessage overrides", async () => {
|
||||
primeMainAgentRun({ cfg: mocks.loadConfigReturn });
|
||||
mocks.agentCommand.mockClear();
|
||||
|
||||
@@ -2770,6 +2770,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
acpTurnSource: request.acpTurnSource,
|
||||
internalEvents: request.internalEvents,
|
||||
inputProvenance,
|
||||
senderIsOwner: clientHasAdminScope(client),
|
||||
sessionEffects,
|
||||
skipInitialSessionTouch: skipAgentInitialSessionTouch,
|
||||
preserveUserFacingSessionModelState,
|
||||
|
||||
Reference in New Issue
Block a user