180 lines
7.4 KiB
JavaScript
180 lines
7.4 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const { CAPABILITY_STATUS, CAPABILITY_SURFACES } = require("../../capabilities/constants.cjs");
|
||
|
|
const { getCapabilityByRpcMethod } = require("../../capabilities/registry.cjs");
|
||
|
|
const { getMcpToolNameForRpcMethod } = require("../../capabilities/adapters/mcpAdapter.cjs");
|
||
|
|
const { createVaultService } = require("../../capabilities/services/vaultService.cjs");
|
||
|
|
const { createPortForwardService } = require("../../capabilities/services/portforwardService.cjs");
|
||
|
|
const { createSessionService } = require("../../capabilities/services/sessionService.cjs");
|
||
|
|
|
||
|
|
const UNROUTED = Symbol("capability-rpc-unrouted");
|
||
|
|
|
||
|
|
const SERVICE_BINDINGS = Object.freeze({
|
||
|
|
"vault.host.get": { domain: "vault", method: "getHost" },
|
||
|
|
"vault.host.list": { domain: "vault", method: "listHosts" },
|
||
|
|
"vault.host.open": { domain: "vault", method: "openHost" },
|
||
|
|
"vault.hosts.create": { domain: "vault", method: "createHosts" },
|
||
|
|
"vault.host.update": { domain: "vault", method: "updateHost" },
|
||
|
|
"vault.host.delete": { domain: "vault", method: "deleteHost" },
|
||
|
|
"vault.host.import": { domain: "vault", method: "importHosts" },
|
||
|
|
"vault.host.notes.get": { domain: "vault", method: "getHostNotes" },
|
||
|
|
"vault.host.notes.set": { domain: "vault", method: "setHostNotes" },
|
||
|
|
"vault.note.list": { domain: "vault", method: "listNotes" },
|
||
|
|
"vault.note.get": { domain: "vault", method: "getNote" },
|
||
|
|
"vault.note.create": { domain: "vault", method: "createNote" },
|
||
|
|
"vault.note.update": { domain: "vault", method: "updateNote" },
|
||
|
|
"vault.note.delete": { domain: "vault", method: "deleteNote" },
|
||
|
|
"vault.identity.list": { domain: "vault", method: "listIdentities" },
|
||
|
|
"vault.proxyProfile.list": { domain: "vault", method: "listProxyProfiles" },
|
||
|
|
"vault.group.list": { domain: "vault", method: "listGroups" },
|
||
|
|
"vault.group.create": { domain: "vault", method: "createGroup" },
|
||
|
|
"vault.group.update": { domain: "vault", method: "updateGroup" },
|
||
|
|
"vault.group.delete": { domain: "vault", method: "deleteGroup" },
|
||
|
|
"vault.snippets.list": { domain: "vault", method: "listSnippets" },
|
||
|
|
"vault.snippets.get": { domain: "vault", method: "getSnippet" },
|
||
|
|
"vault.snippets.run": { domain: "vault", method: "runSnippet" },
|
||
|
|
"vault.snippets.create": { domain: "vault", method: "createSnippet" },
|
||
|
|
"vault.snippets.update": { domain: "vault", method: "updateSnippet" },
|
||
|
|
"vault.snippets.delete": { domain: "vault", method: "deleteSnippet" },
|
||
|
|
"vault.scripts.list": { domain: "vault", method: "listScripts" },
|
||
|
|
"vault.scripts.get": { domain: "vault", method: "getScript" },
|
||
|
|
"vault.scripts.create": { domain: "vault", method: "createScript" },
|
||
|
|
"vault.scripts.update": { domain: "vault", method: "updateScript" },
|
||
|
|
"vault.scripts.delete": { domain: "vault", method: "deleteScript" },
|
||
|
|
"vault.scripts.run": { domain: "vault", method: "runScript" },
|
||
|
|
"vault.scripts.reference": { domain: "vault", method: "getScriptReference" },
|
||
|
|
"vault.scripts.runs.list": { domain: "vault", method: "listScriptRuns" },
|
||
|
|
"vault.scripts.run.stop": { domain: "vault", method: "stopScriptRun" },
|
||
|
|
"vault.scripts.run.pause": { domain: "vault", method: "pauseScriptRun" },
|
||
|
|
"vault.scripts.run.resume": { domain: "vault", method: "resumeScriptRun" },
|
||
|
|
"vault.scripts.targets.set": { domain: "vault", method: "setScriptTargets" },
|
||
|
|
"vault.host.connectScripts.list": { domain: "vault", method: "listHostConnectScripts" },
|
||
|
|
"vault.host.connectScripts.set": { domain: "vault", method: "setHostConnectScripts" },
|
||
|
|
"portforward.rules.list": { domain: "portforward", method: "listRules" },
|
||
|
|
"portforward.rules.create": { domain: "portforward", method: "createRule" },
|
||
|
|
"portforward.rules.update": { domain: "portforward", method: "updateRule" },
|
||
|
|
"portforward.rules.duplicate": { domain: "portforward", method: "duplicateRule" },
|
||
|
|
"portforward.rules.delete": { domain: "portforward", method: "deleteRule" },
|
||
|
|
"portforward.tunnels.list": { domain: "portforward", method: "listTunnels" },
|
||
|
|
"portforward.start": { domain: "portforward", method: "start" },
|
||
|
|
"portforward.stop": { domain: "portforward", method: "stop" },
|
||
|
|
"session.close": { domain: "session", method: "close" },
|
||
|
|
});
|
||
|
|
|
||
|
|
function resolveCapabilitySurface(rpcMethod) {
|
||
|
|
for (const surface of [
|
||
|
|
CAPABILITY_SURFACES.PUBLIC,
|
||
|
|
CAPABILITY_SURFACES.GLOBAL,
|
||
|
|
CAPABILITY_SURFACES.BUILTIN,
|
||
|
|
]) {
|
||
|
|
if (rpcMethod.startsWith("netcatty/") && surface !== CAPABILITY_SURFACES.BUILTIN) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const capability = getCapabilityByRpcMethod(rpcMethod, surface);
|
||
|
|
if (capability) {
|
||
|
|
return { capability, surface };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createCapabilityRpcDispatcher(deps) {
|
||
|
|
const {
|
||
|
|
invokeVaultAgent,
|
||
|
|
evaluatePermissionWithGrants,
|
||
|
|
isChatSessionCancelled,
|
||
|
|
requestApprovalFromRenderer,
|
||
|
|
USER_DENIED_MESSAGE,
|
||
|
|
} = deps;
|
||
|
|
|
||
|
|
const vaultService = createVaultService({ invokeVaultAgent });
|
||
|
|
const portforwardService = createPortForwardService({
|
||
|
|
invokeVaultAgent,
|
||
|
|
listPortForwards: deps.listPortForwards,
|
||
|
|
});
|
||
|
|
const sessionService = deps.sessionService || createSessionService({
|
||
|
|
invokeSessionAgent: invokeVaultAgent,
|
||
|
|
validateClose: deps.validateSessionClose,
|
||
|
|
beforeClose: deps.beforeSessionClose,
|
||
|
|
afterClose: deps.afterSessionClose,
|
||
|
|
onClosed: deps.onSessionClosed,
|
||
|
|
});
|
||
|
|
const services = {
|
||
|
|
vault: vaultService,
|
||
|
|
portforward: portforwardService,
|
||
|
|
session: sessionService,
|
||
|
|
};
|
||
|
|
|
||
|
|
return async function dispatchCapabilityRpc(rpcMethod, params = {}) {
|
||
|
|
if (typeof rpcMethod !== "string" || rpcMethod.startsWith("netcatty/")) {
|
||
|
|
return UNROUTED;
|
||
|
|
}
|
||
|
|
|
||
|
|
const resolved = resolveCapabilitySurface(rpcMethod);
|
||
|
|
if (!resolved) {
|
||
|
|
return UNROUTED;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { capability, surface } = resolved;
|
||
|
|
if (capability.status !== CAPABILITY_STATUS.IMPLEMENTED) {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
code: "CAPABILITY_NOT_IMPLEMENTED",
|
||
|
|
error: `Capability "${capability.id}" is not implemented yet.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const binding = SERVICE_BINDINGS[capability.id];
|
||
|
|
if (!binding) {
|
||
|
|
return UNROUTED;
|
||
|
|
}
|
||
|
|
|
||
|
|
const permission = evaluatePermissionWithGrants({
|
||
|
|
rpcMethod,
|
||
|
|
surface,
|
||
|
|
permissionMode: deps.permissionMode,
|
||
|
|
params,
|
||
|
|
context: {
|
||
|
|
chatSessionCancelled: isChatSessionCancelled(params?.chatSessionId),
|
||
|
|
},
|
||
|
|
}, deps.permissionGrantsSnapshot);
|
||
|
|
|
||
|
|
if (!permission.allowed) {
|
||
|
|
return { ok: false, error: permission.error };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (permission.requiresApproval) {
|
||
|
|
const { chatSessionId, ...toolArgs } = params || {};
|
||
|
|
const toolName = getMcpToolNameForRpcMethod(rpcMethod, surface) || capability.id;
|
||
|
|
const approved = await requestApprovalFromRenderer(toolName, toolArgs, chatSessionId);
|
||
|
|
if (!approved) {
|
||
|
|
return { ok: false, error: USER_DENIED_MESSAGE };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const service = services[binding.domain];
|
||
|
|
const handler = service?.[binding.method];
|
||
|
|
if (typeof handler !== "function") {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
error: `Capability handler "${capability.id}" is unavailable.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const hostOpenGeneration = capability.id === "vault.host.open"
|
||
|
|
? deps.captureHostOpenScope?.(params?.chatSessionId)
|
||
|
|
: null;
|
||
|
|
const result = await handler(params);
|
||
|
|
if (capability.id === "vault.host.open" && result?.ok !== false && result?.sessionId) {
|
||
|
|
await deps.onHostOpened?.(params?.chatSessionId, result.sessionId, hostOpenGeneration, result);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
UNROUTED,
|
||
|
|
createCapabilityRpcDispatcher,
|
||
|
|
SERVICE_BINDINGS,
|
||
|
|
};
|