multicorn-shield 1.12.0 → 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 +10 -0
- package/dist/multicorn-proxy.js +1 -1
- package/dist/multicorn-shield.js +1 -1
- package/dist/server.js +44 -99
- package/dist/shield-extension.js +1 -1
- package/package.json +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.12.1] - 2026-07-13
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Regenerated the vendored local proxy bundle (`vendor/local-proxy-server.js`). Built-in MCP Google tools no longer return fabricated emails, events, or files, or fake write confirmations when Google is not connected; they return a clear "Google is not connected" error instead. Slack tools return a "not available yet" error rather than sample content.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- OpenCode plugin and Codex CLI hooks now redact Google user-data fields (recipient, subject, body, query, and similar) from tool arguments and output before storing them in audit metadata. Regenerated `plugins/codex-cli/hooks/scripts/codex-cli-hooks-shared.cjs` so Codex CLI installs pick up the same redaction as the TypeScript source.
|
|
23
|
+
|
|
14
24
|
## [1.12.0] - 2026-07-06
|
|
15
25
|
|
|
16
26
|
### Fixed
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -5050,7 +5050,7 @@ var init_package = __esm({
|
|
|
5050
5050
|
"package.json"() {
|
|
5051
5051
|
package_default = {
|
|
5052
5052
|
name: "multicorn-shield",
|
|
5053
|
-
version: "1.12.
|
|
5053
|
+
version: "1.12.1",
|
|
5054
5054
|
description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5055
5055
|
license: "MIT",
|
|
5056
5056
|
author: "Multicorn AI Pty Ltd",
|
package/dist/multicorn-shield.js
CHANGED
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
|
@@ -22509,7 +22509,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22509
22509
|
|
|
22510
22510
|
// package.json
|
|
22511
22511
|
var package_default = {
|
|
22512
|
-
version: "1.12.
|
|
22512
|
+
version: "1.12.1"};
|
|
22513
22513
|
|
|
22514
22514
|
// src/package-meta.ts
|
|
22515
22515
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -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
|
|