multicorn-shield 1.11.1 → 1.12.1
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 +34 -0
- package/README.md +10 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/multicorn-proxy.js +250 -201
- package/dist/multicorn-shield.js +246 -199
- package/dist/openclaw-hook/HOOK.md +5 -3
- package/dist/openclaw-hook/handler.js +1 -1
- package/dist/openclaw-plugin/multicorn-shield.js +28 -7
- package/dist/openclaw-plugin/openclaw.plugin.json +1 -1
- package/dist/server.js +44 -99
- package/dist/shield-extension.js +3 -12
- package/package.json +4 -2
- package/plugins/codex-cli/hooks/scripts/codex-cli-hooks-shared.cjs +50 -1
- package/plugins/opencode/multicorn-shield.test.ts +31 -0
- package/plugins/opencode/multicorn-shield.ts +10 -31
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +126 -17
|
@@ -33,9 +33,11 @@ metadata:
|
|
|
33
33
|
> "entries": {
|
|
34
34
|
> "multicorn-shield": {
|
|
35
35
|
> "enabled": true,
|
|
36
|
-
> "
|
|
37
|
-
> "
|
|
38
|
-
> "
|
|
36
|
+
> "config": {
|
|
37
|
+
> "apiKey": "${MULTICORN_API_KEY}",
|
|
38
|
+
> "baseUrl": "https://api.multicorn.ai",
|
|
39
|
+
> "agentName": "openclaw",
|
|
40
|
+
> "failMode": "closed"
|
|
39
41
|
> }
|
|
40
42
|
> }
|
|
41
43
|
> }
|
|
@@ -184,7 +184,7 @@ function handleHttpError(status, logger, retryDelaySeconds) {
|
|
|
184
184
|
if (status === 401 || status === 403) {
|
|
185
185
|
if (!authErrorLogged) {
|
|
186
186
|
authErrorLogged = true;
|
|
187
|
-
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in
|
|
187
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in ~/.multicorn/config.json (from npx multicorn-shield init) or ~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.config.apiKey. Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
188
188
|
process.stderr.write(`${errorMsg}
|
|
189
189
|
`);
|
|
190
190
|
}
|
|
@@ -189,7 +189,7 @@ function handleHttpError(status, logger, retryDelaySeconds) {
|
|
|
189
189
|
if (status === 401 || status === 403) {
|
|
190
190
|
if (!authErrorLogged) {
|
|
191
191
|
authErrorLogged = true;
|
|
192
|
-
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in
|
|
192
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in ~/.multicorn/config.json (from npx multicorn-shield init) or ~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.config.apiKey. Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
193
193
|
logger?.error(errorMsg);
|
|
194
194
|
process.stderr.write(`${errorMsg}
|
|
195
195
|
`);
|
|
@@ -491,20 +491,41 @@ function agentNameFromOpenclawPlatform(cfg) {
|
|
|
491
491
|
}
|
|
492
492
|
return void 0;
|
|
493
493
|
}
|
|
494
|
+
var ENV_REF_PATTERN = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
|
|
495
|
+
function resolveConfigString(value) {
|
|
496
|
+
const raw = asString(value);
|
|
497
|
+
if (raw === void 0) return void 0;
|
|
498
|
+
const match = ENV_REF_PATTERN.exec(raw.trim());
|
|
499
|
+
if (match === null) return raw;
|
|
500
|
+
const envName = match[1];
|
|
501
|
+
if (envName === void 0) return void 0;
|
|
502
|
+
const fromEnv = process.env[envName];
|
|
503
|
+
return typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : void 0;
|
|
504
|
+
}
|
|
505
|
+
function parseFailMode(value) {
|
|
506
|
+
if (value === "open") return "open";
|
|
507
|
+
if (value === "closed") return "closed";
|
|
508
|
+
return "closed";
|
|
509
|
+
}
|
|
494
510
|
function readConfig() {
|
|
495
511
|
const pc = pluginConfig ?? {};
|
|
496
|
-
const
|
|
497
|
-
const
|
|
512
|
+
const fromFileKey = asString(cachedMulticornConfig?.apiKey);
|
|
513
|
+
const fromPluginKey = resolveConfigString(pc["apiKey"]);
|
|
514
|
+
const fromEnvKey = asString(process.env["MULTICORN_API_KEY"]);
|
|
515
|
+
let apiKey = fromFileKey ?? fromPluginKey ?? fromEnvKey ?? "";
|
|
516
|
+
const fromPluginBase = resolveConfigString(pc["baseUrl"]);
|
|
517
|
+
const fromFileBase = asString(cachedMulticornConfig?.baseUrl);
|
|
518
|
+
const fromEnvBase = asString(process.env["MULTICORN_BASE_URL"]);
|
|
519
|
+
const baseUrl = fromPluginBase ?? fromFileBase ?? fromEnvBase ?? "https://api.multicorn.ai";
|
|
498
520
|
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? agentNameFromOpenclawPlatform(cachedMulticornConfig) ?? asString(cachedMulticornConfig?.agentName) ?? null;
|
|
499
|
-
const failMode = "
|
|
500
|
-
let apiKey = resolvedApiKey;
|
|
521
|
+
const failMode = parseFailMode(pc["failMode"]);
|
|
501
522
|
if (apiKey.length > 0 && (!apiKey.startsWith("mcs_") || apiKey.length < 16)) {
|
|
502
523
|
pluginLogger?.error(
|
|
503
524
|
"Invalid API key format. Key must start with mcs_ and be at least 16 characters."
|
|
504
525
|
);
|
|
505
526
|
apiKey = "";
|
|
506
527
|
}
|
|
507
|
-
return { apiKey, baseUrl
|
|
528
|
+
return { apiKey, baseUrl, agentName, failMode };
|
|
508
529
|
}
|
|
509
530
|
function asString(value) {
|
|
510
531
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -919,4 +940,4 @@ function resetState() {
|
|
|
919
940
|
hasLoggedFirstAction = false;
|
|
920
941
|
}
|
|
921
942
|
|
|
922
|
-
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
|
|
943
|
+
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName, resolveConfigString };
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
3
|
+
import { randomUUID, createHash } from 'crypto';
|
|
4
4
|
import { isIP } from 'net';
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
@@ -1275,7 +1275,8 @@ var AGGREGATOR_UPSTREAM_TIMEOUT_MS = UPSTREAM_TIMEOUT_MS;
|
|
|
1275
1275
|
// ../multicorn-proxy/src/multicorn-mcp.ts
|
|
1276
1276
|
var MULTICORN_MCP_SENTINEL2 = "multicorn://mcp";
|
|
1277
1277
|
var GOOGLE_API_TIMEOUT_MS = 15e3;
|
|
1278
|
-
var
|
|
1278
|
+
var GOOGLE_NOT_CONNECTED = "Google is not connected. Open your Shield dashboard and connect your Google account to use this tool.";
|
|
1279
|
+
var SLACK_NOT_AVAILABLE = "Slack is not available yet. Multicorn does not support connecting a Slack account, so this tool cannot be used.";
|
|
1279
1280
|
var TOOLS = [
|
|
1280
1281
|
{
|
|
1281
1282
|
name: "gmail_read_inbox",
|
|
@@ -1516,9 +1517,9 @@ async function executeToolCall(toolName, args, ctx) {
|
|
|
1516
1517
|
case "delete_file":
|
|
1517
1518
|
return workspaceDelete(args, ctx);
|
|
1518
1519
|
case "slack_read_messages":
|
|
1519
|
-
return slackReadMessages(
|
|
1520
|
+
return slackReadMessages();
|
|
1520
1521
|
case "slack_send_message":
|
|
1521
|
-
return slackSendMessage(
|
|
1522
|
+
return slackSendMessage();
|
|
1522
1523
|
default:
|
|
1523
1524
|
return Promise.resolve(null);
|
|
1524
1525
|
}
|
|
@@ -1588,7 +1589,7 @@ async function gmailReadInbox(args, ctx) {
|
|
|
1588
1589
|
const query = typeof args["query"] === "string" ? args["query"] : "";
|
|
1589
1590
|
const limit = typeof args["limit"] === "number" && args["limit"] > 0 ? Math.floor(args["limit"]) : 5;
|
|
1590
1591
|
if (ctx.google === void 0) {
|
|
1591
|
-
return
|
|
1592
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1592
1593
|
}
|
|
1593
1594
|
try {
|
|
1594
1595
|
const listUrl = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
|
|
@@ -1639,7 +1640,7 @@ async function gmailSendEmail(args, ctx) {
|
|
|
1639
1640
|
const subject = typeof args["subject"] === "string" ? args["subject"] : "";
|
|
1640
1641
|
const body = typeof args["body"] === "string" ? args["body"] : "";
|
|
1641
1642
|
if (ctx.google === void 0) {
|
|
1642
|
-
return
|
|
1643
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1643
1644
|
}
|
|
1644
1645
|
try {
|
|
1645
1646
|
const raw = buildRawEmail(to, subject, body);
|
|
@@ -1665,7 +1666,7 @@ async function gmailDeleteThread(args, ctx) {
|
|
|
1665
1666
|
return errorResult("A thread_id is required to delete a thread.");
|
|
1666
1667
|
}
|
|
1667
1668
|
if (ctx.google === void 0) {
|
|
1668
|
-
return
|
|
1669
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1669
1670
|
}
|
|
1670
1671
|
try {
|
|
1671
1672
|
const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${encodeURIComponent(threadId)}/trash`;
|
|
@@ -1681,7 +1682,7 @@ async function gmailDeleteEmail(args, ctx) {
|
|
|
1681
1682
|
return errorResult("A message_id is required to delete an email.");
|
|
1682
1683
|
}
|
|
1683
1684
|
if (ctx.google === void 0) {
|
|
1684
|
-
return
|
|
1685
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1685
1686
|
}
|
|
1686
1687
|
try {
|
|
1687
1688
|
const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}/trash`;
|
|
@@ -1732,7 +1733,7 @@ async function fetchEventsForCalendar(calendarId, start, end, timeZone, auth) {
|
|
|
1732
1733
|
async function calendarListEvents(args, ctx) {
|
|
1733
1734
|
const dateArg = typeof args["date"] === "string" ? args["date"] : void 0;
|
|
1734
1735
|
if (ctx.google === void 0) {
|
|
1735
|
-
return
|
|
1736
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1736
1737
|
}
|
|
1737
1738
|
try {
|
|
1738
1739
|
let timeZone;
|
|
@@ -1845,7 +1846,7 @@ async function calendarCreateEvent(args, ctx) {
|
|
|
1845
1846
|
const durationMinutes = typeof args["duration_minutes"] === "number" && args["duration_minutes"] > 0 ? Math.floor(args["duration_minutes"]) : 60;
|
|
1846
1847
|
const location = typeof args["location"] === "string" ? args["location"] : void 0;
|
|
1847
1848
|
if (ctx.google === void 0) {
|
|
1848
|
-
return
|
|
1849
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1849
1850
|
}
|
|
1850
1851
|
try {
|
|
1851
1852
|
const startDate = parseEventStart(date);
|
|
@@ -1888,7 +1889,7 @@ async function calendarUpdateEvent(args, ctx) {
|
|
|
1888
1889
|
const durationMinutes = typeof args["duration_minutes"] === "number" && args["duration_minutes"] > 0 ? Math.floor(args["duration_minutes"]) : void 0;
|
|
1889
1890
|
const location = typeof args["location"] === "string" ? args["location"] : void 0;
|
|
1890
1891
|
if (ctx.google === void 0) {
|
|
1891
|
-
return
|
|
1892
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1892
1893
|
}
|
|
1893
1894
|
try {
|
|
1894
1895
|
const eventBody = {};
|
|
@@ -1921,7 +1922,7 @@ async function calendarDeleteEvent(args, ctx) {
|
|
|
1921
1922
|
return errorResult("An event_id is required to delete an event.");
|
|
1922
1923
|
}
|
|
1923
1924
|
if (ctx.google === void 0) {
|
|
1924
|
-
return
|
|
1925
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1925
1926
|
}
|
|
1926
1927
|
try {
|
|
1927
1928
|
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${encodeURIComponent(eventId)}`;
|
|
@@ -1934,7 +1935,7 @@ async function calendarDeleteEvent(args, ctx) {
|
|
|
1934
1935
|
async function driveSearchFiles(args, ctx) {
|
|
1935
1936
|
const query = typeof args["query"] === "string" ? args["query"] : "";
|
|
1936
1937
|
if (ctx.google === void 0) {
|
|
1937
|
-
return
|
|
1938
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1938
1939
|
}
|
|
1939
1940
|
try {
|
|
1940
1941
|
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
|
@@ -1963,7 +1964,7 @@ async function driveWriteFile(args, ctx) {
|
|
|
1963
1964
|
const name = typeof args["name"] === "string" ? args["name"] : "";
|
|
1964
1965
|
const content = typeof args["content"] === "string" ? args["content"] : "";
|
|
1965
1966
|
if (ctx.google === void 0) {
|
|
1966
|
-
return
|
|
1967
|
+
return errorResult(GOOGLE_NOT_CONNECTED);
|
|
1967
1968
|
}
|
|
1968
1969
|
try {
|
|
1969
1970
|
const boundary = `multicorn-${Math.random().toString(36).slice(2)}`;
|
|
@@ -1987,86 +1988,11 @@ ${content}\r
|
|
|
1987
1988
|
return googleErrorResult(err);
|
|
1988
1989
|
}
|
|
1989
1990
|
}
|
|
1990
|
-
function slackReadMessages(
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
];
|
|
1996
|
-
const channel = typeof args["channel"] === "string" ? args["channel"].toLowerCase().replace(/^#/, "") : "";
|
|
1997
|
-
let filtered = messages;
|
|
1998
|
-
if (channel.length > 0) {
|
|
1999
|
-
filtered = messages.filter((m) => m.channel === channel);
|
|
2000
|
-
}
|
|
2001
|
-
const text = filtered.length === 0 ? `No recent messages in #${channel}.` : filtered.map((m) => `#${m.channel} \u2014 ${m.author}: "${m.text}"`).join("\n");
|
|
2002
|
-
return Promise.resolve(textResult(text));
|
|
2003
|
-
}
|
|
2004
|
-
function slackSendMessage(args) {
|
|
2005
|
-
const channel = typeof args["channel"] === "string" ? args["channel"].replace(/^#/, "") : "";
|
|
2006
|
-
return Promise.resolve(textResult(`Message sent to #${channel}`));
|
|
2007
|
-
}
|
|
2008
|
-
function sampleGmailReadInbox(query, limit) {
|
|
2009
|
-
const emails = [
|
|
2010
|
-
{
|
|
2011
|
-
from: "Alex Chen <alex@acme.co>",
|
|
2012
|
-
subject: "Q3 planning doc ready for review",
|
|
2013
|
-
date: "yesterday",
|
|
2014
|
-
threadId: "thread_sample_q3",
|
|
2015
|
-
preview: "Hey team, I've finished the Q3 planning document..."
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
from: "Jordan Kim <jordan@acme.co>",
|
|
2019
|
-
subject: "Design review at 2pm",
|
|
2020
|
-
date: "today",
|
|
2021
|
-
threadId: "thread_sample_design",
|
|
2022
|
-
preview: "Quick reminder about the design review..."
|
|
2023
|
-
},
|
|
2024
|
-
{
|
|
2025
|
-
from: "Acme Weekly <newsletter@acme.co>",
|
|
2026
|
-
subject: "This week at Acme",
|
|
2027
|
-
date: "2 days ago",
|
|
2028
|
-
threadId: "thread_sample_weekly",
|
|
2029
|
-
preview: "Product launch update, new hires, and Friday social..."
|
|
2030
|
-
}
|
|
2031
|
-
];
|
|
2032
|
-
const q = query.toLowerCase();
|
|
2033
|
-
let filtered = emails;
|
|
2034
|
-
if (q.length > 0) {
|
|
2035
|
-
filtered = emails.filter(
|
|
2036
|
-
(e) => e.subject.toLowerCase().includes(q) || e.from.toLowerCase().includes(q) || e.preview.toLowerCase().includes(q)
|
|
2037
|
-
);
|
|
2038
|
-
}
|
|
2039
|
-
filtered = filtered.slice(0, limit);
|
|
2040
|
-
return filtered.length === 0 ? "No emails found matching your query." : filtered.map(
|
|
2041
|
-
(e) => `From: ${e.from}
|
|
2042
|
-
Subject: ${e.subject}
|
|
2043
|
-
Date: ${e.date}
|
|
2044
|
-
Thread ID: ${e.threadId}
|
|
2045
|
-
Preview: ${e.preview}`
|
|
2046
|
-
).join("\n\n");
|
|
2047
|
-
}
|
|
2048
|
-
function sampleCalendarListEvents() {
|
|
2049
|
-
return [
|
|
2050
|
-
"9:00 AM \u2014 Daily standup (15 min, recurring) [Event ID: evt_sample_standup]",
|
|
2051
|
-
"11:00 AM \u2014 1:1 with Alex Chen (30 min) [Event ID: evt_sample_1on1]",
|
|
2052
|
-
"12:30 PM \u2014 Team lunch at Sushi Place (60 min) [Event ID: evt_sample_lunch]",
|
|
2053
|
-
"3:00 PM \u2014 Sprint demo (45 min, Friday) [Event ID: evt_sample_demo]"
|
|
2054
|
-
].join("\n");
|
|
2055
|
-
}
|
|
2056
|
-
function sampleDriveSearchFiles(query) {
|
|
2057
|
-
const files = [
|
|
2058
|
-
{ name: "Q3 Planning.docx", modified: "Modified yesterday", shared: "shared with team" },
|
|
2059
|
-
{ name: "Budget 2026.xlsx", modified: "Modified 3 days ago", shared: "shared with finance" },
|
|
2060
|
-
{ name: "Meeting Notes - Sprint Review.md", modified: "Modified today", shared: "" },
|
|
2061
|
-
{ name: "Product Roadmap.pdf", modified: "Modified last week", shared: "shared with leadership" },
|
|
2062
|
-
{ name: "Onboarding Checklist.docx", modified: "Modified 2 weeks ago", shared: "" }
|
|
2063
|
-
];
|
|
2064
|
-
const q = query.toLowerCase();
|
|
2065
|
-
let filtered = files;
|
|
2066
|
-
if (q.length > 0) {
|
|
2067
|
-
filtered = files.filter((f) => f.name.toLowerCase().includes(q));
|
|
2068
|
-
}
|
|
2069
|
-
return filtered.length === 0 ? "No files found matching your query." : filtered.map((f) => `${f.name} \u2014 ${f.modified}${f.shared ? `, ${f.shared}` : ""}`).join("\n");
|
|
1991
|
+
function slackReadMessages() {
|
|
1992
|
+
return errorResult(SLACK_NOT_AVAILABLE);
|
|
1993
|
+
}
|
|
1994
|
+
function slackSendMessage() {
|
|
1995
|
+
return errorResult(SLACK_NOT_AVAILABLE);
|
|
2070
1996
|
}
|
|
2071
1997
|
var WORKSPACE_TIMEOUT_MS = 1e4;
|
|
2072
1998
|
var WORKSPACE_AUTH_HEADER = "X-Multicorn-Key";
|
|
@@ -3331,6 +3257,21 @@ data: ${JSON.stringify({ error: message })}
|
|
|
3331
3257
|
}
|
|
3332
3258
|
};
|
|
3333
3259
|
}
|
|
3260
|
+
function buildUnauthenticatedRejectLogContext(input) {
|
|
3261
|
+
const requestIdHeader = input.req.headers["x-request-id"];
|
|
3262
|
+
const requestId = typeof requestIdHeader === "string" && requestIdHeader.length > 0 ? requestIdHeader : randomUUID();
|
|
3263
|
+
const contentLengthHeader = input.req.headers["content-length"];
|
|
3264
|
+
const headerContentLength = typeof contentLengthHeader === "string" ? Number.parseInt(contentLengthHeader, 10) : Number.NaN;
|
|
3265
|
+
const contentLength = Number.isFinite(headerContentLength) ? headerContentLength : input.bodyByteLength;
|
|
3266
|
+
return {
|
|
3267
|
+
method: input.req.method ?? "UNKNOWN",
|
|
3268
|
+
path: input.path,
|
|
3269
|
+
status: 401,
|
|
3270
|
+
contentLength,
|
|
3271
|
+
requestId,
|
|
3272
|
+
parsedMethod: input.parsedMethod
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3334
3275
|
|
|
3335
3276
|
// ../multicorn-proxy/src/server.ts
|
|
3336
3277
|
var ROUTE_RE = /^\/r\/([^/]+)\/([^/]+)(.*)$/;
|
|
@@ -3424,11 +3365,15 @@ function main() {
|
|
|
3424
3365
|
const body = await bufferFromRequest(req, MCP_PROXY_MAX_BODY_BYTES);
|
|
3425
3366
|
const bodyText = body.toString("utf8");
|
|
3426
3367
|
const rpc = body.length > 0 ? parseJsonRpcLine(bodyText) : null;
|
|
3427
|
-
logger.info(
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3368
|
+
logger.info(
|
|
3369
|
+
"Unauthenticated request rejected",
|
|
3370
|
+
buildUnauthenticatedRejectLogContext({
|
|
3371
|
+
req,
|
|
3372
|
+
path: redactedUrl,
|
|
3373
|
+
bodyByteLength: body.length,
|
|
3374
|
+
parsedMethod: rpc?.method ?? null
|
|
3375
|
+
})
|
|
3376
|
+
);
|
|
3432
3377
|
if (body.length > 0) {
|
|
3433
3378
|
const handshakeKind = classifyUnauthenticatedMcpHandshake(rpc);
|
|
3434
3379
|
if (handshakeKind === "initialize" && rpc !== null) {
|
package/dist/shield-extension.js
CHANGED
|
@@ -22405,18 +22405,9 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
|
22405
22405
|
},
|
|
22406
22406
|
{ slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
|
|
22407
22407
|
];
|
|
22408
|
-
(() =>
|
|
22409
|
-
const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
|
|
22410
|
-
platform: e.slug,
|
|
22411
|
-
label: e.displayName
|
|
22412
|
-
}));
|
|
22413
|
-
return [
|
|
22414
|
-
{ title: "Recommended (native plugin)", items: itemsFor("native") },
|
|
22415
|
-
{ title: "Hosted proxy (MCP only)", items: itemsFor("hosted") }
|
|
22416
|
-
];
|
|
22417
|
-
})();
|
|
22408
|
+
var INIT_WIZARD_PICKER_NATIVE_SLUGS = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === "native").map((e) => e.slug);
|
|
22418
22409
|
Object.fromEntries(
|
|
22419
|
-
|
|
22410
|
+
INIT_WIZARD_PICKER_NATIVE_SLUGS.map((slug, i) => [i + 1, slug])
|
|
22420
22411
|
);
|
|
22421
22412
|
|
|
22422
22413
|
// src/extension/config-reader.ts
|
|
@@ -22518,7 +22509,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22518
22509
|
|
|
22519
22510
|
// package.json
|
|
22520
22511
|
var package_default = {
|
|
22521
|
-
version: "1.
|
|
22512
|
+
version: "1.12.1"};
|
|
22522
22513
|
|
|
22523
22514
|
// src/package-meta.ts
|
|
22524
22515
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Multicorn AI Pty Ltd",
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"prepare": "husky || true",
|
|
80
80
|
"release:patch": "npm version patch && pnpm publish",
|
|
81
81
|
"release:minor": "npm version minor && pnpm publish",
|
|
82
|
-
"release:major": "npm version major && pnpm publish"
|
|
82
|
+
"release:major": "npm version major && pnpm publish",
|
|
83
|
+
"local": "pnpm build &&node dist/multicorn-shield.js init --base-url http://localhost:8080"
|
|
83
84
|
},
|
|
84
85
|
"lint-staged": {
|
|
85
86
|
"*.ts": [
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
"@size-limit/file": "^11.1.6",
|
|
108
109
|
"@types/node": "^22.0.0",
|
|
109
110
|
"@vitest/coverage-v8": "^3.0.5",
|
|
111
|
+
"ajv": "^8.18.0",
|
|
110
112
|
"eslint": "^9.19.0",
|
|
111
113
|
"eslint-config-prettier": "^10.0.1",
|
|
112
114
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
@@ -42,6 +42,46 @@ var path__namespace = /*#__PURE__*/ _interopNamespace(path);
|
|
|
42
42
|
|
|
43
43
|
var AUTH_HEADER = "X-Multicorn-Key";
|
|
44
44
|
var AUDIT_METADATA_MAX_CHARS = 1e4;
|
|
45
|
+
var GOOGLE_USER_DATA_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
46
|
+
"to",
|
|
47
|
+
"subject",
|
|
48
|
+
"body",
|
|
49
|
+
"query",
|
|
50
|
+
"name",
|
|
51
|
+
"content",
|
|
52
|
+
"title",
|
|
53
|
+
"description",
|
|
54
|
+
"location",
|
|
55
|
+
"attendees",
|
|
56
|
+
"summary",
|
|
57
|
+
]);
|
|
58
|
+
function redactGoogleUserDataValue(value) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.map((item) => redactGoogleUserDataValue(item));
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "object" && value !== null) {
|
|
63
|
+
const record = value;
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [key, val] of Object.entries(record)) {
|
|
66
|
+
out[key] = GOOGLE_USER_DATA_FIELD_NAMES.has(key.toLowerCase())
|
|
67
|
+
? "[REDACTED]"
|
|
68
|
+
: redactGoogleUserDataValue(val);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function redactGoogleUserDataInText(text) {
|
|
75
|
+
let out = text;
|
|
76
|
+
out = out.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED]");
|
|
77
|
+
out = out.replace(
|
|
78
|
+
/(\b(?:to|from|subject|body|query|preview|attendees):\s*)([^\n]+)/gi,
|
|
79
|
+
"$1[REDACTED]",
|
|
80
|
+
);
|
|
81
|
+
out = out.replace(/\bwith subject\s+'[^']*'/gi, "with subject '[REDACTED]'");
|
|
82
|
+
out = out.replace(/\bwith subject\s+"[^"]*"/gi, 'with subject "[REDACTED]"');
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
45
85
|
function redactSecretsForAudit(serialized) {
|
|
46
86
|
let out = serialized;
|
|
47
87
|
out = out.replace(/\bsk-[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
|
|
@@ -64,7 +104,14 @@ function truncateForAudit(serialized, maxChars = AUDIT_METADATA_MAX_CHARS) {
|
|
|
64
104
|
}
|
|
65
105
|
function serializeHookAuditFragment(value) {
|
|
66
106
|
try {
|
|
67
|
-
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
return truncateForAudit(redactGoogleUserDataInText(redactSecretsForAudit(value)));
|
|
109
|
+
}
|
|
110
|
+
const redacted = redactGoogleUserDataValue(value);
|
|
111
|
+
const raw =
|
|
112
|
+
typeof redacted === "string"
|
|
113
|
+
? redacted
|
|
114
|
+
: JSON.stringify(redacted === void 0 ? null : redacted);
|
|
68
115
|
return truncateForAudit(redactSecretsForAudit(raw));
|
|
69
116
|
} catch {
|
|
70
117
|
return "[unserializable]";
|
|
@@ -263,6 +310,8 @@ exports.formatShieldNetworkError = formatShieldNetworkError;
|
|
|
263
310
|
exports.isLocalHostname = isLocalHostname;
|
|
264
311
|
exports.loadCodexCliConfig = loadCodexCliConfig;
|
|
265
312
|
exports.readHttpApiKeyRefusalHostname = readHttpApiKeyRefusalHostname;
|
|
313
|
+
exports.redactGoogleUserDataInText = redactGoogleUserDataInText;
|
|
314
|
+
exports.redactGoogleUserDataValue = redactGoogleUserDataValue;
|
|
266
315
|
exports.redactSecretsForAudit = redactSecretsForAudit;
|
|
267
316
|
exports.resolveCodexCliAgentName = resolveCodexCliAgentName;
|
|
268
317
|
exports.serializeHookAuditFragment = serializeHookAuditFragment;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildOpenCodeParametersMetadata,
|
|
5
|
+
buildOpenCodeResultMetadata,
|
|
6
|
+
} from "./multicorn-shield.js";
|
|
7
|
+
|
|
8
|
+
describe("OpenCode audit metadata", () => {
|
|
9
|
+
it("does not persist gmail_send_email recipient, subject or body in parameters", () => {
|
|
10
|
+
const metadata = buildOpenCodeParametersMetadata({
|
|
11
|
+
to: "alice@example.com",
|
|
12
|
+
subject: "Project update",
|
|
13
|
+
body: "Here are the confidential numbers for Q3.",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(metadata).not.toContain("alice@example.com");
|
|
17
|
+
expect(metadata).not.toContain("Project update");
|
|
18
|
+
expect(metadata).not.toContain("confidential numbers");
|
|
19
|
+
expect(metadata).toContain("[REDACTED]");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("does not persist gmail result text with addresses or subjects", () => {
|
|
23
|
+
const metadata = buildOpenCodeResultMetadata(
|
|
24
|
+
"Email sent to alice@example.com with subject 'Project update'.",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(metadata).not.toContain("alice@example.com");
|
|
28
|
+
expect(metadata).not.toContain("Project update");
|
|
29
|
+
expect(metadata).toContain("[REDACTED]");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -11,6 +11,8 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
|
|
14
|
+
import { serializeHookAuditFragment } from "../../src/hooks/codex-cli-hooks-shared.js";
|
|
15
|
+
|
|
14
16
|
const MULTICORN_CONFIG = path.join(homedir(), ".multicorn", "config.json");
|
|
15
17
|
const HTTP_MS = 10_000;
|
|
16
18
|
const PLATFORM = "opencode";
|
|
@@ -230,37 +232,14 @@ function blockedMessage(
|
|
|
230
232
|
return `Shield: Action blocked. Required permission: ${service} (${actionType}). Grant access at ${approvalsUrl}`;
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const clone = { ...(args as Record<string, unknown>) };
|
|
237
|
-
const contentKey = clone["content"];
|
|
238
|
-
if (typeof contentKey === "string") {
|
|
239
|
-
clone["content"] = "[" + contentKey.length.toString() + " chars redacted]";
|
|
240
|
-
}
|
|
241
|
-
const cmd = clone["command"];
|
|
242
|
-
if (typeof cmd === "string" && cmd.length > 200) {
|
|
243
|
-
clone["command"] = cmd.slice(0, 200) + "... [truncated]";
|
|
244
|
-
}
|
|
245
|
-
let out = JSON.stringify(clone);
|
|
246
|
-
if (out.length > 4096) out = out.slice(0, 4096);
|
|
247
|
-
return out;
|
|
248
|
-
} catch {
|
|
249
|
-
return "{}";
|
|
250
|
-
}
|
|
235
|
+
/** Builds redacted parameters metadata for OpenCode audit rows. */
|
|
236
|
+
export function buildOpenCodeParametersMetadata(args: unknown): string {
|
|
237
|
+
return serializeHookAuditFragment(args);
|
|
251
238
|
}
|
|
252
239
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
s = s.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
|
|
257
|
-
s = s.replace(/\bmcs_[A-Za-z0-9_-]+\b/g, "[REDACTED]");
|
|
258
|
-
s = s.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]");
|
|
259
|
-
s = s.replace(/Bearer\s+[^\s]+/gi, "[REDACTED]");
|
|
260
|
-
if (s.length > 500) {
|
|
261
|
-
return s.slice(0, 500) + "[truncated]";
|
|
262
|
-
}
|
|
263
|
-
return s;
|
|
240
|
+
/** Builds redacted result metadata for OpenCode audit rows. */
|
|
241
|
+
export function buildOpenCodeResultMetadata(output: unknown): string {
|
|
242
|
+
return serializeHookAuditFragment(output);
|
|
264
243
|
}
|
|
265
244
|
|
|
266
245
|
async function shieldPostActions(
|
|
@@ -327,7 +306,7 @@ async function shieldBeforeDecision(
|
|
|
327
306
|
/** @type {Record<string, unknown>} */
|
|
328
307
|
const metadata = {
|
|
329
308
|
tool_name: toolName,
|
|
330
|
-
parameters:
|
|
309
|
+
parameters: buildOpenCodeParametersMetadata(args),
|
|
331
310
|
source: PLATFORM,
|
|
332
311
|
};
|
|
333
312
|
|
|
@@ -474,7 +453,7 @@ export const MulticornShieldPlugin: Plugin = (input: PluginInput): Promise<Hooks
|
|
|
474
453
|
if (typeof output === "object" && "output" in output) {
|
|
475
454
|
const rawOut = (output as Record<string, unknown>)["output"];
|
|
476
455
|
if (typeof rawOut === "string") {
|
|
477
|
-
snippet =
|
|
456
|
+
snippet = buildOpenCodeResultMetadata(rawOut);
|
|
478
457
|
}
|
|
479
458
|
}
|
|
480
459
|
|