multicorn-shield 1.10.0 → 1.11.0
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/CHANGELOG.md +18 -0
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +10 -0
- package/dist/multicorn-proxy.js +1593 -164
- package/dist/multicorn-shield.js +1614 -197
- package/dist/proxy.cjs +2 -0
- package/dist/proxy.d.cts +3 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.js +2 -0
- package/dist/shield-extension.js +3 -1
- package/package.json +8 -3
package/dist/multicorn-shield.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync,
|
|
3
|
-
import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, openSync, realpathSync, unlinkSync, writeFileSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { readFile, mkdir, writeFile, copyFile, chmod, rename, rm, unlink } from 'fs/promises';
|
|
4
4
|
import { join, dirname, resolve, basename, sep } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { createInterface } from 'readline';
|
|
9
9
|
import { parse, stringify } from 'yaml';
|
|
10
|
-
import { spawn } from 'child_process';
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
12
|
import 'stream';
|
|
13
|
+
import { createConnection } from 'net';
|
|
13
14
|
|
|
14
15
|
var MULTICORN_DIR = join(homedir(), ".multicorn");
|
|
15
16
|
var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
|
|
@@ -232,6 +233,7 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
|
232
233
|
if (perm.revoked_at !== null) continue;
|
|
233
234
|
if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
|
|
234
235
|
if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
|
|
236
|
+
if (perm.delete === true) scopes.push({ service: perm.service, permissionLevel: "delete" });
|
|
235
237
|
if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
|
|
236
238
|
}
|
|
237
239
|
return scopes;
|
|
@@ -321,7 +323,7 @@ function detectScopeHints() {
|
|
|
321
323
|
return [];
|
|
322
324
|
}
|
|
323
325
|
function sleep(ms) {
|
|
324
|
-
return new Promise((
|
|
326
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
325
327
|
}
|
|
326
328
|
function isApiSuccessResponse(value) {
|
|
327
329
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1976,6 +1978,17 @@ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
|
|
|
1976
1978
|
style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
|
|
1977
1979
|
);
|
|
1978
1980
|
}
|
|
1981
|
+
async function writeFileAtomic(filePath, body) {
|
|
1982
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1983
|
+
const tmp = `${filePath}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
1984
|
+
try {
|
|
1985
|
+
await writeFile(tmp, body, SECRET_JSON_FILE_OPTIONS);
|
|
1986
|
+
await rename(tmp, filePath);
|
|
1987
|
+
} catch (e) {
|
|
1988
|
+
await rm(tmp, { force: true }).catch(() => void 0);
|
|
1989
|
+
throw e;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1979
1992
|
async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
|
|
1980
1993
|
let root = {};
|
|
1981
1994
|
try {
|
|
@@ -2000,21 +2013,25 @@ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entr
|
|
|
2000
2013
|
}
|
|
2001
2014
|
}
|
|
2002
2015
|
const bucketRaw = root[topLevelKey];
|
|
2003
|
-
const
|
|
2004
|
-
if (options.onExisting === "skip" &&
|
|
2016
|
+
const existingBucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? bucketRaw : {};
|
|
2017
|
+
if (options.onExisting === "skip" && existingBucket[shortName] !== void 0) {
|
|
2005
2018
|
return "unchanged";
|
|
2006
2019
|
}
|
|
2020
|
+
const staleKeys = new Set((options.removeKeys ?? []).filter((k) => k !== shortName));
|
|
2021
|
+
const bucket = Object.fromEntries(
|
|
2022
|
+
Object.entries(existingBucket).filter(([k]) => !staleKeys.has(k))
|
|
2023
|
+
);
|
|
2007
2024
|
bucket[shortName] = entry;
|
|
2008
2025
|
root[topLevelKey] = bucket;
|
|
2009
|
-
await
|
|
2010
|
-
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
2026
|
+
await writeFileAtomic(filePath, JSON.stringify(root, null, 2) + "\n");
|
|
2011
2027
|
writeMcpAddedLine(shortName, filePath);
|
|
2012
2028
|
return "ok";
|
|
2013
2029
|
}
|
|
2014
|
-
async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
|
|
2030
|
+
async function mergeMcpServersObjectStyle(filePath, shortName, entry, removeKeys) {
|
|
2015
2031
|
const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
|
|
2016
2032
|
stripJsonComments: false,
|
|
2017
|
-
onExisting: "overwrite"
|
|
2033
|
+
onExisting: "overwrite",
|
|
2034
|
+
removeKeys
|
|
2018
2035
|
});
|
|
2019
2036
|
return result === "parse-error" ? "parse-error" : "ok";
|
|
2020
2037
|
}
|
|
@@ -2751,8 +2768,8 @@ async function runInit(explicitBaseUrl, options) {
|
|
|
2751
2768
|
process.exit(1);
|
|
2752
2769
|
}
|
|
2753
2770
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2754
|
-
const ask = (question) => new Promise((
|
|
2755
|
-
rl.question(question,
|
|
2771
|
+
const ask = (question) => new Promise((resolve3) => {
|
|
2772
|
+
rl.question(question, resolve3);
|
|
2756
2773
|
});
|
|
2757
2774
|
process.stderr.write("\n" + BANNER + "\n");
|
|
2758
2775
|
process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
|
|
@@ -3782,11 +3799,311 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3782
3799
|
}
|
|
3783
3800
|
return lastConfig;
|
|
3784
3801
|
}
|
|
3802
|
+
var CODING_CLIENTS = [
|
|
3803
|
+
"cursor",
|
|
3804
|
+
"cline",
|
|
3805
|
+
"windsurf",
|
|
3806
|
+
"claude",
|
|
3807
|
+
"copilot",
|
|
3808
|
+
"goose",
|
|
3809
|
+
"gemini",
|
|
3810
|
+
"codex",
|
|
3811
|
+
"continue",
|
|
3812
|
+
"kilo",
|
|
3813
|
+
"opencode"
|
|
3814
|
+
];
|
|
3815
|
+
var CLIENT_DISPLAY_NAMES = {
|
|
3816
|
+
cursor: "Cursor",
|
|
3817
|
+
cline: "Cline",
|
|
3818
|
+
windsurf: "Windsurf",
|
|
3819
|
+
claude: "Claude Desktop",
|
|
3820
|
+
copilot: "GitHub Copilot",
|
|
3821
|
+
goose: "Goose",
|
|
3822
|
+
gemini: "Gemini CLI",
|
|
3823
|
+
codex: "Codex CLI",
|
|
3824
|
+
continue: "Continue",
|
|
3825
|
+
kilo: "Kilo Code",
|
|
3826
|
+
opencode: "OpenCode"
|
|
3827
|
+
};
|
|
3828
|
+
var CLIENT_RELOAD_INSTRUCTIONS = {
|
|
3829
|
+
cursor: "Restart Cursor or reload the window to pick up the new server.",
|
|
3830
|
+
cline: "Cline picks up config changes automatically.",
|
|
3831
|
+
windsurf: "Restart Windsurf to pick up the new server.",
|
|
3832
|
+
claude: "Restart Claude Desktop to pick up the new server.",
|
|
3833
|
+
copilot: "Reload the VS Code window (Cmd+Shift+P > Reload Window).",
|
|
3834
|
+
goose: "Restart Goose to pick up the new extension.",
|
|
3835
|
+
gemini: "Restart Gemini CLI to pick up the new server.",
|
|
3836
|
+
codex: "Restart Codex CLI to pick up the new server.",
|
|
3837
|
+
continue: "Continue picks up config changes automatically.",
|
|
3838
|
+
kilo: "Kilo Code picks up config changes automatically.",
|
|
3839
|
+
opencode: "Restart OpenCode to pick up the new server."
|
|
3840
|
+
};
|
|
3841
|
+
function clientDisplayName(client) {
|
|
3842
|
+
return CLIENT_DISPLAY_NAMES[client];
|
|
3843
|
+
}
|
|
3844
|
+
function clientReloadInstruction(client) {
|
|
3845
|
+
return CLIENT_RELOAD_INSTRUCTIONS[client];
|
|
3846
|
+
}
|
|
3847
|
+
function clientConfigPath(client, workspacePath) {
|
|
3848
|
+
switch (client) {
|
|
3849
|
+
case "cursor":
|
|
3850
|
+
return getCursorMcpJsonPath();
|
|
3851
|
+
case "cline":
|
|
3852
|
+
return getClineMcpSettingsPath();
|
|
3853
|
+
case "windsurf":
|
|
3854
|
+
return getWindsurfMcpConfigPath();
|
|
3855
|
+
case "claude":
|
|
3856
|
+
return getClaudeDesktopConfigPath();
|
|
3857
|
+
case "goose":
|
|
3858
|
+
return join(homedir(), ".config", "goose", "config.yaml");
|
|
3859
|
+
case "gemini":
|
|
3860
|
+
return join(homedir(), ".gemini", "settings.json");
|
|
3861
|
+
case "codex":
|
|
3862
|
+
return join(homedir(), ".codex", "config.toml");
|
|
3863
|
+
case "copilot":
|
|
3864
|
+
return join(workspacePath ?? ".", ".vscode", "mcp.json");
|
|
3865
|
+
case "continue":
|
|
3866
|
+
return join(workspacePath ?? ".", ".continue", "mcpServers");
|
|
3867
|
+
case "kilo":
|
|
3868
|
+
return join(workspacePath ?? ".", ".kilo", "kilo.jsonc");
|
|
3869
|
+
case "opencode":
|
|
3870
|
+
return join(workspacePath ?? ".", "opencode.json");
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
function detectInstalledClients() {
|
|
3874
|
+
const found = [];
|
|
3875
|
+
const homeClients = [
|
|
3876
|
+
"cursor",
|
|
3877
|
+
"cline",
|
|
3878
|
+
"windsurf",
|
|
3879
|
+
"claude",
|
|
3880
|
+
"goose",
|
|
3881
|
+
"gemini",
|
|
3882
|
+
"codex"
|
|
3883
|
+
];
|
|
3884
|
+
for (const client of homeClients) {
|
|
3885
|
+
const p = clientConfigPath(client);
|
|
3886
|
+
if (existsSync(p)) {
|
|
3887
|
+
found.push(client);
|
|
3888
|
+
} else {
|
|
3889
|
+
const dir = dirname(p);
|
|
3890
|
+
if (existsSync(dir)) {
|
|
3891
|
+
found.push(client);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
return found;
|
|
3896
|
+
}
|
|
3897
|
+
var CODING_CLIENT_TO_PLATFORM = {
|
|
3898
|
+
cursor: "cursor",
|
|
3899
|
+
cline: "cline",
|
|
3900
|
+
windsurf: "windsurf",
|
|
3901
|
+
claude: "claude-desktop",
|
|
3902
|
+
copilot: "github-copilot",
|
|
3903
|
+
goose: "goose",
|
|
3904
|
+
gemini: "gemini-cli",
|
|
3905
|
+
codex: "codex-cli",
|
|
3906
|
+
continue: "continue-dev",
|
|
3907
|
+
kilo: "kilo-code",
|
|
3908
|
+
opencode: "opencode"
|
|
3909
|
+
};
|
|
3910
|
+
async function writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, workspacePath) {
|
|
3911
|
+
const filePath = clientConfigPath(client, workspacePath);
|
|
3912
|
+
const entryKey = agentName;
|
|
3913
|
+
const legacyKeys = [`${agentName}-files`];
|
|
3914
|
+
if (apiKey.trim().length === 0) {
|
|
3915
|
+
process.stderr.write(
|
|
3916
|
+
style.yellow("\u26A0") + ` Refusing to write MCP config for "${agentName}": missing API key. Left the config file unchanged.
|
|
3917
|
+
`
|
|
3918
|
+
);
|
|
3919
|
+
return null;
|
|
3920
|
+
}
|
|
3921
|
+
const authHeader = `Bearer ${apiKey}`;
|
|
3922
|
+
const url = shouldEmbedKeyInHostedProxyUrl(CODING_CLIENT_TO_PLATFORM[client]) ? hostedProxyUrlWithKeyParam(localProxyUrl, apiKey) : localProxyUrl;
|
|
3923
|
+
switch (client) {
|
|
3924
|
+
case "cursor":
|
|
3925
|
+
return await mergeMcpServersObjectStyle(
|
|
3926
|
+
filePath,
|
|
3927
|
+
entryKey,
|
|
3928
|
+
{
|
|
3929
|
+
url,
|
|
3930
|
+
headers: { Authorization: authHeader }
|
|
3931
|
+
},
|
|
3932
|
+
legacyKeys
|
|
3933
|
+
) === "ok" ? filePath : null;
|
|
3934
|
+
case "cline":
|
|
3935
|
+
return await mergeMcpServersObjectStyle(
|
|
3936
|
+
filePath,
|
|
3937
|
+
entryKey,
|
|
3938
|
+
{
|
|
3939
|
+
url
|
|
3940
|
+
},
|
|
3941
|
+
legacyKeys
|
|
3942
|
+
) === "ok" ? filePath : null;
|
|
3943
|
+
case "windsurf":
|
|
3944
|
+
return await mergeMcpServersObjectStyle(
|
|
3945
|
+
filePath,
|
|
3946
|
+
entryKey,
|
|
3947
|
+
{
|
|
3948
|
+
serverUrl: url,
|
|
3949
|
+
headers: { Authorization: authHeader }
|
|
3950
|
+
},
|
|
3951
|
+
legacyKeys
|
|
3952
|
+
) === "ok" ? filePath : null;
|
|
3953
|
+
case "claude":
|
|
3954
|
+
return await mergeMcpServersObjectStyle(
|
|
3955
|
+
filePath,
|
|
3956
|
+
entryKey,
|
|
3957
|
+
{
|
|
3958
|
+
url,
|
|
3959
|
+
headers: { Authorization: authHeader }
|
|
3960
|
+
},
|
|
3961
|
+
legacyKeys
|
|
3962
|
+
) === "ok" ? filePath : null;
|
|
3963
|
+
case "gemini": {
|
|
3964
|
+
return await mergeMcpServersObjectStyle(
|
|
3965
|
+
filePath,
|
|
3966
|
+
entryKey,
|
|
3967
|
+
{
|
|
3968
|
+
httpUrl: url,
|
|
3969
|
+
headers: { Authorization: authHeader }
|
|
3970
|
+
},
|
|
3971
|
+
legacyKeys
|
|
3972
|
+
) === "ok" ? filePath : null;
|
|
3973
|
+
}
|
|
3974
|
+
case "copilot": {
|
|
3975
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
3976
|
+
filePath,
|
|
3977
|
+
"servers",
|
|
3978
|
+
entryKey,
|
|
3979
|
+
{ type: "http", url, headers: { Authorization: authHeader } },
|
|
3980
|
+
{ stripJsonComments: false, onExisting: "overwrite", removeKeys: legacyKeys }
|
|
3981
|
+
);
|
|
3982
|
+
return result === "ok" ? filePath : null;
|
|
3983
|
+
}
|
|
3984
|
+
case "goose": {
|
|
3985
|
+
const goosePath = filePath;
|
|
3986
|
+
let content = "";
|
|
3987
|
+
try {
|
|
3988
|
+
content = await readFile(goosePath, "utf8");
|
|
3989
|
+
} catch (e) {
|
|
3990
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
3991
|
+
content = "";
|
|
3992
|
+
} else {
|
|
3993
|
+
return null;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
let root;
|
|
3997
|
+
try {
|
|
3998
|
+
const data = content.trim().length === 0 ? {} : parse(content);
|
|
3999
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return null;
|
|
4000
|
+
root = data;
|
|
4001
|
+
} catch {
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
const extensionsRaw = root["extensions"];
|
|
4005
|
+
let extensions;
|
|
4006
|
+
if (isYamlPlainObject(extensionsRaw)) {
|
|
4007
|
+
extensions = { ...extensionsRaw };
|
|
4008
|
+
} else if (extensionsRaw === void 0) {
|
|
4009
|
+
extensions = {};
|
|
4010
|
+
} else {
|
|
4011
|
+
return null;
|
|
4012
|
+
}
|
|
4013
|
+
extensions[entryKey] = {
|
|
4014
|
+
enabled: true,
|
|
4015
|
+
type: "streamable_http",
|
|
4016
|
+
name: entryKey,
|
|
4017
|
+
description: "",
|
|
4018
|
+
uri: url,
|
|
4019
|
+
envs: {},
|
|
4020
|
+
env_keys: [],
|
|
4021
|
+
headers: { Authorization: authHeader },
|
|
4022
|
+
timeout: 300,
|
|
4023
|
+
socket: null,
|
|
4024
|
+
bundled: null,
|
|
4025
|
+
available_tools: []
|
|
4026
|
+
};
|
|
4027
|
+
root["extensions"] = extensions;
|
|
4028
|
+
const out = stringify(root, { indent: 2, lineWidth: 0 });
|
|
4029
|
+
const body = out.endsWith("\n") ? out : `${out}
|
|
4030
|
+
`;
|
|
4031
|
+
await mkdir(dirname(goosePath), { recursive: true });
|
|
4032
|
+
await writeFile(goosePath, body, SECRET_JSON_FILE_OPTIONS);
|
|
4033
|
+
return goosePath;
|
|
4034
|
+
}
|
|
4035
|
+
case "codex": {
|
|
4036
|
+
const codexPath = filePath;
|
|
4037
|
+
let existing = "";
|
|
4038
|
+
try {
|
|
4039
|
+
existing = await readFile(codexPath, "utf8");
|
|
4040
|
+
} catch (e) {
|
|
4041
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
4042
|
+
existing = "";
|
|
4043
|
+
} else {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
const sectionHeader = `[mcp_servers.${entryKey}]`;
|
|
4048
|
+
const sectionBlock = `${sectionHeader}
|
|
4049
|
+
type = "http"
|
|
4050
|
+
url = "${url}"
|
|
4051
|
+
|
|
4052
|
+
[mcp_servers.${entryKey}.http_headers]
|
|
4053
|
+
Authorization = "${authHeader}"
|
|
4054
|
+
`;
|
|
4055
|
+
if (existing.includes(sectionHeader)) {
|
|
4056
|
+
const idx = existing.indexOf(sectionHeader);
|
|
4057
|
+
const nextSection = existing.indexOf("\n[", idx + sectionHeader.length);
|
|
4058
|
+
const before = existing.slice(0, idx);
|
|
4059
|
+
const after = nextSection >= 0 ? existing.slice(nextSection + 1) : "";
|
|
4060
|
+
existing = before + sectionBlock + (after.length > 0 ? "\n" + after : "");
|
|
4061
|
+
} else {
|
|
4062
|
+
existing = existing.trimEnd() + "\n\n" + sectionBlock;
|
|
4063
|
+
}
|
|
4064
|
+
await mkdir(dirname(codexPath), { recursive: true });
|
|
4065
|
+
await writeFile(codexPath, existing, SECRET_JSON_FILE_OPTIONS);
|
|
4066
|
+
return codexPath;
|
|
4067
|
+
}
|
|
4068
|
+
case "continue": {
|
|
4069
|
+
const dir = join(workspacePath ?? ".", ".continue", "mcpServers");
|
|
4070
|
+
const continuePath = join(dir, `${entryKey}.yaml`);
|
|
4071
|
+
const yamlContent = stringify(
|
|
4072
|
+
{ name: entryKey, type: "streamableHttp", url },
|
|
4073
|
+
{ indent: 2, lineWidth: 0 }
|
|
4074
|
+
);
|
|
4075
|
+
await mkdir(dir, { recursive: true });
|
|
4076
|
+
await writeFile(continuePath, yamlContent, SECRET_JSON_FILE_OPTIONS);
|
|
4077
|
+
return continuePath;
|
|
4078
|
+
}
|
|
4079
|
+
case "kilo": {
|
|
4080
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
4081
|
+
filePath,
|
|
4082
|
+
"mcp",
|
|
4083
|
+
entryKey,
|
|
4084
|
+
{ url, headers: { Authorization: authHeader } },
|
|
4085
|
+
{ stripJsonComments: true, onExisting: "overwrite", removeKeys: legacyKeys }
|
|
4086
|
+
);
|
|
4087
|
+
return result === "ok" ? filePath : null;
|
|
4088
|
+
}
|
|
4089
|
+
case "opencode": {
|
|
4090
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
4091
|
+
filePath,
|
|
4092
|
+
"mcp",
|
|
4093
|
+
entryKey,
|
|
4094
|
+
{ type: "streamablehttp", url, headers: { Authorization: authHeader } },
|
|
4095
|
+
{ stripJsonComments: false, onExisting: "overwrite", removeKeys: legacyKeys }
|
|
4096
|
+
);
|
|
4097
|
+
return result === "ok" ? filePath : null;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
3785
4101
|
|
|
3786
4102
|
// src/types/index.ts
|
|
3787
4103
|
var PERMISSION_LEVELS = {
|
|
3788
4104
|
Read: "read",
|
|
3789
4105
|
Write: "write",
|
|
4106
|
+
Delete: "delete",
|
|
3790
4107
|
Execute: "execute",
|
|
3791
4108
|
Publish: "publish",
|
|
3792
4109
|
Create: "create"
|
|
@@ -3996,7 +4313,7 @@ function createActionLogger(config) {
|
|
|
3996
4313
|
};
|
|
3997
4314
|
}
|
|
3998
4315
|
function sleep2(ms) {
|
|
3999
|
-
return new Promise((
|
|
4316
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4000
4317
|
}
|
|
4001
4318
|
|
|
4002
4319
|
// src/spending/spending-checker.ts
|
|
@@ -4554,9 +4871,9 @@ function createProxyServer(config) {
|
|
|
4554
4871
|
timer.unref();
|
|
4555
4872
|
}
|
|
4556
4873
|
config.logger.info("Proxy ready.", { agent: config.agentName });
|
|
4557
|
-
return new Promise((
|
|
4874
|
+
return new Promise((resolve3) => {
|
|
4558
4875
|
childProcess.on("exit", () => {
|
|
4559
|
-
|
|
4876
|
+
resolve3();
|
|
4560
4877
|
});
|
|
4561
4878
|
});
|
|
4562
4879
|
}
|
|
@@ -4668,195 +4985,1253 @@ async function restoreClaudeDesktopMcpFromBackup() {
|
|
|
4668
4985
|
|
|
4669
4986
|
// package.json
|
|
4670
4987
|
var package_default = {
|
|
4671
|
-
version: "1.
|
|
4988
|
+
version: "1.11.0"};
|
|
4672
4989
|
|
|
4673
4990
|
// src/package-meta.ts
|
|
4674
4991
|
var PACKAGE_VERSION = package_default.version;
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
subcommand = "init";
|
|
4693
|
-
} else if (arg === "agents") {
|
|
4694
|
-
subcommand = "agents";
|
|
4695
|
-
} else if (arg === "delete-agent") {
|
|
4696
|
-
subcommand = "delete-agent";
|
|
4697
|
-
const name = args[i + 1];
|
|
4698
|
-
if (name === void 0 || name.startsWith("-")) {
|
|
4699
|
-
process.stderr.write("Error: delete-agent requires an agent name.\n");
|
|
4700
|
-
process.stderr.write("Example: npx multicorn-shield delete-agent my-agent\n");
|
|
4701
|
-
process.exit(1);
|
|
4702
|
-
}
|
|
4703
|
-
deleteAgentName = name;
|
|
4704
|
-
i++;
|
|
4705
|
-
} else if (arg === "--wrap") {
|
|
4706
|
-
subcommand = "wrap";
|
|
4707
|
-
const tail = args.slice(i + 1);
|
|
4708
|
-
const remaining = [];
|
|
4709
|
-
for (let j = 0; j < tail.length; j++) {
|
|
4710
|
-
const token = tail[j];
|
|
4711
|
-
if (token === void 0) continue;
|
|
4712
|
-
if (remaining.length > 0) {
|
|
4713
|
-
remaining.push(token);
|
|
4714
|
-
continue;
|
|
4715
|
-
}
|
|
4716
|
-
if (token === "--agent-name") {
|
|
4717
|
-
const value = tail[j + 1];
|
|
4718
|
-
if (value !== void 0) {
|
|
4719
|
-
agentName = value;
|
|
4720
|
-
j++;
|
|
4721
|
-
}
|
|
4722
|
-
} else if (token === "--log-level") {
|
|
4723
|
-
const value = tail[j + 1];
|
|
4724
|
-
if (value !== void 0 && isValidLogLevel(value)) {
|
|
4725
|
-
logLevel = value;
|
|
4726
|
-
j++;
|
|
4727
|
-
}
|
|
4728
|
-
} else if (token === "--base-url") {
|
|
4729
|
-
const value = tail[j + 1];
|
|
4730
|
-
if (value !== void 0) {
|
|
4731
|
-
baseUrl = value;
|
|
4732
|
-
j++;
|
|
4733
|
-
}
|
|
4734
|
-
} else if (token === "--dashboard-url") {
|
|
4735
|
-
const value = tail[j + 1];
|
|
4736
|
-
if (value !== void 0) {
|
|
4737
|
-
dashboardUrl = value;
|
|
4738
|
-
j++;
|
|
4739
|
-
}
|
|
4740
|
-
} else if (token === "--api-key") {
|
|
4741
|
-
const value = tail[j + 1];
|
|
4742
|
-
if (value !== void 0) {
|
|
4743
|
-
apiKey = value;
|
|
4744
|
-
j++;
|
|
4745
|
-
}
|
|
4746
|
-
} else {
|
|
4747
|
-
remaining.push(token);
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
if (remaining.length === 0) {
|
|
4751
|
-
process.stderr.write("Error: --wrap requires a command to run.\n");
|
|
4752
|
-
process.stderr.write("Example: npx multicorn-shield --wrap my-mcp-server\n");
|
|
4753
|
-
process.exit(1);
|
|
4754
|
-
}
|
|
4755
|
-
wrapCommand = remaining[0] ?? "";
|
|
4756
|
-
wrapArgs = remaining.slice(1);
|
|
4757
|
-
break;
|
|
4758
|
-
} else if (arg === "--log-level") {
|
|
4759
|
-
const next = args[i + 1];
|
|
4760
|
-
if (next !== void 0 && isValidLogLevel(next)) {
|
|
4761
|
-
logLevel = next;
|
|
4762
|
-
i++;
|
|
4763
|
-
}
|
|
4764
|
-
} else if (arg === "--base-url") {
|
|
4765
|
-
const next = args[i + 1];
|
|
4766
|
-
if (next !== void 0) {
|
|
4767
|
-
baseUrl = next;
|
|
4768
|
-
i++;
|
|
4769
|
-
}
|
|
4770
|
-
} else if (arg === "--dashboard-url") {
|
|
4771
|
-
const next = args[i + 1];
|
|
4772
|
-
if (next !== void 0) {
|
|
4773
|
-
dashboardUrl = next;
|
|
4774
|
-
i++;
|
|
4775
|
-
}
|
|
4776
|
-
} else if (arg === "--agent-name") {
|
|
4777
|
-
const next = args[i + 1];
|
|
4778
|
-
if (next !== void 0) {
|
|
4779
|
-
agentName = next;
|
|
4780
|
-
i++;
|
|
4781
|
-
}
|
|
4782
|
-
} else if (arg === "--api-key") {
|
|
4783
|
-
const next = args[i + 1];
|
|
4784
|
-
if (next !== void 0) {
|
|
4785
|
-
apiKey = next;
|
|
4786
|
-
i++;
|
|
4787
|
-
}
|
|
4788
|
-
} else if (arg === "--verbose" || arg === "--debug") {
|
|
4789
|
-
verbose = true;
|
|
4790
|
-
}
|
|
4791
|
-
}
|
|
4792
|
-
return {
|
|
4793
|
-
subcommand,
|
|
4794
|
-
wrapCommand,
|
|
4795
|
-
wrapArgs,
|
|
4796
|
-
logLevel,
|
|
4797
|
-
baseUrl,
|
|
4798
|
-
dashboardUrl,
|
|
4799
|
-
agentName,
|
|
4800
|
-
deleteAgentName,
|
|
4801
|
-
apiKey,
|
|
4802
|
-
verbose
|
|
4803
|
-
};
|
|
4992
|
+
var DEFAULT_FS_PORT = 3005;
|
|
4993
|
+
var DEFAULT_PROXY_PORT = 3001;
|
|
4994
|
+
var PIDFILE_DIR = process.env["MULTICORN_HOME"] ?? join(homedir(), ".multicorn");
|
|
4995
|
+
var PROXY_REGISTRY = join(PIDFILE_DIR, "proxy.json");
|
|
4996
|
+
var FS_REGISTRY = join(PIDFILE_DIR, "fs-servers.json");
|
|
4997
|
+
var LOCK_PATH = join(PIDFILE_DIR, ".resources.lock");
|
|
4998
|
+
var LOCK_WAIT_MS = 6e4;
|
|
4999
|
+
var LOCK_STALE_MS = 12e4;
|
|
5000
|
+
var FS_PORT_SCAN_RANGE = 200;
|
|
5001
|
+
var style2 = {
|
|
5002
|
+
green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
|
|
5003
|
+
cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
|
|
5004
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
5005
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
5006
|
+
};
|
|
5007
|
+
function pidfilePath(agent) {
|
|
5008
|
+
return join(PIDFILE_DIR, `files-${agent}.pid`);
|
|
4804
5009
|
}
|
|
4805
|
-
function
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
" npx multicorn-shield init",
|
|
4812
|
-
" Interactive setup. Saves API key to ~/.multicorn/config.json.",
|
|
4813
|
-
"",
|
|
4814
|
-
" npx multicorn-shield restore",
|
|
4815
|
-
" Restore MCP servers in claude_desktop_config.json from the Shield extension backup.",
|
|
4816
|
-
"",
|
|
4817
|
-
" npx multicorn-shield agents",
|
|
4818
|
-
" List configured agents and show which is the default.",
|
|
4819
|
-
"",
|
|
4820
|
-
" npx multicorn-shield delete-agent <name>",
|
|
4821
|
-
" Remove a saved agent.",
|
|
4822
|
-
"",
|
|
4823
|
-
" npx multicorn-shield --wrap <command> [args...]",
|
|
4824
|
-
" Start <command> as an MCP server and proxy all tool calls through",
|
|
4825
|
-
" Shield's permission layer.",
|
|
4826
|
-
"",
|
|
4827
|
-
"Options:",
|
|
4828
|
-
" --version, -v Print version and exit",
|
|
4829
|
-
" --verbose, --debug Print extra diagnostics during init (menu selection, agent counts)",
|
|
4830
|
-
" --api-key <key> Multicorn API key (overrides MULTICORN_API_KEY env var and config file)",
|
|
4831
|
-
" --log-level <level> Log level: debug | info | warn | error (default: info)",
|
|
4832
|
-
" --base-url <url> Multicorn API base URL (default: https://api.multicorn.ai)",
|
|
4833
|
-
" --dashboard-url <url> Dashboard URL for consent page (default: derived from --base-url)",
|
|
4834
|
-
" --agent-name <name> Override agent name derived from the wrapped command",
|
|
4835
|
-
"",
|
|
4836
|
-
"Examples:",
|
|
4837
|
-
" npx multicorn-shield init",
|
|
4838
|
-
" npx multicorn-shield --wrap npx @modelcontextprotocol/server-filesystem /tmp",
|
|
4839
|
-
" npx multicorn-shield --wrap my-mcp-server --log-level debug",
|
|
4840
|
-
""
|
|
4841
|
-
].join("\n")
|
|
4842
|
-
);
|
|
5010
|
+
function writePidfile(data) {
|
|
5011
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5012
|
+
writeFileSync(pidfilePath(data.agent), JSON.stringify(data), {
|
|
5013
|
+
encoding: "utf8",
|
|
5014
|
+
mode: 384
|
|
5015
|
+
});
|
|
4843
5016
|
}
|
|
4844
|
-
|
|
4845
|
-
const
|
|
4846
|
-
if (
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
return;
|
|
5017
|
+
function readPidfile(agent) {
|
|
5018
|
+
const p = pidfilePath(agent);
|
|
5019
|
+
if (!existsSync(p)) return null;
|
|
5020
|
+
try {
|
|
5021
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
5022
|
+
} catch {
|
|
5023
|
+
return null;
|
|
4852
5024
|
}
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
5025
|
+
}
|
|
5026
|
+
function removePidfile(agent) {
|
|
5027
|
+
const p = pidfilePath(agent);
|
|
5028
|
+
try {
|
|
5029
|
+
unlinkSync(p);
|
|
5030
|
+
} catch {
|
|
4857
5031
|
}
|
|
4858
|
-
|
|
4859
|
-
|
|
5032
|
+
}
|
|
5033
|
+
function isProcessAlive(pid) {
|
|
5034
|
+
try {
|
|
5035
|
+
process.kill(pid, 0);
|
|
5036
|
+
return true;
|
|
5037
|
+
} catch {
|
|
5038
|
+
return false;
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
function listAllPidfiles() {
|
|
5042
|
+
if (!existsSync(PIDFILE_DIR)) return [];
|
|
5043
|
+
const files = readdirSync(PIDFILE_DIR).filter(
|
|
5044
|
+
(f) => f.startsWith("files-") && f.endsWith(".pid")
|
|
5045
|
+
);
|
|
5046
|
+
const results = [];
|
|
5047
|
+
for (const f of files) {
|
|
5048
|
+
try {
|
|
5049
|
+
const data = JSON.parse(readFileSync(join(PIDFILE_DIR, f), "utf8"));
|
|
5050
|
+
results.push(data);
|
|
5051
|
+
} catch {
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
return results;
|
|
5055
|
+
}
|
|
5056
|
+
function runStatus() {
|
|
5057
|
+
const sessions = listAllPidfiles();
|
|
5058
|
+
if (sessions.length === 0) {
|
|
5059
|
+
process.stderr.write("No active file-sharing sessions.\n");
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
const proxyReg = readProxyRegistry();
|
|
5063
|
+
const fsReg = readFsRegistry();
|
|
5064
|
+
process.stderr.write("Active sessions:\n\n");
|
|
5065
|
+
for (const s of sessions) {
|
|
5066
|
+
const supervisorAlive = typeof s.supervisorPid === "number" && isProcessAlive(s.supervisorPid);
|
|
5067
|
+
const fsEntry = fsReg[s.dir];
|
|
5068
|
+
const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
|
|
5069
|
+
const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
|
|
5070
|
+
const agentStatus = supervisorAlive ? style2.green("running") : style2.dim("stopped");
|
|
5071
|
+
process.stderr.write(` ${style2.bold(s.agent)} ${agentStatus}
|
|
5072
|
+
`);
|
|
5073
|
+
process.stderr.write(` Folder: ${s.dir || "(unknown)"}
|
|
5074
|
+
`);
|
|
5075
|
+
process.stderr.write(
|
|
5076
|
+
` FS server: :${String(s.fsPort)} ${fsAlive ? style2.green("running") : style2.dim("stopped")}
|
|
5077
|
+
`
|
|
5078
|
+
);
|
|
5079
|
+
process.stderr.write(
|
|
5080
|
+
` Proxy: :${String(s.proxyPort)} ${proxyAlive ? style2.green("running") : style2.dim("stopped")}
|
|
5081
|
+
`
|
|
5082
|
+
);
|
|
5083
|
+
process.stderr.write("\n");
|
|
5084
|
+
if (!supervisorAlive) {
|
|
5085
|
+
removePidfile(s.agent);
|
|
5086
|
+
process.stderr.write(style2.dim(` (cleaned up stale pidfile)
|
|
5087
|
+
|
|
5088
|
+
`));
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
async function isPortListening(port) {
|
|
5093
|
+
return new Promise((resolve3) => {
|
|
5094
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
5095
|
+
sock.once("connect", () => {
|
|
5096
|
+
sock.destroy();
|
|
5097
|
+
resolve3(true);
|
|
5098
|
+
});
|
|
5099
|
+
sock.once("error", () => {
|
|
5100
|
+
sock.destroy();
|
|
5101
|
+
resolve3(false);
|
|
5102
|
+
});
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
async function probeProxyHealth(port) {
|
|
5106
|
+
try {
|
|
5107
|
+
const resp = await fetch(`http://127.0.0.1:${String(port)}/health`, {
|
|
5108
|
+
signal: AbortSignal.timeout(2e3)
|
|
5109
|
+
});
|
|
5110
|
+
return resp.ok;
|
|
5111
|
+
} catch {
|
|
5112
|
+
return false;
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
async function readProxyVersion(port) {
|
|
5116
|
+
try {
|
|
5117
|
+
const resp = await fetch(`http://127.0.0.1:${String(port)}/health`, {
|
|
5118
|
+
signal: AbortSignal.timeout(2e3)
|
|
5119
|
+
});
|
|
5120
|
+
if (!resp.ok) return null;
|
|
5121
|
+
const body = await resp.json();
|
|
5122
|
+
return typeof body.version === "string" && body.version.length > 0 ? body.version : null;
|
|
5123
|
+
} catch {
|
|
5124
|
+
return null;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function sleep3(ms) {
|
|
5128
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5129
|
+
}
|
|
5130
|
+
function readJsonFile(path) {
|
|
5131
|
+
if (!existsSync(path)) return null;
|
|
5132
|
+
try {
|
|
5133
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
5134
|
+
} catch {
|
|
5135
|
+
return null;
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
function writeJsonFile(path, data) {
|
|
5139
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5140
|
+
writeFileSync(path, JSON.stringify(data), { encoding: "utf8", mode: 384 });
|
|
5141
|
+
}
|
|
5142
|
+
function readProxyRegistry() {
|
|
5143
|
+
return readJsonFile(PROXY_REGISTRY);
|
|
5144
|
+
}
|
|
5145
|
+
function readFsRegistry() {
|
|
5146
|
+
return readJsonFile(FS_REGISTRY) ?? {};
|
|
5147
|
+
}
|
|
5148
|
+
function canonicalFolder(dir) {
|
|
5149
|
+
const abs = resolve(dir);
|
|
5150
|
+
try {
|
|
5151
|
+
return realpathSync(abs);
|
|
5152
|
+
} catch {
|
|
5153
|
+
return abs;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
function readLock() {
|
|
5157
|
+
return readJsonFile(LOCK_PATH);
|
|
5158
|
+
}
|
|
5159
|
+
function isLockStale(holder, now = Date.now()) {
|
|
5160
|
+
if (holder === null) return true;
|
|
5161
|
+
if (!isProcessAlive(holder.pid)) return true;
|
|
5162
|
+
return now - holder.ts > LOCK_STALE_MS;
|
|
5163
|
+
}
|
|
5164
|
+
async function acquireResourceLock() {
|
|
5165
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5166
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
5167
|
+
for (; ; ) {
|
|
5168
|
+
try {
|
|
5169
|
+
writeFileSync(LOCK_PATH, JSON.stringify({ pid: process.pid, ts: Date.now() }), {
|
|
5170
|
+
encoding: "utf8",
|
|
5171
|
+
mode: 384,
|
|
5172
|
+
flag: "wx"
|
|
5173
|
+
});
|
|
5174
|
+
return;
|
|
5175
|
+
} catch {
|
|
5176
|
+
const holder = readLock();
|
|
5177
|
+
if (isLockStale(holder)) {
|
|
5178
|
+
try {
|
|
5179
|
+
unlinkSync(LOCK_PATH);
|
|
5180
|
+
} catch {
|
|
5181
|
+
}
|
|
5182
|
+
continue;
|
|
5183
|
+
}
|
|
5184
|
+
if (Date.now() > deadline) {
|
|
5185
|
+
try {
|
|
5186
|
+
unlinkSync(LOCK_PATH);
|
|
5187
|
+
} catch {
|
|
5188
|
+
}
|
|
5189
|
+
continue;
|
|
5190
|
+
}
|
|
5191
|
+
await sleep3(100);
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
function releaseResourceLock() {
|
|
5196
|
+
const holder = readLock();
|
|
5197
|
+
if (holder !== null && holder.pid === process.pid) {
|
|
5198
|
+
try {
|
|
5199
|
+
unlinkSync(LOCK_PATH);
|
|
5200
|
+
} catch {
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
async function withResourceLock(fn) {
|
|
5205
|
+
await acquireResourceLock();
|
|
5206
|
+
try {
|
|
5207
|
+
return await fn();
|
|
5208
|
+
} finally {
|
|
5209
|
+
releaseResourceLock();
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
function liveAgents() {
|
|
5213
|
+
return listAllPidfiles().filter(
|
|
5214
|
+
(p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
|
|
5215
|
+
);
|
|
5216
|
+
}
|
|
5217
|
+
function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
|
|
5218
|
+
return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
|
|
5219
|
+
}
|
|
5220
|
+
function agentsReferencingFolder(folder, agents, excludeAgent) {
|
|
5221
|
+
return agents.filter((a) => a.agent !== excludeAgent && a.dir === folder);
|
|
5222
|
+
}
|
|
5223
|
+
async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE) {
|
|
5224
|
+
for (let port = start; port < start + range; port++) {
|
|
5225
|
+
if (claimed.has(port)) continue;
|
|
5226
|
+
if (await isBusy(port)) continue;
|
|
5227
|
+
return port;
|
|
5228
|
+
}
|
|
5229
|
+
throw new Error(
|
|
5230
|
+
`No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
|
|
5231
|
+
);
|
|
5232
|
+
}
|
|
5233
|
+
async function resolveConfig(opts) {
|
|
5234
|
+
const fromFlag = opts.apiKey;
|
|
5235
|
+
if (fromFlag && fromFlag.length > 0) {
|
|
5236
|
+
return {
|
|
5237
|
+
apiKey: fromFlag,
|
|
5238
|
+
baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
const envKey = process.env["MULTICORN_API_KEY"];
|
|
5242
|
+
if (typeof envKey === "string" && envKey.length > 0) {
|
|
5243
|
+
return {
|
|
5244
|
+
apiKey: envKey,
|
|
5245
|
+
baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
const config = await loadConfig();
|
|
5249
|
+
if (config !== null) {
|
|
5250
|
+
return {
|
|
5251
|
+
apiKey: config.apiKey,
|
|
5252
|
+
baseUrl: opts.baseUrl ?? config.baseUrl
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
process.stderr.write(
|
|
5256
|
+
"No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
|
|
5257
|
+
);
|
|
5258
|
+
process.exit(1);
|
|
5259
|
+
}
|
|
5260
|
+
function startFsServerDetached(realDir, port) {
|
|
5261
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5262
|
+
const logFile = join(PIDFILE_DIR, `fs-${String(port)}.log`);
|
|
5263
|
+
const out = openSync(logFile, "a");
|
|
5264
|
+
const err = openSync(logFile, "a");
|
|
5265
|
+
const child = spawn(
|
|
5266
|
+
"npx",
|
|
5267
|
+
[
|
|
5268
|
+
"supergateway",
|
|
5269
|
+
// Serve the child over streamable HTTP at /mcp. Without this, supergateway
|
|
5270
|
+
// defaults to SSE (/sse + /message), but the proxy registers the target as
|
|
5271
|
+
// /mcp - the mismatch makes every request 404 with "Cannot POST /mcp".
|
|
5272
|
+
"--outputTransport",
|
|
5273
|
+
"streamableHttp",
|
|
5274
|
+
"--stdio",
|
|
5275
|
+
`npx @modelcontextprotocol/server-filesystem ${realDir}`,
|
|
5276
|
+
"--port",
|
|
5277
|
+
String(port)
|
|
5278
|
+
],
|
|
5279
|
+
{
|
|
5280
|
+
stdio: ["ignore", out, err],
|
|
5281
|
+
detached: true,
|
|
5282
|
+
env: { ...process.env }
|
|
5283
|
+
}
|
|
5284
|
+
);
|
|
5285
|
+
child.unref();
|
|
5286
|
+
return child;
|
|
5287
|
+
}
|
|
5288
|
+
function startLocalProxyDetached(port, apiBaseUrl) {
|
|
5289
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5290
|
+
const logFile = join(PIDFILE_DIR, "proxy.log");
|
|
5291
|
+
const out = openSync(logFile, "a");
|
|
5292
|
+
const err = openSync(logFile, "a");
|
|
5293
|
+
const child = spawn("npx", ["multicorn-proxy"], {
|
|
5294
|
+
stdio: ["ignore", out, err],
|
|
5295
|
+
detached: true,
|
|
5296
|
+
env: {
|
|
5297
|
+
...process.env,
|
|
5298
|
+
PORT: String(port),
|
|
5299
|
+
HOST: "127.0.0.1",
|
|
5300
|
+
SHIELD_API_BASE_URL: apiBaseUrl,
|
|
5301
|
+
// SAFETY: blanket-allow for private targets is acceptable ONLY because
|
|
5302
|
+
// this is a local single-user proxy. The only registered target is the
|
|
5303
|
+
// filesystem server on the same machine. Do NOT copy this pattern to a
|
|
5304
|
+
// multi-tenant or hosted proxy deployment.
|
|
5305
|
+
ALLOW_PRIVATE_TARGETS: "true"
|
|
5306
|
+
}
|
|
5307
|
+
});
|
|
5308
|
+
child.unref();
|
|
5309
|
+
return child;
|
|
5310
|
+
}
|
|
5311
|
+
async function ensureProxy(proxyPort, apiBaseUrl) {
|
|
5312
|
+
if (await probeProxyHealth(proxyPort)) {
|
|
5313
|
+
const reg2 = readProxyRegistry();
|
|
5314
|
+
const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
|
|
5315
|
+
return { reused: true, managed };
|
|
5316
|
+
}
|
|
5317
|
+
const reg = readProxyRegistry();
|
|
5318
|
+
if (reg !== null && !isProcessAlive(reg.pid)) {
|
|
5319
|
+
try {
|
|
5320
|
+
unlinkSync(PROXY_REGISTRY);
|
|
5321
|
+
} catch {
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
if (await isPortListening(proxyPort)) {
|
|
5325
|
+
throw new Error(
|
|
5326
|
+
`A server is already running on port ${String(proxyPort)} but it's not a healthy Shield proxy. Stop it or re-run with --proxy-port <n>.`
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
const child = startLocalProxyDetached(proxyPort, apiBaseUrl);
|
|
5330
|
+
for (let i = 0; i < 30; i++) {
|
|
5331
|
+
await sleep3(500);
|
|
5332
|
+
if (await probeProxyHealth(proxyPort)) {
|
|
5333
|
+
if (child.pid !== void 0) {
|
|
5334
|
+
writeJsonFile(PROXY_REGISTRY, { pid: child.pid, port: proxyPort });
|
|
5335
|
+
}
|
|
5336
|
+
return { reused: false, managed: true };
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
try {
|
|
5340
|
+
if (child.pid !== void 0) killWithEscalation(child.pid, true);
|
|
5341
|
+
} catch {
|
|
5342
|
+
}
|
|
5343
|
+
throw new Error(
|
|
5344
|
+
`Could not start the local proxy on port ${String(proxyPort)}. Check the log at ${join(PIDFILE_DIR, "proxy.log")}.`
|
|
5345
|
+
);
|
|
5346
|
+
}
|
|
5347
|
+
async function ensureFsServer(realDir, requestedPort) {
|
|
5348
|
+
const reg = readFsRegistry();
|
|
5349
|
+
const existing = reg[realDir];
|
|
5350
|
+
if (existing !== void 0 && isProcessAlive(existing.pid) && await isPortListening(existing.port)) {
|
|
5351
|
+
return { port: existing.port, reused: true };
|
|
5352
|
+
}
|
|
5353
|
+
if (existing !== void 0) {
|
|
5354
|
+
const rest = Object.fromEntries(Object.entries(reg).filter(([k]) => k !== realDir));
|
|
5355
|
+
writeJsonFile(FS_REGISTRY, rest);
|
|
5356
|
+
}
|
|
5357
|
+
let port;
|
|
5358
|
+
if (requestedPort !== void 0) {
|
|
5359
|
+
if (await isPortListening(requestedPort)) {
|
|
5360
|
+
throw new Error(
|
|
5361
|
+
`A server is already running on port ${String(requestedPort)}. Re-run without --port to auto-pick a free port, or choose another.`
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
port = requestedPort;
|
|
5365
|
+
} else {
|
|
5366
|
+
const claimed = new Set(Object.values(readFsRegistry()).map((e) => e.port));
|
|
5367
|
+
port = await nextFreePort(DEFAULT_FS_PORT, claimed, isPortListening);
|
|
5368
|
+
}
|
|
5369
|
+
const child = startFsServerDetached(realDir, port);
|
|
5370
|
+
let ready = false;
|
|
5371
|
+
for (let i = 0; i < 20; i++) {
|
|
5372
|
+
await sleep3(500);
|
|
5373
|
+
if (await isPortListening(port)) {
|
|
5374
|
+
ready = true;
|
|
5375
|
+
break;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
if (!ready) {
|
|
5379
|
+
try {
|
|
5380
|
+
if (child.pid !== void 0) killWithEscalation(child.pid, true);
|
|
5381
|
+
} catch {
|
|
5382
|
+
}
|
|
5383
|
+
throw new Error(
|
|
5384
|
+
`Filesystem server failed to start on port ${String(port)}. Check the directory path and the log at ${join(PIDFILE_DIR, `fs-${String(port)}.log`)}.`
|
|
5385
|
+
);
|
|
5386
|
+
}
|
|
5387
|
+
if (child.pid !== void 0) {
|
|
5388
|
+
const next = readFsRegistry();
|
|
5389
|
+
next[realDir] = { pid: child.pid, port };
|
|
5390
|
+
writeJsonFile(FS_REGISTRY, next);
|
|
5391
|
+
}
|
|
5392
|
+
return { port, reused: false };
|
|
5393
|
+
}
|
|
5394
|
+
function releaseProxyIfUnused(proxyPort, stoppingAgent) {
|
|
5395
|
+
const remaining = agentsReferencingProxy(proxyPort, liveAgents(), stoppingAgent);
|
|
5396
|
+
if (remaining.length > 0) return;
|
|
5397
|
+
const reg = readProxyRegistry();
|
|
5398
|
+
if (reg !== null && reg.port === proxyPort && isProcessAlive(reg.pid)) {
|
|
5399
|
+
killWithEscalation(reg.pid, true);
|
|
5400
|
+
}
|
|
5401
|
+
if (reg !== null && reg.port === proxyPort) {
|
|
5402
|
+
try {
|
|
5403
|
+
unlinkSync(PROXY_REGISTRY);
|
|
5404
|
+
} catch {
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
function releaseFsServerIfUnused(folder, stoppingAgent) {
|
|
5409
|
+
const remaining = agentsReferencingFolder(folder, liveAgents(), stoppingAgent);
|
|
5410
|
+
if (remaining.length > 0) return;
|
|
5411
|
+
const reg = readFsRegistry();
|
|
5412
|
+
const entry = reg[folder];
|
|
5413
|
+
if (entry === void 0) return;
|
|
5414
|
+
if (isProcessAlive(entry.pid)) {
|
|
5415
|
+
killWithEscalation(entry.pid, true);
|
|
5416
|
+
}
|
|
5417
|
+
const rest = Object.fromEntries(Object.entries(reg).filter(([k]) => k !== folder));
|
|
5418
|
+
writeJsonFile(FS_REGISTRY, rest);
|
|
5419
|
+
}
|
|
5420
|
+
function proxyServerSlug(agentLabel) {
|
|
5421
|
+
const t = agentLabel.trim().toLowerCase();
|
|
5422
|
+
const slug = t.replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
|
|
5423
|
+
if (slug === "") return "shield-handoff-agent";
|
|
5424
|
+
if (!/^[a-z0-9]/i.test(slug)) return `a-${slug}`;
|
|
5425
|
+
return slug.slice(0, 180);
|
|
5426
|
+
}
|
|
5427
|
+
async function registerAgentAndConfig(apiKey, baseUrl, agentName, fsPort, localDir) {
|
|
5428
|
+
const targetUrl = `http://127.0.0.1:${String(fsPort)}/mcp`;
|
|
5429
|
+
const serverName = proxyServerSlug(agentName);
|
|
5430
|
+
const response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
|
|
5431
|
+
method: "POST",
|
|
5432
|
+
headers: {
|
|
5433
|
+
"Content-Type": "application/json",
|
|
5434
|
+
"X-Multicorn-Key": apiKey
|
|
5435
|
+
},
|
|
5436
|
+
body: JSON.stringify({
|
|
5437
|
+
server_name: serverName,
|
|
5438
|
+
target_url: targetUrl,
|
|
5439
|
+
platform: "other-mcp",
|
|
5440
|
+
agent_name: agentName,
|
|
5441
|
+
local_dir: localDir
|
|
5442
|
+
}),
|
|
5443
|
+
signal: AbortSignal.timeout(1e4)
|
|
5444
|
+
});
|
|
5445
|
+
if (!response.ok) {
|
|
5446
|
+
let detail = `HTTP ${String(response.status)}`;
|
|
5447
|
+
try {
|
|
5448
|
+
const body = await response.json();
|
|
5449
|
+
const errObj = body["error"];
|
|
5450
|
+
if (typeof errObj?.["message"] === "string") detail = errObj["message"];
|
|
5451
|
+
else if (typeof body["message"] === "string") detail = body["message"];
|
|
5452
|
+
} catch {
|
|
5453
|
+
}
|
|
5454
|
+
throw new Error(`Failed to register agent: ${detail}`);
|
|
5455
|
+
}
|
|
5456
|
+
const envelope = await response.json();
|
|
5457
|
+
const data = envelope["data"];
|
|
5458
|
+
const proxyUrl = typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
5459
|
+
if (proxyUrl.length === 0) {
|
|
5460
|
+
throw new Error("Registration succeeded but no proxy URL was returned.");
|
|
5461
|
+
}
|
|
5462
|
+
let pathSegment = "";
|
|
5463
|
+
try {
|
|
5464
|
+
const parsed = new URL(proxyUrl);
|
|
5465
|
+
pathSegment = parsed.pathname + parsed.search;
|
|
5466
|
+
} catch {
|
|
5467
|
+
pathSegment = proxyUrl;
|
|
5468
|
+
}
|
|
5469
|
+
await grantScope(apiKey, baseUrl, agentName, "filesystem", "read");
|
|
5470
|
+
return { proxyUrl, pathSegment };
|
|
5471
|
+
}
|
|
5472
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
5473
|
+
async function sendHeartbeat(apiKey, baseUrl, serverName, proxyVersion) {
|
|
5474
|
+
try {
|
|
5475
|
+
await fetch(`${baseUrl}/api/v1/proxy/heartbeat`, {
|
|
5476
|
+
method: "POST",
|
|
5477
|
+
headers: {
|
|
5478
|
+
"Content-Type": "application/json",
|
|
5479
|
+
"X-Multicorn-Key": apiKey
|
|
5480
|
+
},
|
|
5481
|
+
// proxy_version lets the backend stamp the agent's last-seen version + timestamp
|
|
5482
|
+
// from the heartbeat, so the dashboard can flag an out-of-date proxy that needs a
|
|
5483
|
+
// restart even if the editor never connects through it.
|
|
5484
|
+
body: JSON.stringify(
|
|
5485
|
+
proxyVersion !== null ? { server_name: serverName, proxy_version: proxyVersion } : { server_name: serverName }
|
|
5486
|
+
),
|
|
5487
|
+
signal: AbortSignal.timeout(8e3)
|
|
5488
|
+
});
|
|
5489
|
+
} catch {
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
async function grantScope(apiKey, baseUrl, agentName, service, level) {
|
|
5493
|
+
const agentsResp = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
5494
|
+
headers: { "X-Multicorn-Key": apiKey },
|
|
5495
|
+
signal: AbortSignal.timeout(8e3)
|
|
5496
|
+
});
|
|
5497
|
+
if (!agentsResp.ok) return;
|
|
5498
|
+
const agentsBody = await agentsResp.json();
|
|
5499
|
+
const agentsList = agentsBody["data"];
|
|
5500
|
+
if (!Array.isArray(agentsList)) return;
|
|
5501
|
+
const agent = agentsList.find((a) => a["name"] === agentName);
|
|
5502
|
+
if (!agent || typeof agent["id"] !== "string") return;
|
|
5503
|
+
const agentId = agent["id"];
|
|
5504
|
+
await fetch(`${baseUrl}/api/v1/agents/${agentId}/scopes`, {
|
|
5505
|
+
method: "POST",
|
|
5506
|
+
headers: {
|
|
5507
|
+
"Content-Type": "application/json",
|
|
5508
|
+
"X-Multicorn-Key": apiKey
|
|
5509
|
+
},
|
|
5510
|
+
body: JSON.stringify({ service, permission_level: level }),
|
|
5511
|
+
signal: AbortSignal.timeout(8e3)
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
function killWithEscalation(pid, group = false) {
|
|
5515
|
+
const signal = (sig) => {
|
|
5516
|
+
if (group) {
|
|
5517
|
+
try {
|
|
5518
|
+
process.kill(-pid, sig);
|
|
5519
|
+
return;
|
|
5520
|
+
} catch {
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
process.kill(pid, sig);
|
|
5524
|
+
};
|
|
5525
|
+
try {
|
|
5526
|
+
signal("SIGTERM");
|
|
5527
|
+
} catch {
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
const deadline = Date.now() + 3e3;
|
|
5531
|
+
while (Date.now() < deadline) {
|
|
5532
|
+
if (!isProcessAlive(pid)) return;
|
|
5533
|
+
spawnSync("sleep", ["0.1"], { stdio: "ignore" });
|
|
5534
|
+
}
|
|
5535
|
+
try {
|
|
5536
|
+
signal("SIGKILL");
|
|
5537
|
+
} catch {
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
async function runStop(agent) {
|
|
5541
|
+
const data = readPidfile(agent);
|
|
5542
|
+
if (data === null) {
|
|
5543
|
+
process.stderr.write(`No running session found for agent "${agent}".
|
|
5544
|
+
`);
|
|
5545
|
+
process.exit(1);
|
|
5546
|
+
}
|
|
5547
|
+
await withResourceLock(() => {
|
|
5548
|
+
if (typeof data.supervisorPid === "number" && isProcessAlive(data.supervisorPid)) {
|
|
5549
|
+
killWithEscalation(data.supervisorPid);
|
|
5550
|
+
}
|
|
5551
|
+
removePidfile(agent);
|
|
5552
|
+
releaseFsServerIfUnused(data.dir, agent);
|
|
5553
|
+
releaseProxyIfUnused(data.proxyPort, agent);
|
|
5554
|
+
});
|
|
5555
|
+
process.stderr.write(`Stopped agent "${agent}".
|
|
5556
|
+
`);
|
|
5557
|
+
}
|
|
5558
|
+
async function runRestart(opts) {
|
|
5559
|
+
if (opts.agent.length === 0) {
|
|
5560
|
+
process.stderr.write("Error: --agent <name> is required for restart.\n");
|
|
5561
|
+
process.exit(1);
|
|
5562
|
+
}
|
|
5563
|
+
const existing = readPidfile(opts.agent);
|
|
5564
|
+
const dir = opts.dir.length > 0 ? opts.dir : existing?.dir ?? "";
|
|
5565
|
+
if (dir.length === 0) {
|
|
5566
|
+
process.stderr.write(
|
|
5567
|
+
`Don't know which folder to restart agent "${opts.agent}" with.
|
|
5568
|
+
Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${opts.agent}
|
|
5569
|
+
`
|
|
5570
|
+
);
|
|
5571
|
+
process.exit(1);
|
|
5572
|
+
}
|
|
5573
|
+
if (existing !== null) {
|
|
5574
|
+
await runStop(opts.agent);
|
|
5575
|
+
}
|
|
5576
|
+
await runDetached({
|
|
5577
|
+
...opts,
|
|
5578
|
+
dir,
|
|
5579
|
+
proxyPort: opts.proxyPort ?? existing?.proxyPort});
|
|
5580
|
+
}
|
|
5581
|
+
async function runDetached(opts) {
|
|
5582
|
+
const absDir = resolve(opts.dir);
|
|
5583
|
+
if (!existsSync(absDir)) {
|
|
5584
|
+
process.stderr.write(`Directory not found: ${opts.dir}. Check the path and try again.
|
|
5585
|
+
`);
|
|
5586
|
+
process.exit(1);
|
|
5587
|
+
}
|
|
5588
|
+
const existing = readPidfile(opts.agent);
|
|
5589
|
+
if (existing !== null) {
|
|
5590
|
+
const alive = typeof existing.supervisorPid === "number" && isProcessAlive(existing.supervisorPid);
|
|
5591
|
+
if (alive) {
|
|
5592
|
+
process.stderr.write(
|
|
5593
|
+
`Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
|
|
5594
|
+
Stop with: npx multicorn-shield files stop --agent ${opts.agent}
|
|
5595
|
+
`
|
|
5596
|
+
);
|
|
5597
|
+
return;
|
|
5598
|
+
}
|
|
5599
|
+
removePidfile(opts.agent);
|
|
5600
|
+
}
|
|
5601
|
+
const args = ["files", absDir, "--agent", opts.agent, "--foreground"];
|
|
5602
|
+
if (opts.port !== void 0) args.push("--port", String(opts.port));
|
|
5603
|
+
if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
|
|
5604
|
+
if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
|
|
5605
|
+
if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
|
|
5606
|
+
if (opts.client !== void 0) args.push("--client", opts.client);
|
|
5607
|
+
const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
|
|
5608
|
+
const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
|
|
5609
|
+
mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
|
|
5610
|
+
const out = openSync(logFile, "a");
|
|
5611
|
+
const err = openSync(logFile, "a");
|
|
5612
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
5613
|
+
detached: true,
|
|
5614
|
+
stdio: ["ignore", out, err],
|
|
5615
|
+
env: { ...process.env }
|
|
5616
|
+
});
|
|
5617
|
+
child.unref();
|
|
5618
|
+
let pidData = null;
|
|
5619
|
+
for (let i = 0; i < 60; i++) {
|
|
5620
|
+
await sleep3(500);
|
|
5621
|
+
pidData = readPidfile(opts.agent);
|
|
5622
|
+
if (pidData !== null) break;
|
|
5623
|
+
if (child.pid !== void 0 && !isProcessAlive(child.pid)) break;
|
|
5624
|
+
}
|
|
5625
|
+
if (pidData === null) {
|
|
5626
|
+
process.stderr.write(`Failed to start in background. Check logs: ${logFile}
|
|
5627
|
+
`);
|
|
5628
|
+
process.exit(1);
|
|
5629
|
+
}
|
|
5630
|
+
process.stderr.write("\n");
|
|
5631
|
+
process.stderr.write(
|
|
5632
|
+
` ${style2.green("\u2713")} Setup complete. Shield is running in the background.
|
|
5633
|
+
`
|
|
5634
|
+
);
|
|
5635
|
+
process.stderr.write(` ${style2.green("\u2713")} Folder: ${absDir}
|
|
5636
|
+
`);
|
|
5637
|
+
process.stderr.write(
|
|
5638
|
+
` ${style2.green("\u2713")} FS server :${String(pidData.fsPort)}, Proxy :${String(pidData.proxyPort)}
|
|
5639
|
+
`
|
|
5640
|
+
);
|
|
5641
|
+
process.stderr.write("\n");
|
|
5642
|
+
process.stderr.write(` Stop it any time with:
|
|
5643
|
+
`);
|
|
5644
|
+
process.stderr.write(
|
|
5645
|
+
` ${style2.cyan(`npx multicorn-shield files stop --agent ${opts.agent}`)}
|
|
5646
|
+
`
|
|
5647
|
+
);
|
|
5648
|
+
process.stderr.write("\n");
|
|
5649
|
+
process.stderr.write(style2.dim(` Status: npx multicorn-shield files status
|
|
5650
|
+
`));
|
|
5651
|
+
process.stderr.write(style2.dim(` Logs: ${logFile}
|
|
5652
|
+
`));
|
|
5653
|
+
process.stderr.write("\n");
|
|
5654
|
+
}
|
|
5655
|
+
async function runFilesCommand(opts) {
|
|
5656
|
+
if (opts.status) {
|
|
5657
|
+
runStatus();
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
if (opts.restart) {
|
|
5661
|
+
await runRestart(opts);
|
|
5662
|
+
return;
|
|
5663
|
+
}
|
|
5664
|
+
if (opts.stop) {
|
|
5665
|
+
await runStop(opts.agent);
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
if (!opts.foreground) {
|
|
5669
|
+
await runDetached(opts);
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
const absDir = resolve(opts.dir);
|
|
5673
|
+
if (!existsSync(absDir)) {
|
|
5674
|
+
process.stderr.write(`Directory not found: ${opts.dir}. Check the path and try again.
|
|
5675
|
+
`);
|
|
5676
|
+
process.exit(1);
|
|
5677
|
+
}
|
|
5678
|
+
if (opts.baseUrl && !isAllowedShieldApiBaseUrl(opts.baseUrl)) {
|
|
5679
|
+
process.stderr.write(
|
|
5680
|
+
"Error: --base-url must use HTTPS or http://localhost for local development.\n"
|
|
5681
|
+
);
|
|
5682
|
+
process.exit(1);
|
|
5683
|
+
}
|
|
5684
|
+
const config = await resolveConfig(opts);
|
|
5685
|
+
const realDir = canonicalFolder(absDir);
|
|
5686
|
+
const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
|
|
5687
|
+
const existingPidfile = readPidfile(opts.agent);
|
|
5688
|
+
if (existingPidfile !== null) {
|
|
5689
|
+
const supervisorAlive = typeof existingPidfile.supervisorPid === "number" && isProcessAlive(existingPidfile.supervisorPid);
|
|
5690
|
+
if (supervisorAlive) {
|
|
5691
|
+
process.stderr.write(
|
|
5692
|
+
`A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
|
|
5693
|
+
`
|
|
5694
|
+
);
|
|
5695
|
+
process.exit(1);
|
|
5696
|
+
}
|
|
5697
|
+
removePidfile(opts.agent);
|
|
5698
|
+
}
|
|
5699
|
+
let fsPort;
|
|
5700
|
+
let proxyReused;
|
|
5701
|
+
let fsReused;
|
|
5702
|
+
try {
|
|
5703
|
+
const ensured = await withResourceLock(async () => {
|
|
5704
|
+
const proxyRes = await ensureProxy(proxyPort, config.baseUrl);
|
|
5705
|
+
const fsRes = await ensureFsServer(realDir, opts.port);
|
|
5706
|
+
writePidfile({
|
|
5707
|
+
agent: opts.agent,
|
|
5708
|
+
dir: realDir,
|
|
5709
|
+
supervisorPid: process.pid,
|
|
5710
|
+
fsPort: fsRes.port,
|
|
5711
|
+
proxyPort
|
|
5712
|
+
});
|
|
5713
|
+
return { proxyRes, fsRes };
|
|
5714
|
+
});
|
|
5715
|
+
fsPort = ensured.fsRes.port;
|
|
5716
|
+
proxyReused = ensured.proxyRes.reused;
|
|
5717
|
+
fsReused = ensured.fsRes.reused;
|
|
5718
|
+
} catch (error) {
|
|
5719
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5720
|
+
process.stderr.write(`${msg}
|
|
5721
|
+
`);
|
|
5722
|
+
process.exit(1);
|
|
5723
|
+
}
|
|
5724
|
+
let registration;
|
|
5725
|
+
try {
|
|
5726
|
+
registration = await registerAgentAndConfig(
|
|
5727
|
+
config.apiKey,
|
|
5728
|
+
config.baseUrl,
|
|
5729
|
+
opts.agent,
|
|
5730
|
+
fsPort,
|
|
5731
|
+
realDir
|
|
5732
|
+
);
|
|
5733
|
+
} catch (error) {
|
|
5734
|
+
await withResourceLock(() => {
|
|
5735
|
+
removePidfile(opts.agent);
|
|
5736
|
+
releaseFsServerIfUnused(realDir, opts.agent);
|
|
5737
|
+
releaseProxyIfUnused(proxyPort, opts.agent);
|
|
5738
|
+
});
|
|
5739
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5740
|
+
process.stderr.write(`Registration failed: ${msg}
|
|
5741
|
+
`);
|
|
5742
|
+
process.exit(1);
|
|
5743
|
+
}
|
|
5744
|
+
const heartbeatServerName = proxyServerSlug(opts.agent);
|
|
5745
|
+
const heartbeatTick = async () => {
|
|
5746
|
+
const proxyVersion = await readProxyVersion(proxyPort);
|
|
5747
|
+
const proxyOk = proxyVersion !== null || await probeProxyHealth(proxyPort);
|
|
5748
|
+
const fsOk = await isPortListening(fsPort);
|
|
5749
|
+
if (proxyOk && fsOk) {
|
|
5750
|
+
await sendHeartbeat(config.apiKey, config.baseUrl, heartbeatServerName, proxyVersion);
|
|
5751
|
+
}
|
|
5752
|
+
};
|
|
5753
|
+
void heartbeatTick();
|
|
5754
|
+
const heartbeatTimer = setInterval(() => {
|
|
5755
|
+
void heartbeatTick();
|
|
5756
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
5757
|
+
let cleaningUp = false;
|
|
5758
|
+
process.on("SIGINT", () => {
|
|
5759
|
+
if (cleaningUp) return;
|
|
5760
|
+
cleaningUp = true;
|
|
5761
|
+
void (async () => {
|
|
5762
|
+
clearInterval(heartbeatTimer);
|
|
5763
|
+
await withResourceLock(() => {
|
|
5764
|
+
removePidfile(opts.agent);
|
|
5765
|
+
releaseFsServerIfUnused(realDir, opts.agent);
|
|
5766
|
+
releaseProxyIfUnused(proxyPort, opts.agent);
|
|
5767
|
+
});
|
|
5768
|
+
process.exit(0);
|
|
5769
|
+
})();
|
|
5770
|
+
});
|
|
5771
|
+
process.on("SIGTERM", () => {
|
|
5772
|
+
clearInterval(heartbeatTimer);
|
|
5773
|
+
removePidfile(opts.agent);
|
|
5774
|
+
process.exit(0);
|
|
5775
|
+
});
|
|
5776
|
+
const dirLabel = `./${basename(absDir)}`;
|
|
5777
|
+
const localProxyUrl = `http://127.0.0.1:${String(proxyPort)}${registration.pathSegment}`;
|
|
5778
|
+
process.stderr.write("\n");
|
|
5779
|
+
process.stderr.write(
|
|
5780
|
+
` ${style2.green("\u2713")} ${proxyReused ? "Reusing shared proxy" : "Local proxy running"} (:${String(proxyPort)})
|
|
5781
|
+
`
|
|
5782
|
+
);
|
|
5783
|
+
process.stderr.write(
|
|
5784
|
+
` ${style2.green("\u2713")} ${fsReused ? "Reusing filesystem server for" : "Filesystem server on"} ${dirLabel} (:${String(fsPort)})
|
|
5785
|
+
`
|
|
5786
|
+
);
|
|
5787
|
+
process.stderr.write(` ${style2.green("\u2713")} Agent '${opts.agent}' registered with Shield
|
|
5788
|
+
`);
|
|
5789
|
+
const writeResult = await autoWriteClientConfig(
|
|
5790
|
+
opts.client,
|
|
5791
|
+
opts.agent,
|
|
5792
|
+
localProxyUrl,
|
|
5793
|
+
config.apiKey,
|
|
5794
|
+
absDir
|
|
5795
|
+
);
|
|
5796
|
+
const firstWritten = writeResult.written[0];
|
|
5797
|
+
if (firstWritten !== void 0) {
|
|
5798
|
+
for (const w of writeResult.written) {
|
|
5799
|
+
process.stderr.write(` ${style2.green("\u2713")} Config written to ${style2.cyan(w.path)}
|
|
5800
|
+
`);
|
|
5801
|
+
}
|
|
5802
|
+
process.stderr.write("\n");
|
|
5803
|
+
process.stderr.write(style2.dim(` ${firstWritten.reload}
|
|
5804
|
+
`));
|
|
5805
|
+
} else if (!writeResult.prompted) {
|
|
5806
|
+
process.stderr.write("\n");
|
|
5807
|
+
process.stderr.write(
|
|
5808
|
+
" Add this to your coding agent's MCP config so it routes through Shield:\n\n"
|
|
5809
|
+
);
|
|
5810
|
+
const configBlock = JSON.stringify(
|
|
5811
|
+
{
|
|
5812
|
+
mcpServers: {
|
|
5813
|
+
[opts.agent]: {
|
|
5814
|
+
url: hostedProxyUrlWithKeyParam(localProxyUrl, config.apiKey),
|
|
5815
|
+
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
},
|
|
5819
|
+
null,
|
|
5820
|
+
2
|
|
5821
|
+
);
|
|
5822
|
+
process.stderr.write(style2.cyan(configBlock) + "\n\n");
|
|
5823
|
+
}
|
|
5824
|
+
process.stderr.write(style2.dim(" Write and delete will ask for approval the first time.\n"));
|
|
5825
|
+
process.stderr.write("\n");
|
|
5826
|
+
process.stderr.write(
|
|
5827
|
+
` ${style2.green("\u2713")} Shield is running (foreground mode). Press Ctrl-C to stop.
|
|
5828
|
+
`
|
|
5829
|
+
);
|
|
5830
|
+
process.stderr.write("\n");
|
|
5831
|
+
}
|
|
5832
|
+
async function autoWriteClientConfig(clientFlag, agentName, localProxyUrl, apiKey, workspacePath) {
|
|
5833
|
+
if (clientFlag !== void 0 && clientFlag.length > 0) {
|
|
5834
|
+
if (clientFlag === "all") {
|
|
5835
|
+
const detected2 = detectInstalledClients();
|
|
5836
|
+
if (detected2.length === 0) return { written: [], prompted: false };
|
|
5837
|
+
const results = [];
|
|
5838
|
+
for (const c of detected2) {
|
|
5839
|
+
const path2 = await writeLocalMcpEntry(c, agentName, localProxyUrl, apiKey, workspacePath);
|
|
5840
|
+
if (path2 !== null) {
|
|
5841
|
+
results.push({ client: c, path: path2, reload: clientReloadInstruction(c) });
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
return { written: results, prompted: false };
|
|
5845
|
+
}
|
|
5846
|
+
const client2 = clientFlag;
|
|
5847
|
+
if (!CODING_CLIENTS.includes(client2)) {
|
|
5848
|
+
process.stderr.write(
|
|
5849
|
+
`
|
|
5850
|
+
Unknown --client "${clientFlag}". Valid options: ${CODING_CLIENTS.join(", ")}, all
|
|
5851
|
+
`
|
|
5852
|
+
);
|
|
5853
|
+
return { written: [], prompted: false };
|
|
5854
|
+
}
|
|
5855
|
+
const path = await writeLocalMcpEntry(client2, agentName, localProxyUrl, apiKey, workspacePath);
|
|
5856
|
+
if (path !== null) {
|
|
5857
|
+
return {
|
|
5858
|
+
written: [{ client: client2, path, reload: clientReloadInstruction(client2) }],
|
|
5859
|
+
prompted: false
|
|
5860
|
+
};
|
|
5861
|
+
}
|
|
5862
|
+
process.stderr.write(
|
|
5863
|
+
`
|
|
5864
|
+
Could not write to ${clientDisplayName(client2)} config (parse error or permissions).
|
|
5865
|
+
`
|
|
5866
|
+
);
|
|
5867
|
+
return { written: [], prompted: false };
|
|
5868
|
+
}
|
|
5869
|
+
const detected = detectInstalledClients();
|
|
5870
|
+
if (detected.length === 0) {
|
|
5871
|
+
return { written: [], prompted: false };
|
|
5872
|
+
}
|
|
5873
|
+
if (detected.length === 1) {
|
|
5874
|
+
const client2 = detected[0];
|
|
5875
|
+
if (client2 === void 0) {
|
|
5876
|
+
return { written: [], prompted: false };
|
|
5877
|
+
}
|
|
5878
|
+
const path = await writeLocalMcpEntry(client2, agentName, localProxyUrl, apiKey, workspacePath);
|
|
5879
|
+
if (path !== null) {
|
|
5880
|
+
return {
|
|
5881
|
+
written: [{ client: client2, path, reload: clientReloadInstruction(client2) }],
|
|
5882
|
+
prompted: false
|
|
5883
|
+
};
|
|
5884
|
+
}
|
|
5885
|
+
process.stderr.write(
|
|
5886
|
+
`
|
|
5887
|
+
Could not safely update ${clientDisplayName(client2)} config (parse error or permissions). Left it unchanged.
|
|
5888
|
+
`
|
|
5889
|
+
);
|
|
5890
|
+
return { written: [], prompted: false };
|
|
5891
|
+
}
|
|
5892
|
+
if (!process.stdin.isTTY) {
|
|
5893
|
+
process.stderr.write("\n");
|
|
5894
|
+
process.stderr.write(" Multiple coding agents detected:\n");
|
|
5895
|
+
for (const c of detected) {
|
|
5896
|
+
process.stderr.write(` - ${clientDisplayName(c)} (--client ${c})
|
|
5897
|
+
`);
|
|
5898
|
+
}
|
|
5899
|
+
process.stderr.write(
|
|
5900
|
+
"\n Re-run with --client <name> to write config, or --client all for all.\n"
|
|
5901
|
+
);
|
|
5902
|
+
return { written: [], prompted: true };
|
|
5903
|
+
}
|
|
5904
|
+
process.stderr.write("\n");
|
|
5905
|
+
process.stderr.write(" Multiple coding agents detected. Which should route through Shield?\n\n");
|
|
5906
|
+
for (const [i, c] of detected.entries()) {
|
|
5907
|
+
process.stderr.write(` ${String(i + 1)}) ${clientDisplayName(c)}
|
|
5908
|
+
`);
|
|
5909
|
+
}
|
|
5910
|
+
process.stderr.write(` a) All of them
|
|
5911
|
+
`);
|
|
5912
|
+
process.stderr.write("\n");
|
|
5913
|
+
const choice = await promptLine(" Enter number (or 'a' for all): ");
|
|
5914
|
+
const trimmed = choice.trim().toLowerCase();
|
|
5915
|
+
if (trimmed === "a" || trimmed === "all") {
|
|
5916
|
+
const results = [];
|
|
5917
|
+
for (const c of detected) {
|
|
5918
|
+
const path = await writeLocalMcpEntry(c, agentName, localProxyUrl, apiKey, workspacePath);
|
|
5919
|
+
if (path !== null) {
|
|
5920
|
+
results.push({ client: c, path, reload: clientReloadInstruction(c) });
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
return { written: results, prompted: false };
|
|
5924
|
+
}
|
|
5925
|
+
const num = Number.parseInt(trimmed, 10);
|
|
5926
|
+
const client = num >= 1 && num <= detected.length ? detected[num - 1] : void 0;
|
|
5927
|
+
if (client !== void 0) {
|
|
5928
|
+
const path = await writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, workspacePath);
|
|
5929
|
+
if (path !== null) {
|
|
5930
|
+
return {
|
|
5931
|
+
written: [{ client, path, reload: clientReloadInstruction(client) }],
|
|
5932
|
+
prompted: false
|
|
5933
|
+
};
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
process.stderr.write(` Invalid choice. Use --client <name> next time.
|
|
5937
|
+
`);
|
|
5938
|
+
return { written: [], prompted: true };
|
|
5939
|
+
}
|
|
5940
|
+
function promptLine(question) {
|
|
5941
|
+
return new Promise((resolve3) => {
|
|
5942
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5943
|
+
rl.question(question, (answer) => {
|
|
5944
|
+
rl.close();
|
|
5945
|
+
resolve3(answer);
|
|
5946
|
+
});
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
// bin/multicorn-shield.ts
|
|
5951
|
+
function parseArgs(argv) {
|
|
5952
|
+
const args = argv.slice(2);
|
|
5953
|
+
let subcommand = "help";
|
|
5954
|
+
let wrapCommand = "";
|
|
5955
|
+
let wrapArgs = [];
|
|
5956
|
+
let logLevel = "info";
|
|
5957
|
+
let baseUrl = void 0;
|
|
5958
|
+
let dashboardUrl = "";
|
|
5959
|
+
let agentName = "";
|
|
5960
|
+
let deleteAgentName = "";
|
|
5961
|
+
let apiKey = void 0;
|
|
5962
|
+
let verbose = false;
|
|
5963
|
+
let filesDir = "";
|
|
5964
|
+
let filesPort = void 0;
|
|
5965
|
+
let filesProxyPort = void 0;
|
|
5966
|
+
let filesStop = false;
|
|
5967
|
+
let filesClient = void 0;
|
|
5968
|
+
let filesForeground = false;
|
|
5969
|
+
let filesStatus = false;
|
|
5970
|
+
let filesRestart = false;
|
|
5971
|
+
for (let i = 0; i < args.length; i++) {
|
|
5972
|
+
const arg = args[i];
|
|
5973
|
+
if (arg === "init") {
|
|
5974
|
+
subcommand = "init";
|
|
5975
|
+
} else if (arg === "files") {
|
|
5976
|
+
subcommand = "files";
|
|
5977
|
+
const tail = args.slice(i + 1);
|
|
5978
|
+
for (let j = 0; j < tail.length; j++) {
|
|
5979
|
+
const token = tail[j];
|
|
5980
|
+
if (token === void 0) continue;
|
|
5981
|
+
if (token === "--agent") {
|
|
5982
|
+
const value = tail[j + 1];
|
|
5983
|
+
if (value !== void 0) {
|
|
5984
|
+
agentName = value;
|
|
5985
|
+
j++;
|
|
5986
|
+
}
|
|
5987
|
+
} else if (token === "--port") {
|
|
5988
|
+
const value = tail[j + 1];
|
|
5989
|
+
if (value !== void 0) {
|
|
5990
|
+
filesPort = Number.parseInt(value, 10);
|
|
5991
|
+
j++;
|
|
5992
|
+
}
|
|
5993
|
+
} else if (token === "--proxy-port") {
|
|
5994
|
+
const value = tail[j + 1];
|
|
5995
|
+
if (value !== void 0) {
|
|
5996
|
+
filesProxyPort = Number.parseInt(value, 10);
|
|
5997
|
+
j++;
|
|
5998
|
+
}
|
|
5999
|
+
} else if (token === "--api-key") {
|
|
6000
|
+
const value = tail[j + 1];
|
|
6001
|
+
if (value !== void 0) {
|
|
6002
|
+
apiKey = value;
|
|
6003
|
+
j++;
|
|
6004
|
+
}
|
|
6005
|
+
} else if (token === "--base-url") {
|
|
6006
|
+
const value = tail[j + 1];
|
|
6007
|
+
if (value !== void 0) {
|
|
6008
|
+
baseUrl = value;
|
|
6009
|
+
j++;
|
|
6010
|
+
}
|
|
6011
|
+
} else if (token === "--client") {
|
|
6012
|
+
const value = tail[j + 1];
|
|
6013
|
+
if (value !== void 0) {
|
|
6014
|
+
filesClient = value;
|
|
6015
|
+
j++;
|
|
6016
|
+
}
|
|
6017
|
+
} else if (token === "--stop") {
|
|
6018
|
+
filesStop = true;
|
|
6019
|
+
} else if (token === "--foreground") {
|
|
6020
|
+
filesForeground = true;
|
|
6021
|
+
} else if (token === "--detach") ; else if (token === "stop") {
|
|
6022
|
+
filesStop = true;
|
|
6023
|
+
} else if (token === "status") {
|
|
6024
|
+
filesStatus = true;
|
|
6025
|
+
} else if (token === "restart") {
|
|
6026
|
+
filesRestart = true;
|
|
6027
|
+
} else if (!token.startsWith("-") && filesDir === "") {
|
|
6028
|
+
filesDir = token;
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
break;
|
|
6032
|
+
} else if (arg === "agents") {
|
|
6033
|
+
subcommand = "agents";
|
|
6034
|
+
} else if (arg === "delete-agent") {
|
|
6035
|
+
subcommand = "delete-agent";
|
|
6036
|
+
const name = args[i + 1];
|
|
6037
|
+
if (name === void 0 || name.startsWith("-")) {
|
|
6038
|
+
process.stderr.write("Error: delete-agent requires an agent name.\n");
|
|
6039
|
+
process.stderr.write("Example: npx multicorn-shield delete-agent my-agent\n");
|
|
6040
|
+
process.exit(1);
|
|
6041
|
+
}
|
|
6042
|
+
deleteAgentName = name;
|
|
6043
|
+
i++;
|
|
6044
|
+
} else if (arg === "--wrap") {
|
|
6045
|
+
subcommand = "wrap";
|
|
6046
|
+
const tail = args.slice(i + 1);
|
|
6047
|
+
const remaining = [];
|
|
6048
|
+
for (let j = 0; j < tail.length; j++) {
|
|
6049
|
+
const token = tail[j];
|
|
6050
|
+
if (token === void 0) continue;
|
|
6051
|
+
if (remaining.length > 0) {
|
|
6052
|
+
remaining.push(token);
|
|
6053
|
+
continue;
|
|
6054
|
+
}
|
|
6055
|
+
if (token === "--agent-name") {
|
|
6056
|
+
const value = tail[j + 1];
|
|
6057
|
+
if (value !== void 0) {
|
|
6058
|
+
agentName = value;
|
|
6059
|
+
j++;
|
|
6060
|
+
}
|
|
6061
|
+
} else if (token === "--log-level") {
|
|
6062
|
+
const value = tail[j + 1];
|
|
6063
|
+
if (value !== void 0 && isValidLogLevel(value)) {
|
|
6064
|
+
logLevel = value;
|
|
6065
|
+
j++;
|
|
6066
|
+
}
|
|
6067
|
+
} else if (token === "--base-url") {
|
|
6068
|
+
const value = tail[j + 1];
|
|
6069
|
+
if (value !== void 0) {
|
|
6070
|
+
baseUrl = value;
|
|
6071
|
+
j++;
|
|
6072
|
+
}
|
|
6073
|
+
} else if (token === "--dashboard-url") {
|
|
6074
|
+
const value = tail[j + 1];
|
|
6075
|
+
if (value !== void 0) {
|
|
6076
|
+
dashboardUrl = value;
|
|
6077
|
+
j++;
|
|
6078
|
+
}
|
|
6079
|
+
} else if (token === "--api-key") {
|
|
6080
|
+
const value = tail[j + 1];
|
|
6081
|
+
if (value !== void 0) {
|
|
6082
|
+
apiKey = value;
|
|
6083
|
+
j++;
|
|
6084
|
+
}
|
|
6085
|
+
} else {
|
|
6086
|
+
remaining.push(token);
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
6089
|
+
if (remaining.length === 0) {
|
|
6090
|
+
process.stderr.write("Error: --wrap requires a command to run.\n");
|
|
6091
|
+
process.stderr.write("Example: npx multicorn-shield --wrap my-mcp-server\n");
|
|
6092
|
+
process.exit(1);
|
|
6093
|
+
}
|
|
6094
|
+
wrapCommand = remaining[0] ?? "";
|
|
6095
|
+
wrapArgs = remaining.slice(1);
|
|
6096
|
+
break;
|
|
6097
|
+
} else if (arg === "--log-level") {
|
|
6098
|
+
const next = args[i + 1];
|
|
6099
|
+
if (next !== void 0 && isValidLogLevel(next)) {
|
|
6100
|
+
logLevel = next;
|
|
6101
|
+
i++;
|
|
6102
|
+
}
|
|
6103
|
+
} else if (arg === "--base-url") {
|
|
6104
|
+
const next = args[i + 1];
|
|
6105
|
+
if (next !== void 0) {
|
|
6106
|
+
baseUrl = next;
|
|
6107
|
+
i++;
|
|
6108
|
+
}
|
|
6109
|
+
} else if (arg === "--dashboard-url") {
|
|
6110
|
+
const next = args[i + 1];
|
|
6111
|
+
if (next !== void 0) {
|
|
6112
|
+
dashboardUrl = next;
|
|
6113
|
+
i++;
|
|
6114
|
+
}
|
|
6115
|
+
} else if (arg === "--agent-name") {
|
|
6116
|
+
const next = args[i + 1];
|
|
6117
|
+
if (next !== void 0) {
|
|
6118
|
+
agentName = next;
|
|
6119
|
+
i++;
|
|
6120
|
+
}
|
|
6121
|
+
} else if (arg === "--api-key") {
|
|
6122
|
+
const next = args[i + 1];
|
|
6123
|
+
if (next !== void 0) {
|
|
6124
|
+
apiKey = next;
|
|
6125
|
+
i++;
|
|
6126
|
+
}
|
|
6127
|
+
} else if (arg === "--verbose" || arg === "--debug") {
|
|
6128
|
+
verbose = true;
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
6131
|
+
return {
|
|
6132
|
+
subcommand,
|
|
6133
|
+
wrapCommand,
|
|
6134
|
+
wrapArgs,
|
|
6135
|
+
logLevel,
|
|
6136
|
+
baseUrl,
|
|
6137
|
+
dashboardUrl,
|
|
6138
|
+
agentName,
|
|
6139
|
+
deleteAgentName,
|
|
6140
|
+
apiKey,
|
|
6141
|
+
verbose,
|
|
6142
|
+
filesDir,
|
|
6143
|
+
filesPort,
|
|
6144
|
+
filesProxyPort,
|
|
6145
|
+
filesStop,
|
|
6146
|
+
filesClient,
|
|
6147
|
+
filesForeground,
|
|
6148
|
+
filesStatus,
|
|
6149
|
+
filesRestart
|
|
6150
|
+
};
|
|
6151
|
+
}
|
|
6152
|
+
function printHelp() {
|
|
6153
|
+
process.stderr.write(
|
|
6154
|
+
[
|
|
6155
|
+
"multicorn-shield: MCP permission proxy and Shield setup",
|
|
6156
|
+
"",
|
|
6157
|
+
"Usage:",
|
|
6158
|
+
" npx multicorn-shield init",
|
|
6159
|
+
" Interactive setup. Saves API key to ~/.multicorn/config.json.",
|
|
6160
|
+
"",
|
|
6161
|
+
" npx multicorn-shield files <dir> --agent <name> [--client <client>]",
|
|
6162
|
+
" Share a local folder with a coding agent. Starts a filesystem MCP server",
|
|
6163
|
+
" scoped to <dir>, registers the agent, writes your coding agent's MCP config,",
|
|
6164
|
+
" then exits. The service runs in the background until stopped.",
|
|
6165
|
+
"",
|
|
6166
|
+
" --client <name> Target client: cursor, cline, windsurf, claude, copilot,",
|
|
6167
|
+
" goose, gemini, codex, continue, kilo, opencode",
|
|
6168
|
+
" Auto-detected if omitted. Use --client all to write to every",
|
|
6169
|
+
" detected client.",
|
|
6170
|
+
" --foreground Keep the terminal open (for debugging). Default is background.",
|
|
6171
|
+
" --stop Tear down the servers started by a previous `files` invocation.",
|
|
6172
|
+
"",
|
|
6173
|
+
" npx multicorn-shield files stop --agent <name>",
|
|
6174
|
+
" Stop background processes for the named agent.",
|
|
6175
|
+
"",
|
|
6176
|
+
" npx multicorn-shield files restart --agent <name>",
|
|
6177
|
+
" Stop then start the named agent, reusing the folder from its last run.",
|
|
6178
|
+
" Rewrites the coding agent's MCP config entry, so this repairs a stale",
|
|
6179
|
+
" entry that a plain stop/start would leave in place.",
|
|
6180
|
+
"",
|
|
6181
|
+
" npx multicorn-shield files status",
|
|
6182
|
+
" Show all running file-sharing sessions.",
|
|
6183
|
+
"",
|
|
6184
|
+
" npx multicorn-shield restore",
|
|
6185
|
+
" Restore MCP servers in claude_desktop_config.json from the Shield extension backup.",
|
|
6186
|
+
"",
|
|
6187
|
+
" npx multicorn-shield agents",
|
|
6188
|
+
" List configured agents and show which is the default.",
|
|
6189
|
+
"",
|
|
6190
|
+
" npx multicorn-shield delete-agent <name>",
|
|
6191
|
+
" Remove a saved agent.",
|
|
6192
|
+
"",
|
|
6193
|
+
" npx multicorn-shield --wrap <command> [args...]",
|
|
6194
|
+
" Start <command> as an MCP server and proxy all tool calls through",
|
|
6195
|
+
" Shield's permission layer.",
|
|
6196
|
+
"",
|
|
6197
|
+
"Options:",
|
|
6198
|
+
" --version, -v Print version and exit",
|
|
6199
|
+
" --verbose, --debug Print extra diagnostics during init (menu selection, agent counts)",
|
|
6200
|
+
" --api-key <key> Multicorn API key (overrides MULTICORN_API_KEY env var and config file)",
|
|
6201
|
+
" --log-level <level> Log level: debug | info | warn | error (default: info)",
|
|
6202
|
+
" --base-url <url> Multicorn API base URL (default: https://api.multicorn.ai)",
|
|
6203
|
+
" --dashboard-url <url> Dashboard URL for consent page (default: derived from --base-url)",
|
|
6204
|
+
" --agent-name <name> Override agent name derived from the wrapped command",
|
|
6205
|
+
"",
|
|
6206
|
+
"Examples:",
|
|
6207
|
+
" npx multicorn-shield init",
|
|
6208
|
+
" npx multicorn-shield files ./my-repo --agent my-agent",
|
|
6209
|
+
" npx multicorn-shield files ./my-repo --agent my-agent --foreground",
|
|
6210
|
+
" npx multicorn-shield files status",
|
|
6211
|
+
" npx multicorn-shield files stop --agent my-agent",
|
|
6212
|
+
" npx multicorn-shield files restart --agent my-agent",
|
|
6213
|
+
" npx multicorn-shield --wrap npx @modelcontextprotocol/server-filesystem /tmp",
|
|
6214
|
+
" npx multicorn-shield --wrap my-mcp-server --log-level debug",
|
|
6215
|
+
""
|
|
6216
|
+
].join("\n")
|
|
6217
|
+
);
|
|
6218
|
+
}
|
|
6219
|
+
async function runCli() {
|
|
6220
|
+
const first = process.argv[2];
|
|
6221
|
+
if (first === "restore") {
|
|
6222
|
+
await restoreClaudeDesktopMcpFromBackup();
|
|
6223
|
+
process.stderr.write(
|
|
6224
|
+
"Restored MCP server entries from ~/.multicorn/extension-backup.json into Claude Desktop config.\nRestart Claude Desktop to apply changes.\n"
|
|
6225
|
+
);
|
|
6226
|
+
return;
|
|
6227
|
+
}
|
|
6228
|
+
if (first === "--version" || first === "-v") {
|
|
6229
|
+
process.stdout.write(`${PACKAGE_VERSION}
|
|
6230
|
+
`);
|
|
6231
|
+
process.exit(0);
|
|
6232
|
+
}
|
|
6233
|
+
const cli = parseArgs(process.argv);
|
|
6234
|
+
const logger = createLogger(cli.logLevel);
|
|
4860
6235
|
if (cli.subcommand === "help") {
|
|
4861
6236
|
printHelp();
|
|
4862
6237
|
process.exit(0);
|
|
@@ -4865,6 +6240,48 @@ async function runCli() {
|
|
|
4865
6240
|
await runInit(cli.baseUrl, { verbose: cli.verbose });
|
|
4866
6241
|
return;
|
|
4867
6242
|
}
|
|
6243
|
+
if (cli.subcommand === "files") {
|
|
6244
|
+
if (cli.filesStatus) {
|
|
6245
|
+
await runFilesCommand({
|
|
6246
|
+
dir: "",
|
|
6247
|
+
agent: "",
|
|
6248
|
+
port: void 0,
|
|
6249
|
+
proxyPort: void 0,
|
|
6250
|
+
apiKey: void 0,
|
|
6251
|
+
baseUrl: void 0,
|
|
6252
|
+
stop: false,
|
|
6253
|
+
client: void 0,
|
|
6254
|
+
foreground: true,
|
|
6255
|
+
status: true,
|
|
6256
|
+
restart: false
|
|
6257
|
+
});
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
if (!cli.filesStop && cli.agentName.length === 0) {
|
|
6261
|
+
process.stderr.write("Error: --agent <name> is required for the files command.\n");
|
|
6262
|
+
process.stderr.write("Example: npx multicorn-shield files ./my-repo --agent my-agent\n");
|
|
6263
|
+
process.exit(1);
|
|
6264
|
+
}
|
|
6265
|
+
if (!cli.filesStop && !cli.filesRestart && cli.filesDir.length === 0) {
|
|
6266
|
+
process.stderr.write("Error: a directory path is required.\n");
|
|
6267
|
+
process.stderr.write("Example: npx multicorn-shield files ./my-repo --agent my-agent\n");
|
|
6268
|
+
process.exit(1);
|
|
6269
|
+
}
|
|
6270
|
+
await runFilesCommand({
|
|
6271
|
+
dir: cli.filesDir,
|
|
6272
|
+
agent: cli.agentName,
|
|
6273
|
+
port: cli.filesPort,
|
|
6274
|
+
proxyPort: cli.filesProxyPort,
|
|
6275
|
+
apiKey: cli.apiKey,
|
|
6276
|
+
baseUrl: cli.baseUrl,
|
|
6277
|
+
stop: cli.filesStop,
|
|
6278
|
+
client: cli.filesClient,
|
|
6279
|
+
foreground: cli.filesForeground,
|
|
6280
|
+
status: false,
|
|
6281
|
+
restart: cli.filesRestart
|
|
6282
|
+
});
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
4868
6285
|
if (cli.subcommand === "agents") {
|
|
4869
6286
|
const config2 = await loadConfig();
|
|
4870
6287
|
if (config2 === null) {
|