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 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
@@ -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.0",
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",
@@ -4973,7 +4973,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
4973
4973
 
4974
4974
  // package.json
4975
4975
  var package_default = {
4976
- version: "1.12.0"};
4976
+ version: "1.12.1"};
4977
4977
 
4978
4978
  // src/package-meta.ts
4979
4979
  var PACKAGE_VERSION = package_default.version;
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 SAMPLE_NOTE = "\n\nThis is sample data. Connect your Google account in the Shield dashboard to see your real emails, events, and files.";
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(args);
1520
+ return slackReadMessages();
1520
1521
  case "slack_send_message":
1521
- return slackSendMessage(args);
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 textResult(sampleGmailReadInbox(query, limit) + SAMPLE_NOTE);
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 textResult(`Email sent to ${to} with subject '${subject}'` + SAMPLE_NOTE);
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 textResult(`Thread '${threadId}' moved to trash` + SAMPLE_NOTE);
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 textResult(`Email '${messageId}' moved to Trash` + SAMPLE_NOTE);
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 textResult(sampleCalendarListEvents() + SAMPLE_NOTE);
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 textResult(`Event '${title}' created for ${date}` + SAMPLE_NOTE);
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 textResult(`Event '${eventId}' updated` + SAMPLE_NOTE);
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 textResult(`Event '${eventId}' deleted` + SAMPLE_NOTE);
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 textResult(sampleDriveSearchFiles(query) + SAMPLE_NOTE);
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 textResult(`File '${name}' saved to Drive` + SAMPLE_NOTE);
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(args) {
1991
- const messages = [
1992
- { channel: "general", author: "Sam Patel", text: "Launch is confirmed for next Tuesday. Marketing assets are ready." },
1993
- { channel: "engineering", author: "Alex Chen", text: "PR #247 merged. Deployment starts at 4pm." },
1994
- { channel: "random", author: "Jordan Kim", text: "Friday social is at the rooftop bar this week!" }
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("[DEBUG] Unauthenticated POST body", {
3428
- url: redactedUrl,
3429
- bodyPreview: bodyText.slice(0, 500),
3430
- parsedMethod: rpc?.method ?? null
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) {
@@ -22509,7 +22509,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22509
22509
 
22510
22510
  // package.json
22511
22511
  var package_default = {
22512
- version: "1.12.0"};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.12.0",
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",
@@ -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
- const raw = typeof value === "string" ? value : JSON.stringify(value === void 0 ? null : value);
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
- function scrubMetadataArgs(args: unknown): string {
234
- try {
235
- if (typeof args !== "object" || args === null) return "{}";
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
- function scrubResultSnippet(text: unknown): string {
254
- if (typeof text !== "string") return "";
255
- let s = text;
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: scrubMetadataArgs(args),
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 = scrubResultSnippet(rawOut);
456
+ snippet = buildOpenCodeResultMetadata(rawOut);
478
457
  }
479
458
  }
480
459