multicorn-shield 0.10.0 → 0.12.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 +40 -0
- package/dist/badge.js +44 -0
- package/dist/index.cjs +166 -17
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +165 -18
- package/dist/multicorn-proxy.js +181 -9
- package/dist/openclaw-hook/handler.js +0 -1
- package/dist/openclaw-plugin/multicorn-shield.js +14 -18
- package/dist/openclaw-plugin/openclaw.plugin.json +3 -1
- package/dist/proxy.cjs +174 -0
- package/dist/proxy.d.cts +228 -1
- package/dist/proxy.d.ts +228 -1
- package/dist/proxy.js +174 -1
- package/dist/shield-extension.js +126 -8
- package/package.json +17 -6
- package/plugins/cline/README.md +61 -0
- package/plugins/cline/hooks/scripts/post-tool-use.cjs +116 -0
- package/plugins/cline/hooks/scripts/pre-tool-use.cjs +271 -0
- package/plugins/cline/hooks/scripts/shared.cjs +303 -0
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { mkdir, writeFile, readFile, copyFile, unlink } from 'fs/promises';
|
|
3
|
+
import { mkdir, writeFile, readFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
@@ -473,19 +473,86 @@ async function installWindsurfNativeHooks() {
|
|
|
473
473
|
await mkdir(hooksDir, { recursive: true });
|
|
474
474
|
await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
|
|
475
475
|
}
|
|
476
|
-
|
|
476
|
+
function getClineHooksInstallDir() {
|
|
477
|
+
return join(homedir(), ".multicorn", "cline-hooks");
|
|
478
|
+
}
|
|
479
|
+
function getClineGlobalHooksDir() {
|
|
480
|
+
return join(homedir(), "Documents", "Cline", "Hooks");
|
|
481
|
+
}
|
|
482
|
+
async function installClineNativeHooks() {
|
|
483
|
+
const root = multicornShieldPackageRoot();
|
|
484
|
+
const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
|
|
485
|
+
const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
|
|
486
|
+
const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
|
|
487
|
+
if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const installDir = getClineHooksInstallDir();
|
|
493
|
+
await mkdir(installDir, { recursive: true });
|
|
494
|
+
const destPre = join(installDir, "pre-tool-use.cjs");
|
|
495
|
+
const destPost = join(installDir, "post-tool-use.cjs");
|
|
496
|
+
const destShared = join(installDir, "shared.cjs");
|
|
497
|
+
await copyFile(srcPre, destPre);
|
|
498
|
+
await copyFile(srcPost, destPost);
|
|
499
|
+
await copyFile(srcShared, destShared);
|
|
500
|
+
const hookScriptMode = 493;
|
|
501
|
+
await chmod(destPre, hookScriptMode);
|
|
502
|
+
await chmod(destPost, hookScriptMode);
|
|
503
|
+
await chmod(destShared, hookScriptMode);
|
|
504
|
+
const hooksDir = getClineGlobalHooksDir();
|
|
505
|
+
await mkdir(hooksDir, { recursive: true });
|
|
506
|
+
const preWrapper = join(hooksDir, "PreToolUse");
|
|
507
|
+
const postWrapper = join(hooksDir, "PostToolUse");
|
|
508
|
+
const preContent = `#!/usr/bin/env node
|
|
509
|
+
require(${JSON.stringify(destPre)});
|
|
510
|
+
`;
|
|
511
|
+
const postContent = `#!/usr/bin/env node
|
|
512
|
+
require(${JSON.stringify(destPost)});
|
|
513
|
+
`;
|
|
514
|
+
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
515
|
+
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
516
|
+
}
|
|
517
|
+
async function promptClineIntegrationMode(ask) {
|
|
518
|
+
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
519
|
+
process.stderr.write(
|
|
520
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
|
|
521
|
+
);
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
|
|
524
|
+
);
|
|
525
|
+
let choice = 0;
|
|
526
|
+
while (choice === 0) {
|
|
527
|
+
const input = await ask("Choose integration (1-2): ");
|
|
528
|
+
const num = parseInt(input.trim(), 10);
|
|
529
|
+
if (num === 1) choice = 1;
|
|
530
|
+
if (num === 2) choice = 2;
|
|
531
|
+
}
|
|
532
|
+
return choice === 1 ? "native" : "hosted";
|
|
533
|
+
}
|
|
534
|
+
var PLATFORM_LABELS = [
|
|
535
|
+
"OpenClaw",
|
|
536
|
+
"Claude Code",
|
|
537
|
+
"Cursor",
|
|
538
|
+
"Windsurf",
|
|
539
|
+
"Cline",
|
|
540
|
+
"Local MCP / Other"
|
|
541
|
+
];
|
|
477
542
|
var PLATFORM_BY_SELECTION = {
|
|
478
543
|
1: "openclaw",
|
|
479
544
|
2: "claude-code",
|
|
480
545
|
3: "cursor",
|
|
481
546
|
4: "windsurf",
|
|
482
|
-
5: "
|
|
547
|
+
5: "cline",
|
|
548
|
+
6: "other-mcp"
|
|
483
549
|
};
|
|
484
550
|
var DEFAULT_AGENT_NAMES = {
|
|
485
551
|
openclaw: "my-openclaw-agent",
|
|
486
552
|
"claude-code": "my-claude-code-agent",
|
|
487
553
|
cursor: "my-cursor-agent",
|
|
488
|
-
windsurf: "my-windsurf-agent"
|
|
554
|
+
windsurf: "my-windsurf-agent",
|
|
555
|
+
cline: "my-cline-agent"
|
|
489
556
|
};
|
|
490
557
|
async function promptPlatformSelection(ask) {
|
|
491
558
|
process.stderr.write(
|
|
@@ -505,13 +572,13 @@ async function promptPlatformSelection(ask) {
|
|
|
505
572
|
);
|
|
506
573
|
}
|
|
507
574
|
process.stderr.write(
|
|
508
|
-
style.dim(" Pick
|
|
575
|
+
style.dim(" Pick 6 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
|
|
509
576
|
);
|
|
510
577
|
let selection = 0;
|
|
511
578
|
while (selection === 0) {
|
|
512
|
-
const input = await ask("Select (1-
|
|
579
|
+
const input = await ask("Select (1-6): ");
|
|
513
580
|
const num = parseInt(input.trim(), 10);
|
|
514
|
-
if (num >= 1 && num <=
|
|
581
|
+
if (num >= 1 && num <= 6) {
|
|
515
582
|
selection = num;
|
|
516
583
|
}
|
|
517
584
|
}
|
|
@@ -632,7 +699,7 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
|
|
|
632
699
|
return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
633
700
|
}
|
|
634
701
|
function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
635
|
-
const usesInlineKey = platform === "cursor" || platform === "windsurf";
|
|
702
|
+
const usesInlineKey = platform === "cursor" || platform === "windsurf" || platform === "cline";
|
|
636
703
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
637
704
|
const urlKey = platform === "windsurf" ? "serverUrl" : "url";
|
|
638
705
|
const mcpSnippet = JSON.stringify(
|
|
@@ -657,6 +724,23 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
657
724
|
process.stderr.write(
|
|
658
725
|
"\n" + style.dim("Add this to ~/.codeium/windsurf/mcp_config.json:") + "\n\n"
|
|
659
726
|
);
|
|
727
|
+
} else if (platform === "cline") {
|
|
728
|
+
process.stderr.write("\n" + style.dim("Add this to your Cline MCP settings file:") + "\n");
|
|
729
|
+
process.stderr.write(
|
|
730
|
+
style.dim(
|
|
731
|
+
" macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
|
|
732
|
+
) + "\n"
|
|
733
|
+
);
|
|
734
|
+
process.stderr.write(
|
|
735
|
+
style.dim(
|
|
736
|
+
" Windows: %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json"
|
|
737
|
+
) + "\n"
|
|
738
|
+
);
|
|
739
|
+
process.stderr.write(
|
|
740
|
+
style.dim(
|
|
741
|
+
" Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
|
|
742
|
+
) + "\n\n"
|
|
743
|
+
);
|
|
660
744
|
} else {
|
|
661
745
|
process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
|
|
662
746
|
}
|
|
@@ -680,6 +764,13 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
680
764
|
) + "\n"
|
|
681
765
|
);
|
|
682
766
|
}
|
|
767
|
+
if (platform === "cline") {
|
|
768
|
+
process.stderr.write(
|
|
769
|
+
style.dim(
|
|
770
|
+
"After pasting, restart Cline or reload the VS Code window. Cline will discover the Shield tools automatically."
|
|
771
|
+
) + "\n"
|
|
772
|
+
);
|
|
773
|
+
}
|
|
683
774
|
if (platform === "windsurf") {
|
|
684
775
|
process.stderr.write(style.dim("Then restart Windsurf (Cmd/Ctrl+Q, then reopen).") + "\n");
|
|
685
776
|
process.stderr.write(
|
|
@@ -777,7 +868,7 @@ async function runInit(explicitBaseUrl) {
|
|
|
777
868
|
const selection = await promptPlatformSelection(ask);
|
|
778
869
|
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
779
870
|
const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
|
|
780
|
-
if (selection ===
|
|
871
|
+
if (selection === 6) {
|
|
781
872
|
const raw = existing !== null ? { ...existing } : {};
|
|
782
873
|
raw["apiKey"] = apiKey;
|
|
783
874
|
raw["baseUrl"] = resolvedBaseUrl;
|
|
@@ -1001,6 +1092,71 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1001
1092
|
setupSucceeded = true;
|
|
1002
1093
|
}
|
|
1003
1094
|
}
|
|
1095
|
+
} else if (selection === 5) {
|
|
1096
|
+
const clineMode = await promptClineIntegrationMode(ask);
|
|
1097
|
+
if (clineMode === "native") {
|
|
1098
|
+
try {
|
|
1099
|
+
await installClineNativeHooks();
|
|
1100
|
+
process.stderr.write("\n" + style.bold("Shield Cline hooks installed") + "\n\n");
|
|
1101
|
+
process.stderr.write(
|
|
1102
|
+
style.dim(
|
|
1103
|
+
"The Shield hook runs with your user permissions. It intercepts Cline tool calls to check permissions and log activity. Review the scripts under "
|
|
1104
|
+
) + style.cyan("~/.multicorn/cline-hooks") + style.dim(" if that is a concern.") + "\n"
|
|
1105
|
+
);
|
|
1106
|
+
configuredAgents.push({
|
|
1107
|
+
selection,
|
|
1108
|
+
platform: selectedPlatform,
|
|
1109
|
+
platformLabel: selectedLabel,
|
|
1110
|
+
agentName,
|
|
1111
|
+
clineIntegration: "native"
|
|
1112
|
+
});
|
|
1113
|
+
setupSucceeded = true;
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1116
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1120
|
+
let proxyUrl = "";
|
|
1121
|
+
let created = false;
|
|
1122
|
+
while (!created) {
|
|
1123
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
1124
|
+
try {
|
|
1125
|
+
proxyUrl = await createProxyConfig(
|
|
1126
|
+
resolvedBaseUrl,
|
|
1127
|
+
apiKey,
|
|
1128
|
+
agentName,
|
|
1129
|
+
targetUrl,
|
|
1130
|
+
shortName,
|
|
1131
|
+
selectedPlatform
|
|
1132
|
+
);
|
|
1133
|
+
spinner.stop(true, "Proxy config created!");
|
|
1134
|
+
created = true;
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1137
|
+
spinner.stop(false, detail);
|
|
1138
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
1139
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (created && proxyUrl.length > 0) {
|
|
1145
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
1146
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
1147
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
1148
|
+
configuredAgents.push({
|
|
1149
|
+
selection,
|
|
1150
|
+
platform: selectedPlatform,
|
|
1151
|
+
platformLabel: selectedLabel,
|
|
1152
|
+
agentName,
|
|
1153
|
+
shortName,
|
|
1154
|
+
proxyUrl,
|
|
1155
|
+
clineIntegration: "hosted"
|
|
1156
|
+
});
|
|
1157
|
+
setupSucceeded = true;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1004
1160
|
} else {
|
|
1005
1161
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1006
1162
|
let proxyUrl = "";
|
|
@@ -1119,6 +1275,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1119
1275
|
"\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
|
|
1120
1276
|
);
|
|
1121
1277
|
}
|
|
1278
|
+
const clineNativeConfigured = configuredAgents.some(
|
|
1279
|
+
(a) => a.platform === "cline" && a.clineIntegration === "native"
|
|
1280
|
+
);
|
|
1281
|
+
const clineHostedConfigured = configuredAgents.some(
|
|
1282
|
+
(a) => a.platform === "cline" && a.clineIntegration === "hosted"
|
|
1283
|
+
);
|
|
1284
|
+
if (clineNativeConfigured) {
|
|
1285
|
+
blocks.push(
|
|
1286
|
+
"\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
if (clineHostedConfigured) {
|
|
1290
|
+
blocks.push(
|
|
1291
|
+
"\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1122
1294
|
if (blocks.length > 0) {
|
|
1123
1295
|
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
1124
1296
|
process.stderr.write(blocks.join("") + "\n");
|
|
@@ -377,7 +377,6 @@ ${url}
|
|
|
377
377
|
}
|
|
378
378
|
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
379
379
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
380
|
-
console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
|
|
381
380
|
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
382
381
|
process.stderr.write(
|
|
383
382
|
`[multicorn-shield] Opening consent page...
|
|
@@ -315,14 +315,6 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
|
|
|
315
315
|
}
|
|
316
316
|
async function checkActionPermission(payload, apiKey, baseUrl, logger) {
|
|
317
317
|
try {
|
|
318
|
-
const requestBody = {
|
|
319
|
-
agent: payload.agent,
|
|
320
|
-
service: payload.service,
|
|
321
|
-
actionType: payload.actionType,
|
|
322
|
-
status: payload.status,
|
|
323
|
-
metadata: payload.metadata
|
|
324
|
-
};
|
|
325
|
-
console.error("[SHIELD-CLIENT] POST /api/v1/actions request: " + JSON.stringify(requestBody));
|
|
326
318
|
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
327
319
|
method: "POST",
|
|
328
320
|
headers: {
|
|
@@ -333,22 +325,18 @@ async function checkActionPermission(payload, apiKey, baseUrl, logger) {
|
|
|
333
325
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
334
326
|
});
|
|
335
327
|
if (response.status === 201) {
|
|
336
|
-
console.error(
|
|
337
|
-
"[SHIELD-CLIENT] response status=201, returning approved (body not read - backend may have failed approval creation)"
|
|
338
|
-
);
|
|
328
|
+
console.error("[SHIELD-CLIENT] POST /api/v1/actions: 201 approved");
|
|
339
329
|
return { status: "approved" };
|
|
340
330
|
}
|
|
341
331
|
if (response.status === 202) {
|
|
342
332
|
const body = await response.json();
|
|
343
333
|
const data = isApiSuccess(body) ? body.data : null;
|
|
344
|
-
console.error("[SHIELD-CLIENT]
|
|
334
|
+
console.error("[SHIELD-CLIENT] POST /api/v1/actions: 202 pending");
|
|
345
335
|
if (!isApiSuccess(body) || data === null) {
|
|
346
336
|
return { status: "blocked" };
|
|
347
337
|
}
|
|
348
338
|
const approvalId = typeof data["approval_id"] === "string" ? data["approval_id"] : void 0;
|
|
349
|
-
console.error(
|
|
350
|
-
"[SHIELD-CLIENT] extracted: status=" + String(data["status"]) + " approval_id=" + (approvalId ?? "undefined")
|
|
351
|
-
);
|
|
339
|
+
console.error("[SHIELD-CLIENT] extracted: approval_id=" + (approvalId ?? "undefined"));
|
|
352
340
|
if (approvalId === void 0) {
|
|
353
341
|
return { status: "blocked" };
|
|
354
342
|
}
|
|
@@ -439,7 +427,6 @@ ${url}
|
|
|
439
427
|
}
|
|
440
428
|
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
441
429
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
442
|
-
console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
|
|
443
430
|
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
444
431
|
process.stderr.write(
|
|
445
432
|
`[multicorn-shield] Opening consent page...
|
|
@@ -509,7 +496,14 @@ function readConfig() {
|
|
|
509
496
|
const resolvedBaseUrl = asString(cachedMulticornConfig?.baseUrl) ?? asString(process.env["MULTICORN_BASE_URL"]) ?? "https://api.multicorn.ai";
|
|
510
497
|
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? agentNameFromOpenclawPlatform(cachedMulticornConfig) ?? asString(cachedMulticornConfig?.agentName) ?? null;
|
|
511
498
|
const failMode = "closed";
|
|
512
|
-
|
|
499
|
+
let apiKey = resolvedApiKey;
|
|
500
|
+
if (apiKey.length > 0 && (!apiKey.startsWith("mcs_") || apiKey.length < 16)) {
|
|
501
|
+
pluginLogger?.error(
|
|
502
|
+
"Invalid API key format. Key must start with mcs_ and be at least 16 characters."
|
|
503
|
+
);
|
|
504
|
+
apiKey = "";
|
|
505
|
+
}
|
|
506
|
+
return { apiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
|
|
513
507
|
}
|
|
514
508
|
function asString(value) {
|
|
515
509
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -884,7 +878,9 @@ var plugin = {
|
|
|
884
878
|
if (config.agentName !== null) {
|
|
885
879
|
pinnedAgentName = config.agentName;
|
|
886
880
|
}
|
|
887
|
-
|
|
881
|
+
api.logger.info(
|
|
882
|
+
`Multicorn Shield config loaded: hasApiKey=${String((cachedMulticornConfig?.apiKey ?? "").length > 0)} baseUrl=${cachedMulticornConfig?.baseUrl ?? "default"} agentName=${cachedMulticornConfig?.agentName ?? "unset"} defaultAgent=${cachedMulticornConfig?.defaultAgent ?? "unset"} agents=${String(cachedMulticornConfig?.agents?.length ?? 0)}`
|
|
883
|
+
);
|
|
888
884
|
api.on("before_tool_call", beforeToolCall, { priority: 10 });
|
|
889
885
|
api.on("after_tool_call", afterToolCall);
|
|
890
886
|
api.logger.info("Multicorn Shield plugin registered.");
|
package/dist/proxy.cjs
CHANGED
|
@@ -437,12 +437,186 @@ function mapMcpToolToScope(toolName) {
|
|
|
437
437
|
return { service: head, permissionLevel, actionType };
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// src/logger/action-logger.ts
|
|
441
|
+
function createActionLogger(config) {
|
|
442
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
448
|
+
const timeout = config.timeout ?? 5e3;
|
|
449
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
455
|
+
const batchEnabled = config.batchMode?.enabled ?? false;
|
|
456
|
+
const maxBatchSize = config.batchMode?.maxSize ?? 10;
|
|
457
|
+
const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
|
|
458
|
+
const queue = [];
|
|
459
|
+
let flushTimer;
|
|
460
|
+
let isShutdown = false;
|
|
461
|
+
async function sendActions(actions) {
|
|
462
|
+
if (actions.length === 0) return;
|
|
463
|
+
const convertAction = (action) => ({
|
|
464
|
+
agent: action.agent,
|
|
465
|
+
service: action.service,
|
|
466
|
+
actionType: action.actionType,
|
|
467
|
+
status: action.status,
|
|
468
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
469
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
470
|
+
});
|
|
471
|
+
const convertedActions = actions.map(convertAction);
|
|
472
|
+
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
473
|
+
let lastError;
|
|
474
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
475
|
+
try {
|
|
476
|
+
const controller = new AbortController();
|
|
477
|
+
const timeoutId = setTimeout(() => {
|
|
478
|
+
controller.abort();
|
|
479
|
+
}, timeout);
|
|
480
|
+
const response = await fetch(endpoint, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: {
|
|
483
|
+
"Content-Type": "application/json",
|
|
484
|
+
"X-Multicorn-Key": config.apiKey
|
|
485
|
+
},
|
|
486
|
+
body: JSON.stringify(payload),
|
|
487
|
+
signal: controller.signal
|
|
488
|
+
});
|
|
489
|
+
clearTimeout(timeoutId);
|
|
490
|
+
if (response.ok) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (response.status >= 400 && response.status < 500) {
|
|
494
|
+
const body = await response.text().catch(() => "");
|
|
495
|
+
throw new Error(
|
|
496
|
+
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (response.status >= 500 && attempt === 0) {
|
|
500
|
+
lastError = new Error(
|
|
501
|
+
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
502
|
+
);
|
|
503
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
throw new Error(
|
|
507
|
+
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
508
|
+
);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error instanceof Error) {
|
|
511
|
+
if (error.name === "AbortError") {
|
|
512
|
+
lastError = new Error(
|
|
513
|
+
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
514
|
+
);
|
|
515
|
+
} else if (error.message.includes("Client error") || error.message.includes("Server error")) {
|
|
516
|
+
lastError = error;
|
|
517
|
+
} else {
|
|
518
|
+
lastError = new Error(
|
|
519
|
+
`[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
|
|
524
|
+
}
|
|
525
|
+
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
526
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (lastError) {
|
|
533
|
+
if (config.onError) {
|
|
534
|
+
config.onError(lastError);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function flushQueue() {
|
|
539
|
+
if (queue.length === 0) return;
|
|
540
|
+
const actions = queue.map((item) => item.payload);
|
|
541
|
+
queue.length = 0;
|
|
542
|
+
await sendActions(actions);
|
|
543
|
+
}
|
|
544
|
+
function startFlushTimer() {
|
|
545
|
+
if (flushTimer !== void 0) return;
|
|
546
|
+
flushTimer = setInterval(() => {
|
|
547
|
+
flushQueue().catch(() => {
|
|
548
|
+
});
|
|
549
|
+
}, flushInterval);
|
|
550
|
+
const timer = flushTimer;
|
|
551
|
+
if (typeof timer.unref === "function") {
|
|
552
|
+
timer.unref();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function stopFlushTimer() {
|
|
556
|
+
if (flushTimer) {
|
|
557
|
+
clearInterval(flushTimer);
|
|
558
|
+
flushTimer = void 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (batchEnabled) {
|
|
562
|
+
startFlushTimer();
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
logAction(action) {
|
|
566
|
+
if (isShutdown) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if (action.agent.trim().length === 0) {
|
|
572
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
573
|
+
}
|
|
574
|
+
if (action.service.trim().length === 0) {
|
|
575
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
576
|
+
}
|
|
577
|
+
if (action.actionType.trim().length === 0) {
|
|
578
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
579
|
+
}
|
|
580
|
+
if (action.status.trim().length === 0) {
|
|
581
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
582
|
+
}
|
|
583
|
+
if (batchEnabled) {
|
|
584
|
+
queue.push({ payload: action, timestamp: Date.now() });
|
|
585
|
+
if (queue.length >= maxBatchSize) {
|
|
586
|
+
flushQueue().catch(() => {
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
sendActions([action]).catch(() => {
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return Promise.resolve();
|
|
594
|
+
},
|
|
595
|
+
async flush() {
|
|
596
|
+
if (!batchEnabled) return;
|
|
597
|
+
await flushQueue();
|
|
598
|
+
},
|
|
599
|
+
async shutdown() {
|
|
600
|
+
if (isShutdown) return;
|
|
601
|
+
isShutdown = true;
|
|
602
|
+
stopFlushTimer();
|
|
603
|
+
if (batchEnabled) {
|
|
604
|
+
await flushQueue();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function sleep(ms) {
|
|
610
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
611
|
+
}
|
|
612
|
+
|
|
440
613
|
exports.ShieldAuthError = ShieldAuthError;
|
|
441
614
|
exports.buildAuthErrorResponse = buildAuthErrorResponse;
|
|
442
615
|
exports.buildBlockedResponse = buildBlockedResponse;
|
|
443
616
|
exports.buildInternalErrorResponse = buildInternalErrorResponse;
|
|
444
617
|
exports.buildServiceUnreachableResponse = buildServiceUnreachableResponse;
|
|
445
618
|
exports.buildSpendingBlockedResponse = buildSpendingBlockedResponse;
|
|
619
|
+
exports.createActionLogger = createActionLogger;
|
|
446
620
|
exports.createLogger = createLogger;
|
|
447
621
|
exports.deriveDashboardUrl = deriveDashboardUrl;
|
|
448
622
|
exports.extractActionFromToolName = extractActionFromToolName;
|