@agent-vm/openclaw-mcp-portal-plugin 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +231 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +597 -0
- package/dist/index.js.map +1 -0
- package/dist/openclaw.plugin.json +16 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Shravan Sunder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @agent-vm/openclaw-mcp-portal-plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that supervises the MCP Portal subprocess and wires portal calls
|
|
4
|
+
into the OpenClaw agent loop.
|
|
5
|
+
|
|
6
|
+
## What This Package Owns
|
|
7
|
+
|
|
8
|
+
- Starts and stops the `agent-vm-mcp-portal-server` subprocess through
|
|
9
|
+
OpenClaw `registerService`.
|
|
10
|
+
- Generates per-agent HMAC keys for each plugin boot and passes them to the
|
|
11
|
+
portal subprocess as environment variables.
|
|
12
|
+
- Registers `before_tool_call` to deny disallowed portal calls and attach
|
|
13
|
+
approval tokens to approved calls.
|
|
14
|
+
- Registers `before_prompt_build` to inject scoped progressive-disclosure hints.
|
|
15
|
+
|
|
16
|
+
## Runtime Config
|
|
17
|
+
|
|
18
|
+
The OpenClaw plugin config should only carry runtime process settings:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"configDir": "/home/openclaw/.openclaw/config",
|
|
23
|
+
"binPath": "/opt/agent-vm/portal/bin/agent-vm-mcp-portal-server"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Namespace/tool policy does not live in OpenClaw plugin config. It lives in
|
|
28
|
+
`mcp-portal.config.jsonc` inside the configured directory.
|
|
29
|
+
|
|
30
|
+
## Start Reading
|
|
31
|
+
|
|
32
|
+
- `src/plugin-registration.ts` for OpenClaw service and hook registration.
|
|
33
|
+
- `src/portal-subprocess-supervisor.ts` for process lifecycle.
|
|
34
|
+
- `src/before-tool-call-handler.ts` for policy and approval-token behavior.
|
|
35
|
+
- `src/before-prompt-build-handler.ts` for prompt context injection.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { McpPortalConfig, ResolvedMcpPortalProfile } from "@agent-vm/config-contracts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ChildProcess, SpawnOptions } from "node:child_process";
|
|
4
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
|
|
6
|
+
//#region src/openclaw-plugin-api.d.ts
|
|
7
|
+
interface OpenClawPromptHookContext {
|
|
8
|
+
readonly agentId?: string;
|
|
9
|
+
readonly appendPrompt?: (content: string) => void;
|
|
10
|
+
}
|
|
11
|
+
interface OpenClawPluginHookContext {
|
|
12
|
+
readonly agentId?: string;
|
|
13
|
+
readonly sessionId?: string;
|
|
14
|
+
readonly sessionKey?: string;
|
|
15
|
+
readonly toolCallId?: string;
|
|
16
|
+
readonly toolName?: string;
|
|
17
|
+
}
|
|
18
|
+
interface OpenClawAgentTurnPrepareEvent {
|
|
19
|
+
readonly messages?: readonly unknown[];
|
|
20
|
+
readonly prompt?: string;
|
|
21
|
+
}
|
|
22
|
+
interface OpenClawBeforePromptBuildEvent {
|
|
23
|
+
readonly messages?: readonly unknown[];
|
|
24
|
+
readonly prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
interface OpenClawBeforeToolCallEvent {
|
|
27
|
+
readonly params: Record<string, unknown>;
|
|
28
|
+
readonly toolCallId?: string;
|
|
29
|
+
readonly toolName: string;
|
|
30
|
+
}
|
|
31
|
+
type OpenClawApprovalResolution = 'allow-always' | 'allow-once' | 'cancelled' | 'deny' | 'timeout';
|
|
32
|
+
interface OpenClawBeforeToolCallResult {
|
|
33
|
+
readonly block?: boolean;
|
|
34
|
+
readonly blockReason?: string;
|
|
35
|
+
readonly requireApproval?: {
|
|
36
|
+
readonly description: string;
|
|
37
|
+
readonly onResolution?: (decision: OpenClawApprovalResolution) => Promise<void> | void;
|
|
38
|
+
readonly pluginId?: string;
|
|
39
|
+
readonly severity?: 'critical' | 'info' | 'warning';
|
|
40
|
+
readonly timeoutBehavior?: 'allow' | 'deny';
|
|
41
|
+
readonly timeoutMs?: number;
|
|
42
|
+
readonly title: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
interface OpenClawPromptHookResult {
|
|
46
|
+
readonly appendContext?: string;
|
|
47
|
+
readonly appendSystemContext?: string;
|
|
48
|
+
readonly prependContext?: string;
|
|
49
|
+
readonly prependSystemContext?: string;
|
|
50
|
+
}
|
|
51
|
+
type OpenClawPluginHookEventMap = {
|
|
52
|
+
readonly agent_turn_prepare: OpenClawAgentTurnPrepareEvent;
|
|
53
|
+
readonly before_prompt_build: OpenClawBeforePromptBuildEvent;
|
|
54
|
+
readonly before_tool_call: OpenClawBeforeToolCallEvent;
|
|
55
|
+
};
|
|
56
|
+
type OpenClawPluginHookResultMap = {
|
|
57
|
+
readonly agent_turn_prepare: OpenClawPromptHookResult;
|
|
58
|
+
readonly before_prompt_build: OpenClawPromptHookResult;
|
|
59
|
+
readonly before_tool_call: OpenClawBeforeToolCallResult;
|
|
60
|
+
};
|
|
61
|
+
interface OpenClawPluginHookOptions {
|
|
62
|
+
readonly priority?: number;
|
|
63
|
+
readonly timeoutMs?: number;
|
|
64
|
+
}
|
|
65
|
+
interface OpenClawHttpRouteRegistration {
|
|
66
|
+
readonly auth: 'gateway' | 'plugin';
|
|
67
|
+
readonly handler: (request: IncomingMessage, response: ServerResponse) => Promise<boolean> | boolean;
|
|
68
|
+
readonly match?: 'exact' | 'prefix';
|
|
69
|
+
readonly path: string;
|
|
70
|
+
readonly replaceExisting?: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface OpenClawPluginService {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly start: () => Promise<void> | void;
|
|
75
|
+
readonly stop?: () => Promise<void> | void;
|
|
76
|
+
}
|
|
77
|
+
interface OpenClawPortalPluginApi {
|
|
78
|
+
readonly config?: unknown;
|
|
79
|
+
readonly logger?: {
|
|
80
|
+
readonly debug?: (message: string) => void;
|
|
81
|
+
readonly error?: (message: string) => void;
|
|
82
|
+
readonly info?: (message: string) => void;
|
|
83
|
+
readonly warn?: (message: string) => void;
|
|
84
|
+
};
|
|
85
|
+
readonly pluginConfig?: unknown;
|
|
86
|
+
readonly registrationMode?: string;
|
|
87
|
+
readonly on?: <THookName extends keyof OpenClawPluginHookEventMap>(hookName: THookName, handler: (event: OpenClawPluginHookEventMap[THookName], context: OpenClawPluginHookContext) => OpenClawPluginHookResultMap[THookName] | Promise<OpenClawPluginHookResultMap[THookName] | void> | void, options?: OpenClawPluginHookOptions) => void;
|
|
88
|
+
readonly onDispose?: (cleanup: () => Promise<void> | void) => void;
|
|
89
|
+
readonly registerPromptHook?: (hookName: 'agent_turn_prepare' | 'before_prompt_build', handler: (context: OpenClawPromptHookContext) => Promise<void> | void) => void;
|
|
90
|
+
readonly registerHttpRoute?: (registration: OpenClawHttpRouteRegistration) => void;
|
|
91
|
+
readonly registerService?: (service: OpenClawPluginService) => void;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/plugin-registration.d.ts
|
|
95
|
+
interface TcpPoolConfig {
|
|
96
|
+
readonly basePort: number;
|
|
97
|
+
readonly size: number;
|
|
98
|
+
}
|
|
99
|
+
declare function validatePortalPortAgainstTcpPool(props: {
|
|
100
|
+
readonly port: number;
|
|
101
|
+
readonly tcpPool: TcpPoolConfig | null;
|
|
102
|
+
}): void;
|
|
103
|
+
declare function validatePortalPluginApi(api: OpenClawPortalPluginApi): void;
|
|
104
|
+
declare function registerMcpPortalPlugin(api: OpenClawPortalPluginApi): void;
|
|
105
|
+
declare const pluginEntry: {
|
|
106
|
+
description: string;
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
register: typeof registerMcpPortalPlugin;
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/hmac-key-registry.d.ts
|
|
113
|
+
interface CreateHmacKeyRegistryProps {
|
|
114
|
+
readonly agentIds: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
interface HmacKeyRegistry {
|
|
117
|
+
readonly agentIds: readonly string[];
|
|
118
|
+
readonly getKey: (agentId: string) => Buffer;
|
|
119
|
+
readonly serializeForEnv: () => Readonly<Record<string, string>>;
|
|
120
|
+
}
|
|
121
|
+
declare function createHmacKeyRegistry(props: CreateHmacKeyRegistryProps): HmacKeyRegistry;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/portal-plugin-runtime-state.d.ts
|
|
124
|
+
interface PortalPluginRuntimeState {
|
|
125
|
+
readonly configDir: string;
|
|
126
|
+
readonly getPortalUnavailableReason: () => string | null;
|
|
127
|
+
readonly getKeyRegistry: () => HmacKeyRegistry;
|
|
128
|
+
readonly loadPortalConfig: () => Promise<McpPortalConfig>;
|
|
129
|
+
readonly markPortalAvailable: () => void;
|
|
130
|
+
readonly markPortalUnavailable: (reason: string) => void;
|
|
131
|
+
readonly setKeyRegistry: (registry: HmacKeyRegistry) => void;
|
|
132
|
+
}
|
|
133
|
+
declare function createPortalPluginRuntimeState(props: {
|
|
134
|
+
readonly configDir: string;
|
|
135
|
+
readonly loadPortalConfig?: (path: string) => Promise<McpPortalConfig>;
|
|
136
|
+
}): PortalPluginRuntimeState;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/before-prompt-build-handler.d.ts
|
|
139
|
+
interface CreateBeforePromptBuildHandlerProps {
|
|
140
|
+
readonly runtimeState: PortalPluginRuntimeState;
|
|
141
|
+
}
|
|
142
|
+
declare function createBeforePromptBuildHandler(props: CreateBeforePromptBuildHandlerProps): (event: OpenClawBeforePromptBuildEvent, context: OpenClawPluginHookContext) => Promise<OpenClawPromptHookResult | undefined>;
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/before-tool-call-handler.d.ts
|
|
145
|
+
interface CreateBeforeToolCallHandlerProps {
|
|
146
|
+
readonly logger?: {
|
|
147
|
+
readonly warn?: (message: string) => void;
|
|
148
|
+
};
|
|
149
|
+
readonly runtimeState: PortalPluginRuntimeState;
|
|
150
|
+
}
|
|
151
|
+
declare function createBeforeToolCallHandler(props: CreateBeforeToolCallHandlerProps): (event: OpenClawBeforeToolCallEvent, context: OpenClawPluginHookContext) => Promise<OpenClawBeforeToolCallResult | undefined>;
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/portal-config.d.ts
|
|
154
|
+
declare const defaultPortalBinPath = "/opt/agent-vm/portal/bin/agent-vm-mcp-portal-server";
|
|
155
|
+
declare const portalPluginConfigSchema: z.ZodObject<{
|
|
156
|
+
binPath: z.ZodDefault<z.ZodString>;
|
|
157
|
+
configDir: z.ZodOptional<z.ZodString>;
|
|
158
|
+
}, z.core.$strict>;
|
|
159
|
+
type PortalPluginConfig = z.infer<typeof portalPluginConfigSchema>;
|
|
160
|
+
declare function parsePortalConfig(value: unknown): PortalPluginConfig;
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/portal-subprocess-supervisor.d.ts
|
|
163
|
+
interface PortalSubprocessLogger {
|
|
164
|
+
readonly error: (message: string) => void;
|
|
165
|
+
readonly info: (message: string) => void;
|
|
166
|
+
readonly warn: (message: string) => void;
|
|
167
|
+
}
|
|
168
|
+
type PortalSubprocessSpawnFunction = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
169
|
+
interface CreatePortalSubprocessSupervisorProps {
|
|
170
|
+
readonly backoffSteps?: readonly number[];
|
|
171
|
+
readonly binPath: string;
|
|
172
|
+
readonly configDir: string;
|
|
173
|
+
readonly fetchFn?: typeof fetch;
|
|
174
|
+
readonly healthPollIntervalMs?: number;
|
|
175
|
+
readonly healthTimeoutMs?: number;
|
|
176
|
+
readonly host: string;
|
|
177
|
+
readonly hmacEnv: Readonly<Record<string, string>>;
|
|
178
|
+
readonly logger: PortalSubprocessLogger;
|
|
179
|
+
readonly maxRestarts?: number;
|
|
180
|
+
readonly onFatal?: (reason: string) => void;
|
|
181
|
+
readonly port: number;
|
|
182
|
+
readonly spawnFn?: PortalSubprocessSpawnFunction;
|
|
183
|
+
readonly stopGraceMs?: number;
|
|
184
|
+
}
|
|
185
|
+
interface PortalSubprocessSupervisor {
|
|
186
|
+
readonly isAlive: () => boolean;
|
|
187
|
+
readonly start: () => Promise<void>;
|
|
188
|
+
readonly stop: () => Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
declare function createPortalSubprocessSupervisor(props: CreatePortalSubprocessSupervisorProps): PortalSubprocessSupervisor;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/portal-tool-policy.d.ts
|
|
193
|
+
interface PortalCallRequest {
|
|
194
|
+
readonly arguments: Record<string, unknown>;
|
|
195
|
+
readonly id: string;
|
|
196
|
+
readonly namespace: string;
|
|
197
|
+
readonly toolName: string;
|
|
198
|
+
}
|
|
199
|
+
declare function portalServerNameForAgent(agentId: string): string;
|
|
200
|
+
declare function materializedPortalToolNames(serverName: string): readonly string[];
|
|
201
|
+
declare function profileAllowsPortalCall(profile: ResolvedMcpPortalProfile, call: {
|
|
202
|
+
readonly namespace: string;
|
|
203
|
+
readonly toolName: string;
|
|
204
|
+
}): boolean;
|
|
205
|
+
declare function profileRequiresPortalApproval(profile: ResolvedMcpPortalProfile, call: {
|
|
206
|
+
readonly namespace: string;
|
|
207
|
+
readonly toolName: string;
|
|
208
|
+
}): boolean;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/portal-prompt-context.d.ts
|
|
211
|
+
interface PortalPromptNamespaceSummary {
|
|
212
|
+
readonly namespace: string;
|
|
213
|
+
readonly toolCount: number;
|
|
214
|
+
}
|
|
215
|
+
interface PortalPromptDiagnostic {
|
|
216
|
+
readonly message: string;
|
|
217
|
+
readonly namespace: string;
|
|
218
|
+
}
|
|
219
|
+
declare function createPortalPromptContext(props: {
|
|
220
|
+
readonly diagnostics?: readonly PortalPromptDiagnostic[];
|
|
221
|
+
readonly namespaces: readonly PortalPromptNamespaceSummary[];
|
|
222
|
+
}): string;
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/redaction.d.ts
|
|
225
|
+
declare function redactPortalSecrets(text: string, secretValues?: readonly string[]): string;
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/index.d.ts
|
|
228
|
+
declare const OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME = "@agent-vm/openclaw-mcp-portal-plugin";
|
|
229
|
+
//#endregion
|
|
230
|
+
export { CreateBeforePromptBuildHandlerProps, CreateBeforeToolCallHandlerProps, CreateHmacKeyRegistryProps, CreatePortalSubprocessSupervisorProps, HmacKeyRegistry, OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME, OpenClawAgentTurnPrepareEvent, OpenClawApprovalResolution, OpenClawBeforePromptBuildEvent, OpenClawBeforeToolCallEvent, OpenClawBeforeToolCallResult, OpenClawHttpRouteRegistration, OpenClawPluginHookContext, OpenClawPluginHookEventMap, OpenClawPluginHookOptions, OpenClawPluginHookResultMap, OpenClawPluginService, OpenClawPortalPluginApi, OpenClawPromptHookContext, OpenClawPromptHookResult, PortalCallRequest, PortalPluginConfig, PortalPluginRuntimeState, PortalPromptDiagnostic, PortalPromptNamespaceSummary, PortalSubprocessLogger, PortalSubprocessSpawnFunction, PortalSubprocessSupervisor, createBeforePromptBuildHandler, createBeforeToolCallHandler, createHmacKeyRegistry, createPortalPluginRuntimeState, createPortalPromptContext, createPortalSubprocessSupervisor, pluginEntry as default, defaultPortalBinPath, materializedPortalToolNames, parsePortalConfig, portalPluginConfigSchema, portalServerNameForAgent, profileAllowsPortalCall, profileRequiresPortalApproval, redactPortalSecrets, registerMcpPortalPlugin, validatePortalPluginApi, validatePortalPortAgainstTcpPool };
|
|
231
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/openclaw-plugin-api.ts","../src/plugin-registration.ts","../src/hmac-key-registry.ts","../src/portal-plugin-runtime-state.ts","../src/before-prompt-build-handler.ts","../src/before-tool-call-handler.ts","../src/portal-config.ts","../src/portal-subprocess-supervisor.ts","../src/portal-tool-policy.ts","../src/portal-prompt-context.ts","../src/redaction.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { loadMcpPortalConfig, mcpPortalCallRequiresApproval, resolveMcpPortalProfile } from "@agent-vm/config-contracts";
|
|
2
|
+
import { hashCallArguments, portalHmacKeyEnvName, redactCredentialText, signApprovalToken } from "@agent-vm/mcp-portal";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
//#region src/before-prompt-build-handler.ts
|
|
8
|
+
function createBeforePromptBuildHandler(props) {
|
|
9
|
+
return async (_event, context) => {
|
|
10
|
+
const agentId = context.agentId;
|
|
11
|
+
if (agentId === void 0) return;
|
|
12
|
+
const portalConfig = await props.runtimeState.loadPortalConfig();
|
|
13
|
+
const agent = portalConfig.agents[agentId];
|
|
14
|
+
if (agent === void 0) return;
|
|
15
|
+
const profile = resolveMcpPortalProfile(portalConfig, agent.profile);
|
|
16
|
+
if (!profile.promptContext.enabled) return;
|
|
17
|
+
const namespaces = profile.enabledNamespaces.toSorted().slice(0, profile.promptContext.maxNamespaces);
|
|
18
|
+
return { appendSystemContext: [
|
|
19
|
+
"MCP Portal namespaces available to this agent:",
|
|
20
|
+
namespaces.length === 0 ? " (none in your profile)" : namespaces.map((name) => ` ${name}`).join("\n"),
|
|
21
|
+
"Use mcp_portal_search to find tools by intent, then mcp_portal_describe before mcp_portal_call."
|
|
22
|
+
].join("\n") };
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/portal-tool-policy.ts
|
|
27
|
+
function encodePortalServerNameSegment(value) {
|
|
28
|
+
const encodedCharacters = [];
|
|
29
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
30
|
+
const character = value.charAt(index);
|
|
31
|
+
if (/^[A-Za-z0-9]$/u.test(character)) encodedCharacters.push(character);
|
|
32
|
+
else encodedCharacters.push(`_${character.charCodeAt(0).toString(16).padStart(2, "0")}_`);
|
|
33
|
+
}
|
|
34
|
+
return encodedCharacters.join("");
|
|
35
|
+
}
|
|
36
|
+
function portalServerNameForAgent(agentId) {
|
|
37
|
+
return `mcp_portal_${encodePortalServerNameSegment(agentId)}`;
|
|
38
|
+
}
|
|
39
|
+
function materializedPortalToolNames(serverName) {
|
|
40
|
+
return [
|
|
41
|
+
`${serverName}__mcp_portal_list`,
|
|
42
|
+
`${serverName}__mcp_portal_search`,
|
|
43
|
+
`${serverName}__mcp_portal_describe`,
|
|
44
|
+
`${serverName}__mcp_portal_call`
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function profileAllowsPortalCall(profile, call) {
|
|
48
|
+
if (!profile.enabledNamespaces.includes(call.namespace)) return false;
|
|
49
|
+
const enabledTools = profile.enabledToolsByNamespace[call.namespace] ?? [];
|
|
50
|
+
if (enabledTools.length > 0 && !enabledTools.includes(call.toolName)) return false;
|
|
51
|
+
return !(profile.hiddenToolsByNamespace[call.namespace] ?? []).includes(call.toolName);
|
|
52
|
+
}
|
|
53
|
+
function profileRequiresPortalApproval(profile, call) {
|
|
54
|
+
return mcpPortalCallRequiresApproval(profile, call);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/before-tool-call-handler.ts
|
|
58
|
+
const approvalTokenTtlMs = 6e4;
|
|
59
|
+
function isObjectRecord$1(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
function parseCallRequest(value) {
|
|
63
|
+
if (!isObjectRecord$1(value)) return null;
|
|
64
|
+
const id = value.id;
|
|
65
|
+
const namespace = value.namespace;
|
|
66
|
+
const toolName = value.toolName;
|
|
67
|
+
const argumentsValue = value.arguments;
|
|
68
|
+
if (typeof id !== "string" || typeof namespace !== "string" || typeof toolName !== "string" || !isObjectRecord$1(argumentsValue)) return null;
|
|
69
|
+
return {
|
|
70
|
+
arguments: argumentsValue,
|
|
71
|
+
id,
|
|
72
|
+
namespace,
|
|
73
|
+
toolName
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function portalAgentIdFromToolName(toolName, agentIds) {
|
|
77
|
+
return agentIds.find((agentId) => toolName.startsWith(`${portalServerNameForAgent(agentId)}__mcp_portal_`)) ?? null;
|
|
78
|
+
}
|
|
79
|
+
function parseCallRequests(params) {
|
|
80
|
+
const calls = params.calls;
|
|
81
|
+
if (!Array.isArray(calls)) return null;
|
|
82
|
+
const parsedCalls = [];
|
|
83
|
+
for (const call of calls) {
|
|
84
|
+
const parsedCall = parseCallRequest(call);
|
|
85
|
+
if (parsedCall === null) return null;
|
|
86
|
+
parsedCalls.push(parsedCall);
|
|
87
|
+
}
|
|
88
|
+
return parsedCalls;
|
|
89
|
+
}
|
|
90
|
+
function errorMessage$1(error) {
|
|
91
|
+
return error instanceof Error ? error.message : String(error);
|
|
92
|
+
}
|
|
93
|
+
function createBeforeToolCallHandler(props) {
|
|
94
|
+
return async (event, context) => {
|
|
95
|
+
const portalConfig = await props.runtimeState.loadPortalConfig();
|
|
96
|
+
const agentId = portalAgentIdFromToolName(event.toolName, Object.keys(portalConfig.agents));
|
|
97
|
+
if (agentId === null) return;
|
|
98
|
+
const portalUnavailableReason = props.runtimeState.getPortalUnavailableReason();
|
|
99
|
+
if (portalUnavailableReason !== null) return {
|
|
100
|
+
block: true,
|
|
101
|
+
blockReason: `mcp-portal: portal subprocess unavailable (${portalUnavailableReason}).`
|
|
102
|
+
};
|
|
103
|
+
if (context.agentId === void 0) return {
|
|
104
|
+
block: true,
|
|
105
|
+
blockReason: `mcp-portal: missing OpenClaw agent context for ${event.toolName}.`
|
|
106
|
+
};
|
|
107
|
+
if (context.agentId !== agentId) return {
|
|
108
|
+
block: true,
|
|
109
|
+
blockReason: `mcp-portal: tool ${event.toolName} is not assigned to agent ${context.agentId}.`
|
|
110
|
+
};
|
|
111
|
+
if (!event.toolName.endsWith("__mcp_portal_call")) return;
|
|
112
|
+
const agent = portalConfig.agents[agentId];
|
|
113
|
+
if (agent === void 0) return {
|
|
114
|
+
block: true,
|
|
115
|
+
blockReason: `mcp-portal: agent "${agentId}" is not configured.`
|
|
116
|
+
};
|
|
117
|
+
const profile = resolveMcpPortalProfile(portalConfig, agent.profile);
|
|
118
|
+
const calls = parseCallRequests(event.params);
|
|
119
|
+
if (calls === null || calls.length === 0) return {
|
|
120
|
+
block: true,
|
|
121
|
+
blockReason: "mcp-portal: malformed portal call batch."
|
|
122
|
+
};
|
|
123
|
+
for (const call of calls) if (!profileAllowsPortalCall(profile, call)) return {
|
|
124
|
+
block: true,
|
|
125
|
+
blockReason: `policy: ${agentId}/${call.namespace}/${call.toolName} not enabled`
|
|
126
|
+
};
|
|
127
|
+
const approvalCalls = calls.filter((call) => profileRequiresPortalApproval(profile, call));
|
|
128
|
+
if (approvalCalls.length === 0) return;
|
|
129
|
+
const token = signApprovalToken({
|
|
130
|
+
agentId,
|
|
131
|
+
calls: approvalCalls.map((call) => ({
|
|
132
|
+
argumentsHash: hashCallArguments(call.arguments),
|
|
133
|
+
namespace: call.namespace,
|
|
134
|
+
toolName: call.toolName
|
|
135
|
+
})),
|
|
136
|
+
expiresAtMs: Date.now() + approvalTokenTtlMs,
|
|
137
|
+
key: props.runtimeState.getKeyRegistry().getKey(agentId)
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
event.params.portalApprovalToken = token;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
props.logger?.warn?.(`[mcp-portal] could not attach server-side approval token: ${errorMessage$1(error)}`);
|
|
143
|
+
return {
|
|
144
|
+
block: true,
|
|
145
|
+
blockReason: "mcp-portal: could not attach server-side approval token."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (event.params.portalApprovalToken !== token) return {
|
|
149
|
+
block: true,
|
|
150
|
+
blockReason: "mcp-portal: could not attach server-side approval token."
|
|
151
|
+
};
|
|
152
|
+
const toolNames = approvalCalls.map((call) => `${call.namespace}.${call.toolName}`).toSorted().join(", ");
|
|
153
|
+
return { requireApproval: {
|
|
154
|
+
description: `Allow MCP Portal batch for agent ${agentId}: ${toolNames}.`,
|
|
155
|
+
pluginId: "mcp-portal",
|
|
156
|
+
severity: "warning",
|
|
157
|
+
timeoutBehavior: "deny",
|
|
158
|
+
timeoutMs: 6e4,
|
|
159
|
+
title: `MCP Portal batch: ${toolNames}`
|
|
160
|
+
} };
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/hmac-key-registry.ts
|
|
165
|
+
const hmacKeyBytes = 32;
|
|
166
|
+
function createHmacKeyRegistry(props) {
|
|
167
|
+
const keysByAgent = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const agentId of props.agentIds) keysByAgent.set(agentId, randomBytes(hmacKeyBytes));
|
|
169
|
+
return {
|
|
170
|
+
agentIds: [...props.agentIds],
|
|
171
|
+
getKey: (agentId) => {
|
|
172
|
+
const key = keysByAgent.get(agentId);
|
|
173
|
+
if (key === void 0) throw new Error(`HMAC key registry: unknown agent "${agentId}".`);
|
|
174
|
+
return key;
|
|
175
|
+
},
|
|
176
|
+
serializeForEnv: () => Object.fromEntries([...keysByAgent.entries()].map(([agentId, key]) => [portalHmacKeyEnvName(agentId), key.toString("hex")]))
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/portal-config.ts
|
|
181
|
+
const defaultPortalBinPath = "/opt/agent-vm/portal/bin/agent-vm-mcp-portal-server";
|
|
182
|
+
const portalPluginConfigSchema = z.object({
|
|
183
|
+
binPath: z.string().min(1).default(defaultPortalBinPath),
|
|
184
|
+
configDir: z.string().min(1).optional()
|
|
185
|
+
}).strict();
|
|
186
|
+
function parsePortalConfig(value) {
|
|
187
|
+
return portalPluginConfigSchema.parse(value ?? {});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/portal-plugin-runtime-state.ts
|
|
191
|
+
function createPortalPluginRuntimeState(props) {
|
|
192
|
+
let keyRegistry = null;
|
|
193
|
+
let portalConfigPromise = null;
|
|
194
|
+
let portalUnavailableReason = null;
|
|
195
|
+
const loadPortalConfigFile = props.loadPortalConfig ?? loadMcpPortalConfig;
|
|
196
|
+
const portalConfigPath = join(props.configDir, "mcp-portal.config.jsonc");
|
|
197
|
+
function loadPortalConfig() {
|
|
198
|
+
if (portalConfigPromise !== null) return portalConfigPromise;
|
|
199
|
+
const nextPromise = loadPortalConfigFile(portalConfigPath).catch((error) => {
|
|
200
|
+
if (portalConfigPromise === nextPromise) portalConfigPromise = null;
|
|
201
|
+
throw error;
|
|
202
|
+
});
|
|
203
|
+
portalConfigPromise = nextPromise;
|
|
204
|
+
return nextPromise;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
configDir: props.configDir,
|
|
208
|
+
getPortalUnavailableReason: () => portalUnavailableReason,
|
|
209
|
+
getKeyRegistry: () => {
|
|
210
|
+
if (keyRegistry === null) throw new Error("MCP Portal HMAC key registry is not initialized.");
|
|
211
|
+
return keyRegistry;
|
|
212
|
+
},
|
|
213
|
+
loadPortalConfig,
|
|
214
|
+
markPortalAvailable: () => {
|
|
215
|
+
portalUnavailableReason = null;
|
|
216
|
+
},
|
|
217
|
+
markPortalUnavailable: (reason) => {
|
|
218
|
+
portalUnavailableReason = reason;
|
|
219
|
+
},
|
|
220
|
+
setKeyRegistry: (registry) => {
|
|
221
|
+
keyRegistry = registry;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/portal-subprocess-supervisor.ts
|
|
227
|
+
const defaultBackoffSteps = [
|
|
228
|
+
200,
|
|
229
|
+
400,
|
|
230
|
+
800,
|
|
231
|
+
1600,
|
|
232
|
+
3200,
|
|
233
|
+
5e3
|
|
234
|
+
];
|
|
235
|
+
const inheritedPortalEnvNames = [
|
|
236
|
+
"HOME",
|
|
237
|
+
"PATH",
|
|
238
|
+
"TEMP",
|
|
239
|
+
"TMP",
|
|
240
|
+
"TMPDIR"
|
|
241
|
+
];
|
|
242
|
+
function createPortalSubprocessEnv(hmacEnv) {
|
|
243
|
+
const env = {};
|
|
244
|
+
for (const name of inheritedPortalEnvNames) {
|
|
245
|
+
const value = process.env[name];
|
|
246
|
+
if (value !== void 0) env[name] = value;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...env,
|
|
250
|
+
...hmacEnv
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function logSubprocessOutput(props) {
|
|
254
|
+
const text = String(props.chunk);
|
|
255
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
256
|
+
if (line.length === 0) continue;
|
|
257
|
+
const message = `[mcp-portal ${props.streamName}] ${line}`;
|
|
258
|
+
if (props.streamName === "stderr") props.logger.warn(message);
|
|
259
|
+
else props.logger.info(message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function delay(ms) {
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
setTimeout(resolve, ms);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async function waitForExit(child, timeoutMs) {
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
let settled = false;
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
if (settled) return;
|
|
272
|
+
settled = true;
|
|
273
|
+
child.off("exit", handleExit);
|
|
274
|
+
resolve(false);
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
const handleExit = () => {
|
|
277
|
+
if (settled) return;
|
|
278
|
+
settled = true;
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
resolve(true);
|
|
281
|
+
};
|
|
282
|
+
child.once("exit", handleExit);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async function waitForHealthAttempt(props) {
|
|
286
|
+
if (Date.now() - props.startedAt > props.timeoutMs) {
|
|
287
|
+
const message = props.lastError instanceof Error ? props.lastError.message : String(props.lastError);
|
|
288
|
+
throw new Error(`Timed out waiting for MCP Portal health: ${message}`);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const response = await props.fetchFn(`http://${props.host}:${String(props.port)}/health`);
|
|
292
|
+
if (response.ok) return;
|
|
293
|
+
await delay(props.intervalMs);
|
|
294
|
+
return waitForHealthAttempt({
|
|
295
|
+
...props,
|
|
296
|
+
lastError: /* @__PURE__ */ new Error(`health returned ${String(response.status)}`)
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
await delay(props.intervalMs);
|
|
300
|
+
return waitForHealthAttempt({
|
|
301
|
+
...props,
|
|
302
|
+
lastError: error
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function waitForHealth(props) {
|
|
307
|
+
const startedAt = Date.now();
|
|
308
|
+
return waitForHealthAttempt({
|
|
309
|
+
...props,
|
|
310
|
+
lastError: void 0,
|
|
311
|
+
startedAt
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function errorMessage(error) {
|
|
315
|
+
return error instanceof Error ? error.message : String(error);
|
|
316
|
+
}
|
|
317
|
+
function createPortalSubprocessSupervisor(props) {
|
|
318
|
+
const spawnFn = props.spawnFn ?? ((command, args, options) => spawn(command, [...args], options));
|
|
319
|
+
const fetchFn = props.fetchFn ?? fetch;
|
|
320
|
+
const healthPollIntervalMs = props.healthPollIntervalMs ?? 200;
|
|
321
|
+
const healthTimeoutMs = props.healthTimeoutMs ?? 1e4;
|
|
322
|
+
const stopGraceMs = props.stopGraceMs ?? 5e3;
|
|
323
|
+
const maxRestarts = props.maxRestarts ?? 5;
|
|
324
|
+
const backoffSteps = props.backoffSteps ?? defaultBackoffSteps;
|
|
325
|
+
let child = null;
|
|
326
|
+
let stopping = false;
|
|
327
|
+
let restartCount = 0;
|
|
328
|
+
const spawnChild = () => {
|
|
329
|
+
const nextChild = spawnFn(props.binPath, ["--config-dir", props.configDir], {
|
|
330
|
+
env: createPortalSubprocessEnv(props.hmacEnv),
|
|
331
|
+
stdio: [
|
|
332
|
+
"ignore",
|
|
333
|
+
"pipe",
|
|
334
|
+
"pipe"
|
|
335
|
+
]
|
|
336
|
+
});
|
|
337
|
+
let autoRestartEnabled = false;
|
|
338
|
+
let failureHandled = false;
|
|
339
|
+
let rejectEarlyFailure;
|
|
340
|
+
const earlyFailure = new Promise((_resolve, reject) => {
|
|
341
|
+
rejectEarlyFailure = reject;
|
|
342
|
+
});
|
|
343
|
+
const rejectBeforeHealth = (error) => {
|
|
344
|
+
if (rejectEarlyFailure === void 0) throw new Error("MCP Portal early-failure rejector was not initialized.");
|
|
345
|
+
rejectEarlyFailure(error);
|
|
346
|
+
};
|
|
347
|
+
child = nextChild;
|
|
348
|
+
nextChild.stdout?.on("data", (chunk) => {
|
|
349
|
+
logSubprocessOutput({
|
|
350
|
+
chunk,
|
|
351
|
+
logger: props.logger,
|
|
352
|
+
streamName: "stdout"
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
nextChild.stdout?.on("error", (error) => {
|
|
356
|
+
props.logger.warn(`[mcp-portal stdout] stream error: ${error.message}`);
|
|
357
|
+
});
|
|
358
|
+
nextChild.stderr?.on("data", (chunk) => {
|
|
359
|
+
logSubprocessOutput({
|
|
360
|
+
chunk,
|
|
361
|
+
logger: props.logger,
|
|
362
|
+
streamName: "stderr"
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
nextChild.stderr?.on("error", (error) => {
|
|
366
|
+
props.logger.warn(`[mcp-portal stderr] stream error: ${error.message}`);
|
|
367
|
+
});
|
|
368
|
+
nextChild.on("error", (error) => {
|
|
369
|
+
props.logger.error(`[mcp-portal] subprocess spawn failed: ${error.message}`);
|
|
370
|
+
if (failureHandled) return;
|
|
371
|
+
failureHandled = true;
|
|
372
|
+
if (child === nextChild) child = null;
|
|
373
|
+
if (stopping) return;
|
|
374
|
+
if (autoRestartEnabled) scheduleRestart();
|
|
375
|
+
else rejectBeforeHealth(error);
|
|
376
|
+
});
|
|
377
|
+
nextChild.on("exit", (code, signal) => {
|
|
378
|
+
if (failureHandled) return;
|
|
379
|
+
failureHandled = true;
|
|
380
|
+
if (child === nextChild) child = null;
|
|
381
|
+
if (stopping) return;
|
|
382
|
+
if (autoRestartEnabled) scheduleRestart();
|
|
383
|
+
else rejectBeforeHealth(/* @__PURE__ */ new Error(`MCP Portal subprocess exited before health check completed (code=${String(code)} signal=${String(signal)}).`));
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
child: nextChild,
|
|
387
|
+
earlyFailure,
|
|
388
|
+
enableAutoRestart: () => {
|
|
389
|
+
autoRestartEnabled = true;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
const spawnChildAndWaitForHealth = async () => {
|
|
394
|
+
const spawnedChild = spawnChild();
|
|
395
|
+
try {
|
|
396
|
+
await Promise.race([waitForHealth({
|
|
397
|
+
fetchFn,
|
|
398
|
+
host: props.host,
|
|
399
|
+
intervalMs: healthPollIntervalMs,
|
|
400
|
+
port: props.port,
|
|
401
|
+
timeoutMs: healthTimeoutMs
|
|
402
|
+
}), spawnedChild.earlyFailure]);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (child === spawnedChild.child) {
|
|
405
|
+
child = null;
|
|
406
|
+
if (!spawnedChild.child.killed) spawnedChild.child.kill("SIGTERM");
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
spawnedChild.enableAutoRestart();
|
|
411
|
+
restartCount = 0;
|
|
412
|
+
props.logger.info("[mcp-portal] subprocess is healthy.");
|
|
413
|
+
};
|
|
414
|
+
const scheduleRestart = async () => {
|
|
415
|
+
restartCount += 1;
|
|
416
|
+
if (restartCount > maxRestarts) {
|
|
417
|
+
props.logger.error("[mcp-portal] subprocess restart limit exhausted.");
|
|
418
|
+
props.onFatal?.("backoff-exhausted");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const backoffMs = backoffSteps[Math.min(restartCount - 1, backoffSteps.length - 1)] ?? backoffSteps[backoffSteps.length - 1] ?? 5e3;
|
|
422
|
+
props.logger.warn(`[mcp-portal] subprocess exited; restarting in ${String(backoffMs)}ms.`);
|
|
423
|
+
await delay(backoffMs);
|
|
424
|
+
if (stopping) return;
|
|
425
|
+
try {
|
|
426
|
+
await spawnChildAndWaitForHealth();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
props.logger.error(`[mcp-portal] subprocess restart failed: ${errorMessage(error)}`);
|
|
429
|
+
if (!stopping) await scheduleRestart();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
isAlive: () => child !== null && !child.killed,
|
|
434
|
+
start: async () => {
|
|
435
|
+
stopping = false;
|
|
436
|
+
await spawnChildAndWaitForHealth();
|
|
437
|
+
},
|
|
438
|
+
stop: async () => {
|
|
439
|
+
stopping = true;
|
|
440
|
+
const activeChild = child;
|
|
441
|
+
child = null;
|
|
442
|
+
if (activeChild === null || activeChild.killed) return;
|
|
443
|
+
activeChild.kill("SIGTERM");
|
|
444
|
+
if (!await waitForExit(activeChild, stopGraceMs) && !activeChild.killed) activeChild.kill("SIGKILL");
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/plugin-registration.ts
|
|
450
|
+
const pluginId = "mcp-portal";
|
|
451
|
+
function hasFunction(value) {
|
|
452
|
+
return typeof value === "function";
|
|
453
|
+
}
|
|
454
|
+
function isObjectRecord(value) {
|
|
455
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
456
|
+
}
|
|
457
|
+
function isUnknownArray(value) {
|
|
458
|
+
return Array.isArray(value);
|
|
459
|
+
}
|
|
460
|
+
function getObjectProperty(value, property) {
|
|
461
|
+
return isObjectRecord(value) ? value[property] : void 0;
|
|
462
|
+
}
|
|
463
|
+
function messageFromUnknown(error) {
|
|
464
|
+
return error instanceof Error ? error.message : String(error);
|
|
465
|
+
}
|
|
466
|
+
function resolveConfigDir(api) {
|
|
467
|
+
const pluginConfig = parsePortalConfig(api.pluginConfig ?? {});
|
|
468
|
+
if (pluginConfig.configDir !== void 0) return pluginConfig.configDir;
|
|
469
|
+
const topLevelMcpConfigDir = getObjectProperty(getObjectProperty(api.config, "mcp"), "configDir");
|
|
470
|
+
if (typeof topLevelMcpConfigDir === "string" && topLevelMcpConfigDir.length > 0) return topLevelMcpConfigDir;
|
|
471
|
+
const zones = getObjectProperty(api.config, "zones");
|
|
472
|
+
if (isUnknownArray(zones)) {
|
|
473
|
+
const zoneMcpConfigDir = getObjectProperty(getObjectProperty(zones.at(0), "mcp"), "configDir");
|
|
474
|
+
if (typeof zoneMcpConfigDir === "string" && zoneMcpConfigDir.length > 0) return zoneMcpConfigDir;
|
|
475
|
+
}
|
|
476
|
+
throw new Error("MCP Portal plugin requires configDir in plugin config or zone mcp config.");
|
|
477
|
+
}
|
|
478
|
+
function tcpPoolConfigFromApi(api) {
|
|
479
|
+
const tcpPool = getObjectProperty(api.config, "tcpPool");
|
|
480
|
+
const basePort = getObjectProperty(tcpPool, "basePort");
|
|
481
|
+
const size = getObjectProperty(tcpPool, "size");
|
|
482
|
+
return typeof basePort === "number" && typeof size === "number" ? {
|
|
483
|
+
basePort,
|
|
484
|
+
size
|
|
485
|
+
} : null;
|
|
486
|
+
}
|
|
487
|
+
function validatePortalPortAgainstTcpPool(props) {
|
|
488
|
+
if (props.tcpPool === null) return;
|
|
489
|
+
const firstTcpPoolPort = props.tcpPool.basePort;
|
|
490
|
+
const lastTcpPoolPortExclusive = props.tcpPool.basePort + props.tcpPool.size;
|
|
491
|
+
if (props.port >= firstTcpPoolPort && props.port < lastTcpPoolPortExclusive) throw new Error(`MCP Portal port ${String(props.port)} overlaps the Tool VM TCP pool [${String(firstTcpPoolPort)}, ${String(lastTcpPoolPortExclusive)}).`);
|
|
492
|
+
}
|
|
493
|
+
function createLoggerAdapter(api) {
|
|
494
|
+
return {
|
|
495
|
+
error: (message) => api.logger?.error?.(message),
|
|
496
|
+
info: (message) => api.logger?.info?.(message),
|
|
497
|
+
warn: (message) => api.logger?.warn?.(message)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function validatePortalPluginApi(api) {
|
|
501
|
+
if (!hasFunction(api.registerService)) throw new Error("MCP Portal plugin requires OpenClaw registerService API.");
|
|
502
|
+
if (!hasFunction(api.on) && !hasFunction(api.registerPromptHook)) throw new Error("MCP Portal plugin requires OpenClaw prompt hook registration API.");
|
|
503
|
+
if (!hasFunction(api.onDispose)) throw new Error("MCP Portal plugin requires an OpenClaw lifecycle cleanup API.");
|
|
504
|
+
}
|
|
505
|
+
function registerPortalService(props) {
|
|
506
|
+
const portalConfig = parsePortalConfig(props.api.pluginConfig ?? {});
|
|
507
|
+
let supervisor = null;
|
|
508
|
+
props.api.registerService?.({
|
|
509
|
+
id: "mcp-portal-subprocess",
|
|
510
|
+
start: async () => {
|
|
511
|
+
const mcpPortalConfig = await props.runtimeState.loadPortalConfig();
|
|
512
|
+
validatePortalPortAgainstTcpPool({
|
|
513
|
+
port: mcpPortalConfig.server.port,
|
|
514
|
+
tcpPool: tcpPoolConfigFromApi(props.api)
|
|
515
|
+
});
|
|
516
|
+
const keyRegistry = createHmacKeyRegistry({ agentIds: Object.keys(mcpPortalConfig.agents).toSorted() });
|
|
517
|
+
props.runtimeState.setKeyRegistry(keyRegistry);
|
|
518
|
+
supervisor = createPortalSubprocessSupervisor({
|
|
519
|
+
binPath: portalConfig.binPath,
|
|
520
|
+
configDir: props.configDir,
|
|
521
|
+
host: mcpPortalConfig.server.host,
|
|
522
|
+
hmacEnv: keyRegistry.serializeForEnv(),
|
|
523
|
+
logger: createLoggerAdapter(props.api),
|
|
524
|
+
onFatal: (reason) => {
|
|
525
|
+
props.runtimeState.markPortalUnavailable(reason);
|
|
526
|
+
props.api.logger?.error?.(`[mcp-portal] subprocess supervisor fatal: ${reason}`);
|
|
527
|
+
},
|
|
528
|
+
port: mcpPortalConfig.server.port
|
|
529
|
+
});
|
|
530
|
+
await supervisor.start();
|
|
531
|
+
props.runtimeState.markPortalAvailable();
|
|
532
|
+
},
|
|
533
|
+
stop: async () => {
|
|
534
|
+
await supervisor?.stop();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return { getSupervisor: () => supervisor };
|
|
538
|
+
}
|
|
539
|
+
function registerMcpPortalPlugin(api) {
|
|
540
|
+
if (api.registrationMode !== void 0 && api.registrationMode !== "full") return;
|
|
541
|
+
validatePortalPluginApi(api);
|
|
542
|
+
const configDir = resolveConfigDir(api);
|
|
543
|
+
const runtimeState = createPortalPluginRuntimeState({ configDir });
|
|
544
|
+
const registeredService = registerPortalService({
|
|
545
|
+
api,
|
|
546
|
+
configDir,
|
|
547
|
+
runtimeState
|
|
548
|
+
});
|
|
549
|
+
api.on?.("before_tool_call", createBeforeToolCallHandler({
|
|
550
|
+
logger: createLoggerAdapter(api),
|
|
551
|
+
runtimeState
|
|
552
|
+
}), { priority: 80 });
|
|
553
|
+
api.on?.("before_prompt_build", createBeforePromptBuildHandler({ runtimeState }), { priority: 80 });
|
|
554
|
+
if (!api.on && api.registerPromptHook) api.registerPromptHook("before_prompt_build", async (context) => {
|
|
555
|
+
const result = await createBeforePromptBuildHandler({ runtimeState })({}, context);
|
|
556
|
+
if (result?.appendSystemContext !== void 0) context.appendPrompt?.(result.appendSystemContext);
|
|
557
|
+
});
|
|
558
|
+
api.onDispose?.(() => registeredService.getSupervisor()?.stop());
|
|
559
|
+
runtimeState.loadPortalConfig().catch((error) => {
|
|
560
|
+
api.logger?.error?.(`[mcp-portal] failed to initialize portal config: ${messageFromUnknown(error)}`);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const pluginEntry = {
|
|
564
|
+
description: "Supervises the MCP Portal subprocess and wires per-agent approval hooks.",
|
|
565
|
+
id: pluginId,
|
|
566
|
+
name: "MCP Portal",
|
|
567
|
+
register: registerMcpPortalPlugin
|
|
568
|
+
};
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/portal-prompt-context.ts
|
|
571
|
+
function createPortalPromptContext(props) {
|
|
572
|
+
const namespaceList = props.namespaces.length > 0 ? props.namespaces.map((entry) => `${entry.namespace}(${entry.toolCount} tools)`).join(", ") : "none configured";
|
|
573
|
+
const diagnostics = props.diagnostics !== void 0 && props.diagnostics.length > 0 ? [`Discovery diagnostics: ${props.diagnostics.map((entry) => `${entry.namespace}: ${entry.message}`).join("; ")}`] : [];
|
|
574
|
+
return [
|
|
575
|
+
"MCP Portal is available as an MCP server.",
|
|
576
|
+
"Use mcp_portal_list with requests[], mcp_portal_search with requests[],",
|
|
577
|
+
"mcp_portal_describe with requests[], and mcp_portal_call with calls[].",
|
|
578
|
+
"Responses are { ok, results, errors, diagnostics }; results is keyed by each request/call id and each value is discriminated by ok: true or ok: false.",
|
|
579
|
+
"Call upstream tools by namespace + toolName inside calls[].",
|
|
580
|
+
"Call mcp_portal_describe before mcp_portal_call unless you already saw the full schema for that tool in this portal session.",
|
|
581
|
+
"Gateway owns MCP auth.",
|
|
582
|
+
`Namespaces: ${namespaceList}`,
|
|
583
|
+
...diagnostics
|
|
584
|
+
].join("\n");
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/redaction.ts
|
|
588
|
+
function redactPortalSecrets(text, secretValues = []) {
|
|
589
|
+
return secretValues.filter((secretValue) => secretValue.length > 0).reduce((current, secretValue) => current.split(secretValue).join("[REDACTED]"), redactCredentialText(text));
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/index.ts
|
|
593
|
+
const OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME = "@agent-vm/openclaw-mcp-portal-plugin";
|
|
594
|
+
//#endregion
|
|
595
|
+
export { OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME, createBeforePromptBuildHandler, createBeforeToolCallHandler, createHmacKeyRegistry, createPortalPluginRuntimeState, createPortalPromptContext, createPortalSubprocessSupervisor, pluginEntry as default, defaultPortalBinPath, materializedPortalToolNames, parsePortalConfig, portalPluginConfigSchema, portalServerNameForAgent, profileAllowsPortalCall, profileRequiresPortalApproval, redactPortalSecrets, registerMcpPortalPlugin, validatePortalPluginApi, validatePortalPortAgainstTcpPool };
|
|
596
|
+
|
|
597
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["isObjectRecord","errorMessage"],"sources":["../src/before-prompt-build-handler.ts","../src/portal-tool-policy.ts","../src/before-tool-call-handler.ts","../src/hmac-key-registry.ts","../src/portal-config.ts","../src/portal-plugin-runtime-state.ts","../src/portal-subprocess-supervisor.ts","../src/plugin-registration.ts","../src/portal-prompt-context.ts","../src/redaction.ts","../src/index.ts"],"sourcesContent":["import { resolveMcpPortalProfile } from '@agent-vm/config-contracts';\n\nimport type {\n\tOpenClawBeforePromptBuildEvent,\n\tOpenClawPluginHookContext,\n\tOpenClawPromptHookResult,\n} from './openclaw-plugin-api.js';\nimport type { PortalPluginRuntimeState } from './portal-plugin-runtime-state.js';\n\nexport interface CreateBeforePromptBuildHandlerProps {\n\treadonly runtimeState: PortalPluginRuntimeState;\n}\n\nexport function createBeforePromptBuildHandler(\n\tprops: CreateBeforePromptBuildHandlerProps,\n): (\n\tevent: OpenClawBeforePromptBuildEvent,\n\tcontext: OpenClawPluginHookContext,\n) => Promise<OpenClawPromptHookResult | undefined> {\n\treturn async (_event, context) => {\n\t\tconst agentId = context.agentId;\n\t\tif (agentId === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst portalConfig = await props.runtimeState.loadPortalConfig();\n\t\tconst agent = portalConfig.agents[agentId];\n\t\tif (agent === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst profile = resolveMcpPortalProfile(portalConfig, agent.profile);\n\t\tif (!profile.promptContext.enabled) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst namespaces = profile.enabledNamespaces\n\t\t\t.toSorted()\n\t\t\t.slice(0, profile.promptContext.maxNamespaces);\n\t\tconst namespaceText =\n\t\t\tnamespaces.length === 0\n\t\t\t\t? ' (none in your profile)'\n\t\t\t\t: namespaces.map((name) => ` ${name}`).join('\\n');\n\t\treturn {\n\t\t\tappendSystemContext: [\n\t\t\t\t'MCP Portal namespaces available to this agent:',\n\t\t\t\tnamespaceText,\n\t\t\t\t'Use mcp_portal_search to find tools by intent, then mcp_portal_describe before mcp_portal_call.',\n\t\t\t].join('\\n'),\n\t\t};\n\t};\n}\n","import {\n\tmcpPortalCallRequiresApproval,\n\ttype ResolvedMcpPortalProfile,\n} from '@agent-vm/config-contracts';\n\nexport interface PortalCallRequest {\n\treadonly arguments: Record<string, unknown>;\n\treadonly id: string;\n\treadonly namespace: string;\n\treadonly toolName: string;\n}\n\nfunction encodePortalServerNameSegment(value: string): string {\n\tconst encodedCharacters: string[] = [];\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tconst character = value.charAt(index);\n\t\tif (/^[A-Za-z0-9]$/u.test(character)) {\n\t\t\tencodedCharacters.push(character);\n\t\t} else {\n\t\t\tencodedCharacters.push(`_${character.charCodeAt(0).toString(16).padStart(2, '0')}_`);\n\t\t}\n\t}\n\treturn encodedCharacters.join('');\n}\n\nexport function portalServerNameForAgent(agentId: string): string {\n\treturn `mcp_portal_${encodePortalServerNameSegment(agentId)}`;\n}\n\nexport function materializedPortalToolNames(serverName: string): readonly string[] {\n\treturn [\n\t\t`${serverName}__mcp_portal_list`,\n\t\t`${serverName}__mcp_portal_search`,\n\t\t`${serverName}__mcp_portal_describe`,\n\t\t`${serverName}__mcp_portal_call`,\n\t];\n}\n\nexport function profileAllowsPortalCall(\n\tprofile: ResolvedMcpPortalProfile,\n\tcall: { readonly namespace: string; readonly toolName: string },\n): boolean {\n\tif (!profile.enabledNamespaces.includes(call.namespace)) {\n\t\treturn false;\n\t}\n\tconst enabledTools = profile.enabledToolsByNamespace[call.namespace] ?? [];\n\tif (enabledTools.length > 0 && !enabledTools.includes(call.toolName)) {\n\t\treturn false;\n\t}\n\tconst hiddenTools = profile.hiddenToolsByNamespace[call.namespace] ?? [];\n\treturn !hiddenTools.includes(call.toolName);\n}\n\nexport function profileRequiresPortalApproval(\n\tprofile: ResolvedMcpPortalProfile,\n\tcall: { readonly namespace: string; readonly toolName: string },\n): boolean {\n\treturn mcpPortalCallRequiresApproval(profile, call);\n}\n","import { resolveMcpPortalProfile } from '@agent-vm/config-contracts';\nimport { hashCallArguments, signApprovalToken } from '@agent-vm/mcp-portal';\n\nimport type {\n\tOpenClawBeforeToolCallEvent,\n\tOpenClawBeforeToolCallResult,\n\tOpenClawPluginHookContext,\n} from './openclaw-plugin-api.js';\nimport type { PortalPluginRuntimeState } from './portal-plugin-runtime-state.js';\nimport {\n\tportalServerNameForAgent,\n\tprofileAllowsPortalCall,\n\tprofileRequiresPortalApproval,\n\ttype PortalCallRequest,\n} from './portal-tool-policy.js';\n\nconst approvalTokenTtlMs = 60_000;\n\nexport interface CreateBeforeToolCallHandlerProps {\n\treadonly logger?: {\n\t\treadonly warn?: (message: string) => void;\n\t};\n\treadonly runtimeState: PortalPluginRuntimeState;\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseCallRequest(value: unknown): PortalCallRequest | null {\n\tif (!isObjectRecord(value)) {\n\t\treturn null;\n\t}\n\tconst id = value.id;\n\tconst namespace = value.namespace;\n\tconst toolName = value.toolName;\n\tconst argumentsValue = value.arguments;\n\tif (\n\t\ttypeof id !== 'string' ||\n\t\ttypeof namespace !== 'string' ||\n\t\ttypeof toolName !== 'string' ||\n\t\t!isObjectRecord(argumentsValue)\n\t) {\n\t\treturn null;\n\t}\n\treturn { arguments: argumentsValue, id, namespace, toolName };\n}\n\nfunction portalAgentIdFromToolName(toolName: string, agentIds: readonly string[]): string | null {\n\treturn (\n\t\tagentIds.find((agentId) =>\n\t\t\ttoolName.startsWith(`${portalServerNameForAgent(agentId)}__mcp_portal_`),\n\t\t) ?? null\n\t);\n}\n\nfunction parseCallRequests(params: Record<string, unknown>): readonly PortalCallRequest[] | null {\n\tconst calls = params.calls;\n\tif (!Array.isArray(calls)) {\n\t\treturn null;\n\t}\n\tconst parsedCalls: PortalCallRequest[] = [];\n\tfor (const call of calls) {\n\t\tconst parsedCall = parseCallRequest(call);\n\t\tif (parsedCall === null) {\n\t\t\treturn null;\n\t\t}\n\t\tparsedCalls.push(parsedCall);\n\t}\n\treturn parsedCalls;\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nexport function createBeforeToolCallHandler(\n\tprops: CreateBeforeToolCallHandlerProps,\n): (\n\tevent: OpenClawBeforeToolCallEvent,\n\tcontext: OpenClawPluginHookContext,\n) => Promise<OpenClawBeforeToolCallResult | undefined> {\n\treturn async (event, context) => {\n\t\tconst portalConfig = await props.runtimeState.loadPortalConfig();\n\t\tconst agentId = portalAgentIdFromToolName(event.toolName, Object.keys(portalConfig.agents));\n\t\tif (agentId === null) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst portalUnavailableReason = props.runtimeState.getPortalUnavailableReason();\n\t\tif (portalUnavailableReason !== null) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\tblockReason: `mcp-portal: portal subprocess unavailable (${portalUnavailableReason}).`,\n\t\t\t};\n\t\t}\n\t\tif (context.agentId === undefined) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\tblockReason: `mcp-portal: missing OpenClaw agent context for ${event.toolName}.`,\n\t\t\t};\n\t\t}\n\t\tif (context.agentId !== agentId) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\tblockReason: `mcp-portal: tool ${event.toolName} is not assigned to agent ${context.agentId}.`,\n\t\t\t};\n\t\t}\n\t\tif (!event.toolName.endsWith('__mcp_portal_call')) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst agent = portalConfig.agents[agentId];\n\t\tif (agent === undefined) {\n\t\t\treturn { block: true, blockReason: `mcp-portal: agent \"${agentId}\" is not configured.` };\n\t\t}\n\t\tconst profile = resolveMcpPortalProfile(portalConfig, agent.profile);\n\t\tconst calls = parseCallRequests(event.params);\n\t\tif (calls === null || calls.length === 0) {\n\t\t\treturn { block: true, blockReason: 'mcp-portal: malformed portal call batch.' };\n\t\t}\n\n\t\tfor (const call of calls) {\n\t\t\tif (!profileAllowsPortalCall(profile, call)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\tblockReason: `policy: ${agentId}/${call.namespace}/${call.toolName} not enabled`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst approvalCalls = calls.filter((call) => profileRequiresPortalApproval(profile, call));\n\t\tif (approvalCalls.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst token = signApprovalToken({\n\t\t\tagentId,\n\t\t\tcalls: approvalCalls.map((call) => ({\n\t\t\t\targumentsHash: hashCallArguments(call.arguments),\n\t\t\t\tnamespace: call.namespace,\n\t\t\t\ttoolName: call.toolName,\n\t\t\t})),\n\t\t\texpiresAtMs: Date.now() + approvalTokenTtlMs,\n\t\t\tkey: props.runtimeState.getKeyRegistry().getKey(agentId),\n\t\t});\n\t\ttry {\n\t\t\tevent.params.portalApprovalToken = token;\n\t\t} catch (error) {\n\t\t\tprops.logger?.warn?.(\n\t\t\t\t`[mcp-portal] could not attach server-side approval token: ${errorMessage(error)}`,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\tblockReason: 'mcp-portal: could not attach server-side approval token.',\n\t\t\t};\n\t\t}\n\t\tif (event.params.portalApprovalToken !== token) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\tblockReason: 'mcp-portal: could not attach server-side approval token.',\n\t\t\t};\n\t\t}\n\n\t\tconst toolNames = approvalCalls\n\t\t\t.map((call) => `${call.namespace}.${call.toolName}`)\n\t\t\t.toSorted()\n\t\t\t.join(', ');\n\t\treturn {\n\t\t\trequireApproval: {\n\t\t\t\tdescription: `Allow MCP Portal batch for agent ${agentId}: ${toolNames}.`,\n\t\t\t\tpluginId: 'mcp-portal',\n\t\t\t\tseverity: 'warning',\n\t\t\t\ttimeoutBehavior: 'deny',\n\t\t\t\ttimeoutMs: 60_000,\n\t\t\t\ttitle: `MCP Portal batch: ${toolNames}`,\n\t\t\t},\n\t\t};\n\t};\n}\n","import { randomBytes } from 'node:crypto';\n\nimport { portalHmacKeyEnvName } from '@agent-vm/mcp-portal';\n\nconst hmacKeyBytes = 32;\n\nexport interface CreateHmacKeyRegistryProps {\n\treadonly agentIds: readonly string[];\n}\n\nexport interface HmacKeyRegistry {\n\treadonly agentIds: readonly string[];\n\treadonly getKey: (agentId: string) => Buffer;\n\treadonly serializeForEnv: () => Readonly<Record<string, string>>;\n}\n\nexport function createHmacKeyRegistry(props: CreateHmacKeyRegistryProps): HmacKeyRegistry {\n\tconst keysByAgent = new Map<string, Buffer>();\n\tfor (const agentId of props.agentIds) {\n\t\tkeysByAgent.set(agentId, randomBytes(hmacKeyBytes));\n\t}\n\n\treturn {\n\t\tagentIds: [...props.agentIds],\n\t\tgetKey: (agentId) => {\n\t\t\tconst key = keysByAgent.get(agentId);\n\t\t\tif (key === undefined) {\n\t\t\t\tthrow new Error(`HMAC key registry: unknown agent \"${agentId}\".`);\n\t\t\t}\n\t\t\treturn key;\n\t\t},\n\t\tserializeForEnv: () =>\n\t\t\tObject.fromEntries(\n\t\t\t\t[...keysByAgent.entries()].map(([agentId, key]) => [\n\t\t\t\t\tportalHmacKeyEnvName(agentId),\n\t\t\t\t\tkey.toString('hex'),\n\t\t\t\t]),\n\t\t\t),\n\t};\n}\n","import { z } from 'zod';\n\nexport const defaultPortalBinPath = '/opt/agent-vm/portal/bin/agent-vm-mcp-portal-server';\n\nexport const portalPluginConfigSchema = z\n\t.object({\n\t\tbinPath: z.string().min(1).default(defaultPortalBinPath),\n\t\tconfigDir: z.string().min(1).optional(),\n\t})\n\t.strict();\n\nexport type PortalPluginConfig = z.infer<typeof portalPluginConfigSchema>;\n\nexport function parsePortalConfig(value: unknown): PortalPluginConfig {\n\treturn portalPluginConfigSchema.parse(value ?? {});\n}\n","import { join } from 'node:path';\n\nimport { loadMcpPortalConfig, type McpPortalConfig } from '@agent-vm/config-contracts';\n\nimport type { HmacKeyRegistry } from './hmac-key-registry.js';\n\nexport interface PortalPluginRuntimeState {\n\treadonly configDir: string;\n\treadonly getPortalUnavailableReason: () => string | null;\n\treadonly getKeyRegistry: () => HmacKeyRegistry;\n\treadonly loadPortalConfig: () => Promise<McpPortalConfig>;\n\treadonly markPortalAvailable: () => void;\n\treadonly markPortalUnavailable: (reason: string) => void;\n\treadonly setKeyRegistry: (registry: HmacKeyRegistry) => void;\n}\n\nexport function createPortalPluginRuntimeState(props: {\n\treadonly configDir: string;\n\treadonly loadPortalConfig?: (path: string) => Promise<McpPortalConfig>;\n}): PortalPluginRuntimeState {\n\tlet keyRegistry: HmacKeyRegistry | null = null;\n\tlet portalConfigPromise: Promise<McpPortalConfig> | null = null;\n\tlet portalUnavailableReason: string | null = null;\n\tconst loadPortalConfigFile = props.loadPortalConfig ?? loadMcpPortalConfig;\n\tconst portalConfigPath = join(props.configDir, 'mcp-portal.config.jsonc');\n\n\tfunction loadPortalConfig(): Promise<McpPortalConfig> {\n\t\tif (portalConfigPromise !== null) {\n\t\t\treturn portalConfigPromise;\n\t\t}\n\t\tconst nextPromise = loadPortalConfigFile(portalConfigPath).catch((error: unknown) => {\n\t\t\tif (portalConfigPromise === nextPromise) {\n\t\t\t\tportalConfigPromise = null;\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tportalConfigPromise = nextPromise;\n\t\treturn nextPromise;\n\t}\n\n\treturn {\n\t\tconfigDir: props.configDir,\n\t\tgetPortalUnavailableReason: () => portalUnavailableReason,\n\t\tgetKeyRegistry: () => {\n\t\t\tif (keyRegistry === null) {\n\t\t\t\tthrow new Error('MCP Portal HMAC key registry is not initialized.');\n\t\t\t}\n\t\t\treturn keyRegistry;\n\t\t},\n\t\tloadPortalConfig,\n\t\tmarkPortalAvailable: () => {\n\t\t\tportalUnavailableReason = null;\n\t\t},\n\t\tmarkPortalUnavailable: (reason) => {\n\t\t\tportalUnavailableReason = reason;\n\t\t},\n\t\tsetKeyRegistry: (registry) => {\n\t\t\tkeyRegistry = registry;\n\t\t},\n\t};\n}\n","import { spawn } from 'node:child_process';\nimport type { ChildProcess, SpawnOptions } from 'node:child_process';\n\nexport interface PortalSubprocessLogger {\n\treadonly error: (message: string) => void;\n\treadonly info: (message: string) => void;\n\treadonly warn: (message: string) => void;\n}\n\nexport type PortalSubprocessSpawnFunction = (\n\tcommand: string,\n\targs: readonly string[],\n\toptions: SpawnOptions,\n) => ChildProcess;\n\nexport interface CreatePortalSubprocessSupervisorProps {\n\treadonly backoffSteps?: readonly number[];\n\treadonly binPath: string;\n\treadonly configDir: string;\n\treadonly fetchFn?: typeof fetch;\n\treadonly healthPollIntervalMs?: number;\n\treadonly healthTimeoutMs?: number;\n\treadonly host: string;\n\treadonly hmacEnv: Readonly<Record<string, string>>;\n\treadonly logger: PortalSubprocessLogger;\n\treadonly maxRestarts?: number;\n\treadonly onFatal?: (reason: string) => void;\n\treadonly port: number;\n\treadonly spawnFn?: PortalSubprocessSpawnFunction;\n\treadonly stopGraceMs?: number;\n}\n\nexport interface PortalSubprocessSupervisor {\n\treadonly isAlive: () => boolean;\n\treadonly start: () => Promise<void>;\n\treadonly stop: () => Promise<void>;\n}\n\nconst defaultBackoffSteps = [200, 400, 800, 1_600, 3_200, 5_000] as const;\nconst inheritedPortalEnvNames = ['HOME', 'PATH', 'TEMP', 'TMP', 'TMPDIR'] as const;\n\nfunction createPortalSubprocessEnv(\n\thmacEnv: Readonly<Record<string, string>>,\n): Readonly<Record<string, string>> {\n\tconst env: Record<string, string> = {};\n\tfor (const name of inheritedPortalEnvNames) {\n\t\tconst value = process.env[name];\n\t\tif (value !== undefined) {\n\t\t\tenv[name] = value;\n\t\t}\n\t}\n\treturn { ...env, ...hmacEnv };\n}\n\nfunction logSubprocessOutput(props: {\n\treadonly chunk: Buffer | string;\n\treadonly logger: PortalSubprocessLogger;\n\treadonly streamName: 'stderr' | 'stdout';\n}): void {\n\tconst text = String(props.chunk);\n\tfor (const line of text.split(/\\r?\\n/u)) {\n\t\tif (line.length === 0) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst message = `[mcp-portal ${props.streamName}] ${line}`;\n\t\tif (props.streamName === 'stderr') {\n\t\t\tprops.logger.warn(message);\n\t\t} else {\n\t\t\tprops.logger.info(message);\n\t\t}\n\t}\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => {\n\t\tsetTimeout(resolve, ms);\n\t});\n}\n\nasync function waitForExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {\n\treturn new Promise<boolean>((resolve) => {\n\t\tlet settled = false;\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tchild.off('exit', handleExit);\n\t\t\tresolve(false);\n\t\t}, timeoutMs);\n\t\tconst handleExit = (): void => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tresolve(true);\n\t\t};\n\t\tchild.once('exit', handleExit);\n\t});\n}\n\ninterface WaitForHealthProps {\n\treadonly fetchFn: typeof fetch;\n\treadonly host: string;\n\treadonly intervalMs: number;\n\treadonly port: number;\n\treadonly timeoutMs: number;\n}\n\nasync function waitForHealthAttempt(props: {\n\treadonly fetchFn: typeof fetch;\n\treadonly host: string;\n\treadonly intervalMs: number;\n\treadonly lastError: unknown;\n\treadonly port: number;\n\treadonly startedAt: number;\n\treadonly timeoutMs: number;\n}): Promise<void> {\n\tif (Date.now() - props.startedAt > props.timeoutMs) {\n\t\tconst message =\n\t\t\tprops.lastError instanceof Error ? props.lastError.message : String(props.lastError);\n\t\tthrow new Error(`Timed out waiting for MCP Portal health: ${message}`);\n\t}\n\ttry {\n\t\tconst response = await props.fetchFn(`http://${props.host}:${String(props.port)}/health`);\n\t\tif (response.ok) {\n\t\t\treturn;\n\t\t}\n\t\tawait delay(props.intervalMs);\n\t\treturn waitForHealthAttempt({\n\t\t\t...props,\n\t\t\tlastError: new Error(`health returned ${String(response.status)}`),\n\t\t});\n\t} catch (error) {\n\t\tawait delay(props.intervalMs);\n\t\treturn waitForHealthAttempt({ ...props, lastError: error });\n\t}\n}\n\nasync function waitForHealth(props: WaitForHealthProps): Promise<void> {\n\tconst startedAt = Date.now();\n\treturn waitForHealthAttempt({ ...props, lastError: undefined, startedAt });\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ninterface SpawnedPortalChild {\n\treadonly child: ChildProcess;\n\treadonly enableAutoRestart: () => void;\n\treadonly earlyFailure: Promise<never>;\n}\n\nexport function createPortalSubprocessSupervisor(\n\tprops: CreatePortalSubprocessSupervisorProps,\n): PortalSubprocessSupervisor {\n\tconst spawnFn: PortalSubprocessSpawnFunction =\n\t\tprops.spawnFn ?? ((command, args, options) => spawn(command, [...args], options));\n\tconst fetchFn = props.fetchFn ?? fetch;\n\tconst healthPollIntervalMs = props.healthPollIntervalMs ?? 200;\n\tconst healthTimeoutMs = props.healthTimeoutMs ?? 10_000;\n\tconst stopGraceMs = props.stopGraceMs ?? 5_000;\n\tconst maxRestarts = props.maxRestarts ?? 5;\n\tconst backoffSteps = props.backoffSteps ?? defaultBackoffSteps;\n\tlet child: ChildProcess | null = null;\n\tlet stopping = false;\n\tlet restartCount = 0;\n\n\tconst spawnChild = (): SpawnedPortalChild => {\n\t\tconst nextChild = spawnFn(props.binPath, ['--config-dir', props.configDir], {\n\t\t\tenv: createPortalSubprocessEnv(props.hmacEnv),\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t});\n\t\tlet autoRestartEnabled = false;\n\t\tlet failureHandled = false;\n\t\tlet rejectEarlyFailure: ((error: Error) => void) | undefined;\n\t\tconst earlyFailure = new Promise<never>((_resolve, reject) => {\n\t\t\trejectEarlyFailure = reject;\n\t\t});\n\t\tconst rejectBeforeHealth = (error: Error): void => {\n\t\t\tif (rejectEarlyFailure === undefined) {\n\t\t\t\tthrow new Error('MCP Portal early-failure rejector was not initialized.');\n\t\t\t}\n\t\t\trejectEarlyFailure(error);\n\t\t};\n\t\tchild = nextChild;\n\t\tnextChild.stdout?.on('data', (chunk: Buffer | string) => {\n\t\t\tlogSubprocessOutput({ chunk, logger: props.logger, streamName: 'stdout' });\n\t\t});\n\t\tnextChild.stdout?.on('error', (error: Error) => {\n\t\t\tprops.logger.warn(`[mcp-portal stdout] stream error: ${error.message}`);\n\t\t});\n\t\tnextChild.stderr?.on('data', (chunk: Buffer | string) => {\n\t\t\tlogSubprocessOutput({ chunk, logger: props.logger, streamName: 'stderr' });\n\t\t});\n\t\tnextChild.stderr?.on('error', (error: Error) => {\n\t\t\tprops.logger.warn(`[mcp-portal stderr] stream error: ${error.message}`);\n\t\t});\n\t\tnextChild.on('error', (error: Error) => {\n\t\t\tprops.logger.error(`[mcp-portal] subprocess spawn failed: ${error.message}`);\n\t\t\tif (failureHandled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailureHandled = true;\n\t\t\tif (child === nextChild) {\n\t\t\t\tchild = null;\n\t\t\t}\n\t\t\tif (stopping) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (autoRestartEnabled) {\n\t\t\t\tvoid scheduleRestart();\n\t\t\t} else {\n\t\t\t\trejectBeforeHealth(error);\n\t\t\t}\n\t\t});\n\t\tnextChild.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {\n\t\t\tif (failureHandled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailureHandled = true;\n\t\t\tif (child === nextChild) {\n\t\t\t\tchild = null;\n\t\t\t}\n\t\t\tif (stopping) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (autoRestartEnabled) {\n\t\t\t\tvoid scheduleRestart();\n\t\t\t} else {\n\t\t\t\trejectBeforeHealth(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`MCP Portal subprocess exited before health check completed (code=${String(code)} signal=${String(signal)}).`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\tchild: nextChild,\n\t\t\tearlyFailure,\n\t\t\tenableAutoRestart: () => {\n\t\t\t\tautoRestartEnabled = true;\n\t\t\t},\n\t\t};\n\t};\n\n\tconst spawnChildAndWaitForHealth = async (): Promise<void> => {\n\t\tconst spawnedChild = spawnChild();\n\t\ttry {\n\t\t\tawait Promise.race([\n\t\t\t\twaitForHealth({\n\t\t\t\t\tfetchFn,\n\t\t\t\t\thost: props.host,\n\t\t\t\t\tintervalMs: healthPollIntervalMs,\n\t\t\t\t\tport: props.port,\n\t\t\t\t\ttimeoutMs: healthTimeoutMs,\n\t\t\t\t}),\n\t\t\t\tspawnedChild.earlyFailure,\n\t\t\t]);\n\t\t} catch (error) {\n\t\t\tif (child === spawnedChild.child) {\n\t\t\t\tchild = null;\n\t\t\t\tif (!spawnedChild.child.killed) {\n\t\t\t\t\tspawnedChild.child.kill('SIGTERM');\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tspawnedChild.enableAutoRestart();\n\t\trestartCount = 0;\n\t\tprops.logger.info('[mcp-portal] subprocess is healthy.');\n\t};\n\n\tconst scheduleRestart = async (): Promise<void> => {\n\t\trestartCount += 1;\n\t\tif (restartCount > maxRestarts) {\n\t\t\tprops.logger.error('[mcp-portal] subprocess restart limit exhausted.');\n\t\t\tprops.onFatal?.('backoff-exhausted');\n\t\t\treturn;\n\t\t}\n\t\tconst backoffIndex = Math.min(restartCount - 1, backoffSteps.length - 1);\n\t\tconst backoffMs = backoffSteps[backoffIndex] ?? backoffSteps[backoffSteps.length - 1] ?? 5_000;\n\t\tprops.logger.warn(`[mcp-portal] subprocess exited; restarting in ${String(backoffMs)}ms.`);\n\t\tawait delay(backoffMs);\n\t\tif (stopping) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait spawnChildAndWaitForHealth();\n\t\t} catch (error) {\n\t\t\tprops.logger.error(`[mcp-portal] subprocess restart failed: ${errorMessage(error)}`);\n\t\t\tif (!stopping) {\n\t\t\t\tawait scheduleRestart();\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tisAlive: () => child !== null && !child.killed,\n\t\tstart: async () => {\n\t\t\tstopping = false;\n\t\t\tawait spawnChildAndWaitForHealth();\n\t\t},\n\t\tstop: async () => {\n\t\t\tstopping = true;\n\t\t\tconst activeChild = child;\n\t\t\tchild = null;\n\t\t\tif (activeChild === null || activeChild.killed) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tactiveChild.kill('SIGTERM');\n\t\t\tconst exited = await waitForExit(activeChild, stopGraceMs);\n\t\t\tif (!exited && !activeChild.killed) {\n\t\t\t\tactiveChild.kill('SIGKILL');\n\t\t\t}\n\t\t},\n\t};\n}\n","import { createBeforePromptBuildHandler } from './before-prompt-build-handler.js';\nimport { createBeforeToolCallHandler } from './before-tool-call-handler.js';\nimport { createHmacKeyRegistry } from './hmac-key-registry.js';\nimport type { OpenClawPortalPluginApi } from './openclaw-plugin-api.js';\nimport { parsePortalConfig } from './portal-config.js';\nimport {\n\tcreatePortalPluginRuntimeState,\n\ttype PortalPluginRuntimeState,\n} from './portal-plugin-runtime-state.js';\nimport {\n\tcreatePortalSubprocessSupervisor,\n\ttype PortalSubprocessSupervisor,\n} from './portal-subprocess-supervisor.js';\n\ninterface PortalPluginEntry {\n\treadonly description: string;\n\treadonly id: string;\n\treadonly name: string;\n\treadonly register: (api: OpenClawPortalPluginApi) => void;\n}\n\ninterface TcpPoolConfig {\n\treadonly basePort: number;\n\treadonly size: number;\n}\n\nconst pluginId = 'mcp-portal';\n\nfunction hasFunction(value: unknown): value is (...args: readonly unknown[]) => unknown {\n\treturn typeof value === 'function';\n}\n\nfunction isObjectRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n\treturn Array.isArray(value);\n}\n\nfunction getObjectProperty(value: unknown, property: string): unknown {\n\treturn isObjectRecord(value) ? value[property] : undefined;\n}\n\nfunction messageFromUnknown(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction resolveConfigDir(api: OpenClawPortalPluginApi): string {\n\tconst pluginConfig = parsePortalConfig(api.pluginConfig ?? {});\n\tif (pluginConfig.configDir !== undefined) {\n\t\treturn pluginConfig.configDir;\n\t}\n\tconst topLevelMcpConfigDir = getObjectProperty(getObjectProperty(api.config, 'mcp'), 'configDir');\n\tif (typeof topLevelMcpConfigDir === 'string' && topLevelMcpConfigDir.length > 0) {\n\t\treturn topLevelMcpConfigDir;\n\t}\n\tconst zones = getObjectProperty(api.config, 'zones');\n\tif (isUnknownArray(zones)) {\n\t\tconst firstZone = zones.at(0);\n\t\tconst zoneMcpConfigDir = getObjectProperty(getObjectProperty(firstZone, 'mcp'), 'configDir');\n\t\tif (typeof zoneMcpConfigDir === 'string' && zoneMcpConfigDir.length > 0) {\n\t\t\treturn zoneMcpConfigDir;\n\t\t}\n\t}\n\tthrow new Error('MCP Portal plugin requires configDir in plugin config or zone mcp config.');\n}\n\nfunction tcpPoolConfigFromApi(api: OpenClawPortalPluginApi): TcpPoolConfig | null {\n\tconst tcpPool = getObjectProperty(api.config, 'tcpPool');\n\tconst basePort = getObjectProperty(tcpPool, 'basePort');\n\tconst size = getObjectProperty(tcpPool, 'size');\n\treturn typeof basePort === 'number' && typeof size === 'number' ? { basePort, size } : null;\n}\n\nexport function validatePortalPortAgainstTcpPool(props: {\n\treadonly port: number;\n\treadonly tcpPool: TcpPoolConfig | null;\n}): void {\n\tif (props.tcpPool === null) {\n\t\treturn;\n\t}\n\tconst firstTcpPoolPort = props.tcpPool.basePort;\n\tconst lastTcpPoolPortExclusive = props.tcpPool.basePort + props.tcpPool.size;\n\tif (props.port >= firstTcpPoolPort && props.port < lastTcpPoolPortExclusive) {\n\t\tthrow new Error(\n\t\t\t`MCP Portal port ${String(props.port)} overlaps the Tool VM TCP pool ` +\n\t\t\t\t`[${String(firstTcpPoolPort)}, ${String(lastTcpPoolPortExclusive)}).`,\n\t\t);\n\t}\n}\n\nfunction createLoggerAdapter(api: OpenClawPortalPluginApi): {\n\treadonly error: (message: string) => void;\n\treadonly info: (message: string) => void;\n\treadonly warn: (message: string) => void;\n} {\n\treturn {\n\t\terror: (message) => api.logger?.error?.(message),\n\t\tinfo: (message) => api.logger?.info?.(message),\n\t\twarn: (message) => api.logger?.warn?.(message),\n\t};\n}\n\nexport function validatePortalPluginApi(api: OpenClawPortalPluginApi): void {\n\tif (!hasFunction(api.registerService)) {\n\t\tthrow new Error('MCP Portal plugin requires OpenClaw registerService API.');\n\t}\n\tif (!hasFunction(api.on) && !hasFunction(api.registerPromptHook)) {\n\t\tthrow new Error('MCP Portal plugin requires OpenClaw prompt hook registration API.');\n\t}\n\tif (!hasFunction(api.onDispose)) {\n\t\tthrow new Error('MCP Portal plugin requires an OpenClaw lifecycle cleanup API.');\n\t}\n}\n\nfunction registerPortalService(props: {\n\treadonly api: OpenClawPortalPluginApi;\n\treadonly configDir: string;\n\treadonly runtimeState: PortalPluginRuntimeState;\n}): { readonly getSupervisor: () => PortalSubprocessSupervisor | null } {\n\tconst portalConfig = parsePortalConfig(props.api.pluginConfig ?? {});\n\tlet supervisor: PortalSubprocessSupervisor | null = null;\n\n\tprops.api.registerService?.({\n\t\tid: 'mcp-portal-subprocess',\n\t\tstart: async () => {\n\t\t\tconst mcpPortalConfig = await props.runtimeState.loadPortalConfig();\n\t\t\tvalidatePortalPortAgainstTcpPool({\n\t\t\t\tport: mcpPortalConfig.server.port,\n\t\t\t\ttcpPool: tcpPoolConfigFromApi(props.api),\n\t\t\t});\n\t\t\tconst keyRegistry = createHmacKeyRegistry({\n\t\t\t\tagentIds: Object.keys(mcpPortalConfig.agents).toSorted(),\n\t\t\t});\n\t\t\tprops.runtimeState.setKeyRegistry(keyRegistry);\n\t\t\tsupervisor = createPortalSubprocessSupervisor({\n\t\t\t\tbinPath: portalConfig.binPath,\n\t\t\t\tconfigDir: props.configDir,\n\t\t\t\thost: mcpPortalConfig.server.host,\n\t\t\t\thmacEnv: keyRegistry.serializeForEnv(),\n\t\t\t\tlogger: createLoggerAdapter(props.api),\n\t\t\t\tonFatal: (reason) => {\n\t\t\t\t\tprops.runtimeState.markPortalUnavailable(reason);\n\t\t\t\t\tprops.api.logger?.error?.(`[mcp-portal] subprocess supervisor fatal: ${reason}`);\n\t\t\t\t},\n\t\t\t\tport: mcpPortalConfig.server.port,\n\t\t\t});\n\t\t\tawait supervisor.start();\n\t\t\tprops.runtimeState.markPortalAvailable();\n\t\t},\n\t\tstop: async () => {\n\t\t\tawait supervisor?.stop();\n\t\t},\n\t});\n\n\treturn { getSupervisor: () => supervisor };\n}\n\nexport function registerMcpPortalPlugin(api: OpenClawPortalPluginApi): void {\n\tif (api.registrationMode !== undefined && api.registrationMode !== 'full') {\n\t\treturn;\n\t}\n\tvalidatePortalPluginApi(api);\n\tconst configDir = resolveConfigDir(api);\n\tconst runtimeState = createPortalPluginRuntimeState({ configDir });\n\tconst registeredService = registerPortalService({ api, configDir, runtimeState });\n\n\tapi.on?.(\n\t\t'before_tool_call',\n\t\tcreateBeforeToolCallHandler({ logger: createLoggerAdapter(api), runtimeState }),\n\t\t{\n\t\t\tpriority: 80,\n\t\t},\n\t);\n\n\tapi.on?.('before_prompt_build', createBeforePromptBuildHandler({ runtimeState }), {\n\t\tpriority: 80,\n\t});\n\n\tif (!api.on && api.registerPromptHook) {\n\t\tapi.registerPromptHook('before_prompt_build', async (context) => {\n\t\t\tconst handler = createBeforePromptBuildHandler({ runtimeState });\n\t\t\tconst result = await handler({}, context);\n\t\t\tif (result?.appendSystemContext !== undefined) {\n\t\t\t\tcontext.appendPrompt?.(result.appendSystemContext);\n\t\t\t}\n\t\t});\n\t}\n\n\tapi.onDispose?.(() => registeredService.getSupervisor()?.stop());\n\tvoid runtimeState.loadPortalConfig().catch((error: unknown) => {\n\t\tapi.logger?.error?.(\n\t\t\t`[mcp-portal] failed to initialize portal config: ${messageFromUnknown(error)}`,\n\t\t);\n\t});\n}\n\nconst pluginEntry = {\n\tdescription: 'Supervises the MCP Portal subprocess and wires per-agent approval hooks.',\n\tid: pluginId,\n\tname: 'MCP Portal',\n\tregister: registerMcpPortalPlugin,\n} satisfies PortalPluginEntry;\n\nexport default pluginEntry;\n","export interface PortalPromptNamespaceSummary {\n\treadonly namespace: string;\n\treadonly toolCount: number;\n}\n\nexport interface PortalPromptDiagnostic {\n\treadonly message: string;\n\treadonly namespace: string;\n}\n\nexport function createPortalPromptContext(props: {\n\treadonly diagnostics?: readonly PortalPromptDiagnostic[];\n\treadonly namespaces: readonly PortalPromptNamespaceSummary[];\n}): string {\n\tconst namespaceList =\n\t\tprops.namespaces.length > 0\n\t\t\t? props.namespaces.map((entry) => `${entry.namespace}(${entry.toolCount} tools)`).join(', ')\n\t\t\t: 'none configured';\n\tconst diagnostics =\n\t\tprops.diagnostics !== undefined && props.diagnostics.length > 0\n\t\t\t? [\n\t\t\t\t\t`Discovery diagnostics: ${props.diagnostics\n\t\t\t\t\t\t.map((entry) => `${entry.namespace}: ${entry.message}`)\n\t\t\t\t\t\t.join('; ')}`,\n\t\t\t\t]\n\t\t\t: [];\n\n\treturn [\n\t\t'MCP Portal is available as an MCP server.',\n\t\t'Use mcp_portal_list with requests[], mcp_portal_search with requests[],',\n\t\t'mcp_portal_describe with requests[], and mcp_portal_call with calls[].',\n\t\t'Responses are { ok, results, errors, diagnostics }; results is keyed by each request/call id and each value is discriminated by ok: true or ok: false.',\n\t\t'Call upstream tools by namespace + toolName inside calls[].',\n\t\t'Call mcp_portal_describe before mcp_portal_call unless you already saw the full schema for that tool in this portal session.',\n\t\t'Gateway owns MCP auth.',\n\t\t`Namespaces: ${namespaceList}`,\n\t\t...diagnostics,\n\t].join('\\n');\n}\n","import { redactCredentialText } from '@agent-vm/mcp-portal';\n\nexport function redactPortalSecrets(text: string, secretValues: readonly string[] = []): string {\n\treturn secretValues\n\t\t.filter((secretValue) => secretValue.length > 0)\n\t\t.reduce(\n\t\t\t(current, secretValue) => current.split(secretValue).join('[REDACTED]'),\n\t\t\tredactCredentialText(text),\n\t\t);\n}\n","export * from './openclaw-plugin-api.js';\nexport * from './plugin-registration.js';\nexport * from './before-prompt-build-handler.js';\nexport * from './before-tool-call-handler.js';\nexport * from './hmac-key-registry.js';\nexport * from './portal-config.js';\nexport * from './portal-plugin-runtime-state.js';\nexport * from './portal-subprocess-supervisor.js';\nexport * from './portal-tool-policy.js';\nexport * from './portal-prompt-context.js';\nexport * from './redaction.js';\nexport { default } from './plugin-registration.js';\n\nexport const OPENCLAW_MCP_PORTAL_PLUGIN_PACKAGE_NAME = '@agent-vm/openclaw-mcp-portal-plugin';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "mcp-portal",
|
|
3
|
+
"name": "MCP Portal",
|
|
4
|
+
"description": "Managed per-agent MCP server facade over configured upstream MCP servers.",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"binPath": { "type": "string" },
|
|
13
|
+
"configDir": { "type": "string" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-vm/openclaw-mcp-portal-plugin",
|
|
3
|
+
"version": "0.0.59",
|
|
4
|
+
"description": "OpenClaw plugin that supervises per-agent MCP Portal endpoints over configured upstream MCP servers.",
|
|
5
|
+
"homepage": "https://github.com/ShravanSunder/agent-vm#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/ShravanSunder/agent-vm/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Shravan Sunder <ShravanSunder@users.noreply.github.com>",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ShravanSunder/agent-vm.git",
|
|
14
|
+
"directory": "packages/openclaw-mcp-portal-plugin"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zod": "^4.4.3",
|
|
33
|
+
"@agent-vm/config-contracts": "0.0.59",
|
|
34
|
+
"@agent-vm/mcp-portal": "0.0.59"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"vitest": "^4.1.5"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown && cp openclaw.plugin.json dist/",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"test": "vitest run --root ../../ --config vitest.config.ts packages/openclaw-mcp-portal-plugin/src"
|
|
43
|
+
}
|
|
44
|
+
}
|