Skip to content

Propagate security context (senderIsOwner, toolProfile) to MCP server #59

@alexey-pelykh

Description

@alexey-pelykh

Problem

The MCP server currently registers all 29 tools unconditionally for every invocation. OpenClaw gates owner-only tools (cron management, gateway admin) behind a senderIsOwner flag resolved from command authorization, and applies tool profiles (minimal/coding/messaging/full) to control which tool categories are available. The MCP server needs the same gating.

Without this, a non-owner channel user can instruct the agent to modify cron schedules or restart the gateway via MCP tools — actions that should be restricted to the bot owner.

Current State

ChannelBridge passes no auth context

ChannelBridge.#buildMcpConfig() (src/middleware/channel-bridge.ts ~line 177) sets env vars for the MCP server but includes no security context:

#buildMcpConfig(message, sessionKey, sideEffectsFile) {
  return {
    remoteclaw: {
      command: "node",
      args: [this.#mcpServerPath],
      env: {
        REMOTECLAW_GATEWAY_URL: ...,
        REMOTECLAW_GATEWAY_TOKEN: ...,
        REMOTECLAW_SESSION_KEY: ...,
        REMOTECLAW_SIDE_EFFECTS_FILE: ...,
        REMOTECLAW_CHANNEL: ...,
        REMOTECLAW_ACCOUNT_ID: ...,
        REMOTECLAW_TO: ...,
        // No REMOTECLAW_SENDER_IS_OWNER
        // No REMOTECLAW_TOOL_PROFILE
      },
    },
  };
}

MCP server registers all tools unconditionally

registerAllTools() (src/middleware/mcp-tools.ts) calls all 4 registration functions without filtering:

export function registerAllTools(server: McpServer, ctx: McpHandlerContext): void {
  registerSessionTools(server, ctx);   // 7 tools
  registerMessageTools(server, ctx);   // 10 tools
  registerCronTools(server, ctx);      // 7 tools — owner-only
  registerGatewayTools(server, ctx);   // 5 tools — owner-only (write), read OK
}

McpHandlerContext has no auth fields

McpHandlerContext (src/middleware/mcp-handlers/context.ts) currently only carries routing fields (gateway URL, session key, channel info).

Where senderIsOwner comes from

resolveCommandAuthorization() in src/auto-reply/command-auth.ts resolves senderIsOwner: boolean from the commands.ownerAllowFrom config. This is called in the auto-reply pipeline before the agent runs. The cron dispatch always uses senderIsOwner: false (cron agents don't self-modify).

Where tool profiles come from

resolveToolProfilePolicy() in src/agents/tool-policy-shared.ts resolves a ToolProfileId ("minimal" | "coding" | "messaging" | "full") from agent config. OpenClaw uses this to control which tool categories are registered.

Which tools are owner-only

From src/agents/tools/:

  • cron-tool.ts: ownerOnly: true (cron_add, cron_list, cron_remove, cron_update, cron_get, cron_pause, cron_resume)
  • gateway-tool.ts: ownerOnly: true (gateway_restart, gateway_config_apply, gateway_config_get, gateway_config_schema, gateway_status)

Required Changes

1. Add auth fields to McpHandlerContext (~3 lines)

In src/middleware/mcp-handlers/context.ts, add:

export type McpHandlerContext = {
  // ... existing fields ...
  /** Whether the message sender is the bot owner. */
  senderIsOwner: boolean;
  /** Tool profile controlling which tool categories are available. */
  toolProfile: string;  // "minimal" | "coding" | "messaging" | "full"
};

2. Pass auth env vars from ChannelBridge (~3 lines)

In ChannelBridge.#buildMcpConfig(), add env vars:

env: {
  // ... existing vars ...
  REMOTECLAW_SENDER_IS_OWNER: String(senderIsOwner),
  REMOTECLAW_TOOL_PROFILE: toolProfile,
},

This requires ChannelBridge.handle() to accept or resolve senderIsOwner and toolProfile. The caller (each dispatch site) provides these.

3. Read auth env vars in MCP server startup (~3 lines)

In createContext() (src/middleware/mcp-server.ts), add:

senderIsOwner: process.env.REMOTECLAW_SENDER_IS_OWNER === "true",
toolProfile: process.env.REMOTECLAW_TOOL_PROFILE || "full",

4. Filter tool registration in registerAllTools() (~10 lines)

In src/middleware/mcp-tools.ts, gate registrations:

export function registerAllTools(server: McpServer, ctx: McpHandlerContext): void {
  registerSessionTools(server, ctx);
  registerMessageTools(server, ctx);
  if (ctx.senderIsOwner) {
    registerCronTools(server, ctx);
    registerGatewayTools(server, ctx);
  }
  // Tool profile filtering can be layered on top if needed
}

5. Thread senderIsOwner through dispatch sites (~5 lines each)

Each dispatch site that calls ChannelBridge.handle() must pass the security context:

  • Auto-reply (src/auto-reply/reply/agent-runner-execution.ts): Already resolves commandAuth.senderIsOwner — pass it through.
  • CLI command (src/commands/agent.ts): CLI user is always the owner → senderIsOwner: true.
  • Cron (src/cron/isolated-agent/run.ts): Cron agents are never owner → senderIsOwner: false.
  • Voice-call (extensions/voice-call/src/core-bridge.ts): Check extension context for owner status.

6. Add unit tests (~30 lines)

  • Test that registerAllTools with senderIsOwner: false does NOT register cron/gateway tools
  • Test that registerAllTools with senderIsOwner: true registers all tools
  • Test that ChannelBridge.#buildMcpConfig() includes REMOTECLAW_SENDER_IS_OWNER env var
  • Test that createContext() parses REMOTECLAW_SENDER_IS_OWNER correctly

Acceptance Criteria

  • senderIsOwner flag is propagated from dispatch sites → ChannelBridge → MCP server env → McpHandlerContext
  • Tool profile is propagated through the same path
  • MCP tools respect the security context: owner-only tools (cron, gateway) are NOT registered for non-owner senders
  • Auto-reply dispatch passes senderIsOwner from resolveCommandAuthorization()
  • CLI command dispatch passes senderIsOwner: true (CLI user is always owner)
  • Cron dispatch passes senderIsOwner: false (cron agents don't self-modify)
  • Unit tests verify security propagation for owner and non-owner senders
  • pnpm build passes
  • Existing tests pass (no regressions)

Files to Touch

File Change
src/middleware/mcp-handlers/context.ts Add senderIsOwner and toolProfile fields
src/middleware/mcp-server.ts Read new env vars in createContext()
src/middleware/mcp-tools.ts Gate cron/gateway tool registration on senderIsOwner
src/middleware/channel-bridge.ts Accept auth context, pass as env vars to MCP server
src/middleware/channel-bridge.test.ts Test auth env var propagation
src/middleware/types.ts Optional: add auth context to ChannelBridge constructor or handle() params
src/middleware/mcp-tools.test.ts Test owner/non-owner tool filtering
src/auto-reply/reply/agent-runner-execution.ts Pass senderIsOwner from command auth
src/commands/agent.ts Pass senderIsOwner: true
src/cron/isolated-agent/run.ts Pass senderIsOwner: false
extensions/voice-call/src/core-bridge.ts Pass senderIsOwner from extension context

Context

  • OpenClaw tool gating: src/agents/tool-policy.tsapplyOwnerOnlyToolPolicy() wraps owner-only tools
  • Owner-only flag: src/agents/tools/cron-tool.ts (ownerOnly: true), src/agents/tools/gateway-tool.ts (ownerOnly: true)
  • Command auth: src/auto-reply/command-auth.tsresolveCommandAuthorization() returns { senderIsOwner: boolean, ... }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions