multicorn-shield 1.9.0 → 1.9.2

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
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - Bump `version` in `package.json` before publishing to npm.
11
11
 
12
+ ## [1.9.2] - 2026-05-12
13
+
14
+ ### Fixed
15
+
16
+ - Codex CLI hosted proxy next-steps now includes /mcp verification step
17
+
18
+ ## [1.9.1] - 2026-05-12
19
+
20
+ ### Fixed
21
+
22
+ - Include `plugins/codex-cli` in published npm package (missing from `files` in package.json)
23
+
12
24
  ## [1.9.0] - 2026-05-12
13
25
 
14
26
  ### Added
@@ -3241,8 +3241,8 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3241
3241
  );
3242
3242
  process.stderr.write(
3243
3243
  "\n" + style.dim(
3244
- "Add the TOML snippet above to ~/.codex/config.toml. Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
3245
- ) + "\n"
3244
+ "Add the TOML snippet above to ~/.codex/config.toml. Then set the environment variable:"
3245
+ ) + "\n\n " + style.cyan(`export MULTICORN_API_KEY="${apiKey}"`) + "\n\n" + style.dim("Restart Codex CLI after saving config.toml.") + "\n"
3246
3246
  );
3247
3247
  configuredAgents.push({
3248
3248
  selection,
@@ -3581,7 +3581,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3581
3581
  }
3582
3582
  if (codexHostedConfigured) {
3583
3583
  blocks.push(
3584
- "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3584
+ "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3585
3585
  );
3586
3586
  }
3587
3587
  if (configuredPlatforms.has("other-mcp")) {
@@ -4660,7 +4660,7 @@ var init_package = __esm({
4660
4660
  "package.json"() {
4661
4661
  package_default = {
4662
4662
  name: "multicorn-shield",
4663
- version: "1.9.0",
4663
+ version: "1.9.2",
4664
4664
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
4665
4665
  license: "MIT",
4666
4666
  author: "Multicorn AI Pty Ltd",
@@ -4701,6 +4701,7 @@ var init_package = __esm({
4701
4701
  "plugins/cline",
4702
4702
  "plugins/gemini-cli",
4703
4703
  "plugins/opencode",
4704
+ "plugins/codex-cli",
4704
4705
  "LICENSE",
4705
4706
  "README.md",
4706
4707
  "CHANGELOG.md"
@@ -3335,8 +3335,8 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3335
3335
  );
3336
3336
  process.stderr.write(
3337
3337
  "\n" + style.dim(
3338
- "Add the TOML snippet above to ~/.codex/config.toml. Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
3339
- ) + "\n"
3338
+ "Add the TOML snippet above to ~/.codex/config.toml. Then set the environment variable:"
3339
+ ) + "\n\n " + style.cyan(`export MULTICORN_API_KEY="${apiKey}"`) + "\n\n" + style.dim("Restart Codex CLI after saving config.toml.") + "\n"
3340
3340
  );
3341
3341
  configuredAgents.push({
3342
3342
  selection,
@@ -3675,7 +3675,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3675
3675
  }
3676
3676
  if (codexHostedConfigured) {
3677
3677
  blocks.push(
3678
- "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3678
+ "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3679
3679
  );
3680
3680
  }
3681
3681
  if (configuredPlatforms.has("other-mcp")) {
@@ -4583,7 +4583,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
4583
4583
 
4584
4584
  // package.json
4585
4585
  var package_default = {
4586
- version: "1.9.0"};
4586
+ version: "1.9.2"};
4587
4587
 
4588
4588
  // src/package-meta.ts
4589
4589
  var PACKAGE_VERSION = package_default.version;
@@ -22517,7 +22517,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22517
22517
 
22518
22518
  // package.json
22519
22519
  var package_default = {
22520
- version: "1.9.0"};
22520
+ version: "1.9.2"};
22521
22521
 
22522
22522
  // src/package-meta.ts
22523
22523
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
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",
@@ -41,6 +41,7 @@
41
41
  "plugins/cline",
42
42
  "plugins/gemini-cli",
43
43
  "plugins/opencode",
44
+ "plugins/codex-cli",
44
45
  "LICENSE",
45
46
  "README.md",
46
47
  "CHANGELOG.md"
@@ -0,0 +1,41 @@
1
+ # Codex CLI hook scripts for Multicorn Shield
2
+
3
+ These files support **Codex CLI** native hooks (`PreToolUse` / `PostToolUse`): permission checks before tools run and logging afterward.
4
+
5
+ ## Generated outputs
6
+
7
+ The runnable scripts under `hooks/scripts/` are **built from TypeScript** in `src/hooks/`:
8
+
9
+ | Output (`hooks/scripts/`) | Source |
10
+ | ---------------------------- | -------------------------------------- |
11
+ | `pre-tool-use.cjs` | `src/hooks/codex-cli-pre-tool-use.ts` |
12
+ | `post-tool-use.cjs` | `src/hooks/codex-cli-post-tool-use.ts` |
13
+ | `codex-cli-hooks-shared.cjs` | `src/hooks/codex-cli-hooks-shared.ts` |
14
+ | `codex-cli-tool-map.cjs` | `src/hooks/codex-cli-tool-map.ts` |
15
+
16
+ Do **not** edit the `.cjs` files by hand. Change the `.ts` sources and rebuild.
17
+
18
+ ## Build
19
+
20
+ From the **multicorn-shield** package root:
21
+
22
+ ```bash
23
+ pnpm build
24
+ ```
25
+
26
+ That runs `tsup`, which emits the Codex CLI hook bundle into `plugins/codex-cli/hooks/scripts/`.
27
+
28
+ ## Manual testing
29
+
30
+ Hooks read **one JSON object from stdin** (Codex passes the hook payload). Examples:
31
+
32
+ ```bash
33
+ echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | node plugins/codex-cli/hooks/scripts/pre-tool-use.cjs
34
+ echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"tool_result":"ok"}' | node plugins/codex-cli/hooks/scripts/post-tool-use.cjs
35
+ ```
36
+
37
+ Use a valid `~/.multicorn/config.json` with `apiKey`, `baseUrl`, and agent entries when exercising the Shield API paths.
38
+
39
+ ## Main documentation
40
+
41
+ See the [multicorn-shield README](../../README.md) for installation, configuration, and Shield concepts.
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+
3
+ var fs = require("fs");
4
+ var http = require("http");
5
+ var https = require("https");
6
+ var os = require("os");
7
+ var path = require("path");
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== "default") {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(
17
+ n,
18
+ k,
19
+ d.get
20
+ ? d
21
+ : {
22
+ enumerable: true,
23
+ get: function () {
24
+ return e[k];
25
+ },
26
+ },
27
+ );
28
+ }
29
+ });
30
+ }
31
+ n.default = e;
32
+ return Object.freeze(n);
33
+ }
34
+
35
+ var fs__namespace = /*#__PURE__*/ _interopNamespace(fs);
36
+ var http__namespace = /*#__PURE__*/ _interopNamespace(http);
37
+ var https__namespace = /*#__PURE__*/ _interopNamespace(https);
38
+ var os__namespace = /*#__PURE__*/ _interopNamespace(os);
39
+ var path__namespace = /*#__PURE__*/ _interopNamespace(path);
40
+
41
+ // AUTO-GENERATED from src/hooks/codex-cli-*.ts — do not edit manually. Run pnpm build from the package root to regenerate.
42
+
43
+ var AUTH_HEADER = "X-Multicorn-Key";
44
+ var AUDIT_METADATA_MAX_CHARS = 1e4;
45
+ function redactSecretsForAudit(serialized) {
46
+ let out = serialized;
47
+ out = out.replace(/\bsk-[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
48
+ out = out.replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED]");
49
+ out = out.replace(/\bghp_[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
50
+ out = out.replace(/\bgho_[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
51
+ out = out.replace(/\bghu_[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
52
+ out = out.replace(/\bghs_[a-zA-Z0-9]{20,}\b/g, "[REDACTED]");
53
+ out = out.replace(
54
+ /-----BEGIN[A-Z0-9 \n\r-]+-----[\s\S]*?-----END[A-Z0-9 \n\r-]+-----/g,
55
+ "[REDACTED]",
56
+ );
57
+ out = out.replace(/token=[^\s"&]+/gi, "token=[REDACTED]");
58
+ out = out.replace(/\bBearer\s+[a-zA-Z0-9._\-+/=]+\b/gi, "Bearer [REDACTED]");
59
+ return out;
60
+ }
61
+ function truncateForAudit(serialized, maxChars = AUDIT_METADATA_MAX_CHARS) {
62
+ if (serialized.length <= maxChars) return serialized;
63
+ return `${serialized.slice(0, maxChars)}[truncated]`;
64
+ }
65
+ function serializeHookAuditFragment(value) {
66
+ try {
67
+ const raw = typeof value === "string" ? value : JSON.stringify(value === void 0 ? null : value);
68
+ return truncateForAudit(redactSecretsForAudit(raw));
69
+ } catch {
70
+ return "[unserializable]";
71
+ }
72
+ }
73
+ function isLocalHostname(hostname) {
74
+ const h = hostname.toLowerCase();
75
+ return h === "localhost" || h === "127.0.0.1" || h === "::1";
76
+ }
77
+ function assertHttpsOrLocalhostForApiKey(u) {
78
+ if (u.protocol === "http:" && !isLocalHostname(u.hostname)) {
79
+ throw new Error(`HTTP_API_KEY_REFUSED:${u.hostname}`);
80
+ }
81
+ }
82
+ function cwdUnderWorkspacePath(cwdResolved, workspacePath) {
83
+ const w = path__namespace.resolve(workspacePath);
84
+ if (cwdResolved === w) return true;
85
+ const prefix = w.endsWith(path__namespace.sep) ? w : w + path__namespace.sep;
86
+ return cwdResolved.startsWith(prefix);
87
+ }
88
+ function resolveCodexCliAgentName(obj) {
89
+ const pwd = process.env["PWD"];
90
+ const cwdRaw = pwd !== void 0 && pwd.length > 0 ? pwd : process.cwd();
91
+ const agents = obj["agents"];
92
+ const defaultAgentRaw = obj["defaultAgent"];
93
+ const defaultAgentName =
94
+ typeof defaultAgentRaw === "string" && defaultAgentRaw.length > 0 ? defaultAgentRaw : "";
95
+ if (!Array.isArray(agents)) {
96
+ return typeof obj["agentName"] === "string" ? obj["agentName"] : "";
97
+ }
98
+ const matches = [];
99
+ for (const entry of agents) {
100
+ if (entry === null || typeof entry !== "object") continue;
101
+ const e = entry;
102
+ if (e["platform"] !== "codex-cli") continue;
103
+ const n = e["name"];
104
+ if (typeof n !== "string") continue;
105
+ const wp = e["workspacePath"];
106
+ matches.push({
107
+ name: n,
108
+ ...(typeof wp === "string" && wp.length > 0 ? { workspacePath: wp } : {}),
109
+ });
110
+ }
111
+ if (matches.length === 0) {
112
+ return typeof obj["agentName"] === "string" ? obj["agentName"] : "";
113
+ }
114
+ const withWs = matches.filter(
115
+ (m) => typeof m.workspacePath === "string" && m.workspacePath.length > 0,
116
+ );
117
+ const resolvedCwd = path__namespace.resolve(cwdRaw);
118
+ let best = null;
119
+ let bestLen = -1;
120
+ for (const m of withWs) {
121
+ const wp = m.workspacePath;
122
+ if (!cwdUnderWorkspacePath(resolvedCwd, wp)) continue;
123
+ const len = path__namespace.resolve(wp).length;
124
+ if (len > bestLen) {
125
+ bestLen = len;
126
+ best = { name: m.name, workspacePath: wp };
127
+ }
128
+ }
129
+ if (best !== null) return best.name;
130
+ if (defaultAgentName.length > 0) {
131
+ const d = matches.find((m) => m.name === defaultAgentName);
132
+ if (d !== void 0) return d.name;
133
+ }
134
+ const first = matches[0];
135
+ return first !== void 0 ? first.name : "";
136
+ }
137
+ function warnIfConfigWorldReadable(configPath) {
138
+ try {
139
+ const st = fs__namespace.statSync(configPath);
140
+ const mode777 = st.mode & 511;
141
+ if ((st.mode & 63) !== 0) {
142
+ process.stderr.write(
143
+ `[Shield] Warning: ~/.multicorn/config.json is readable by other users (current: 0${mode777.toString(8)}). Run: chmod 600 ~/.multicorn/config.json
144
+ `,
145
+ );
146
+ }
147
+ } catch {}
148
+ }
149
+ function loadCodexCliConfig() {
150
+ try {
151
+ const configPath = path__namespace.join(os__namespace.homedir(), ".multicorn", "config.json");
152
+ const raw = fs__namespace.readFileSync(configPath, "utf8");
153
+ warnIfConfigWorldReadable(configPath);
154
+ const obj = JSON.parse(raw);
155
+ const apiKey = typeof obj["apiKey"] === "string" ? obj["apiKey"] : "";
156
+ const baseUrl =
157
+ typeof obj["baseUrl"] === "string" && obj["baseUrl"].length > 0
158
+ ? obj["baseUrl"].replace(/\/+$/, "")
159
+ : "https://api.multicorn.ai";
160
+ const agentName = resolveCodexCliAgentName(obj);
161
+ return { apiKey, baseUrl, agentName };
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ function formatShieldNetworkError(err) {
167
+ const debugEnv = process.env["MULTICORN_DEBUG"];
168
+ const debug = debugEnv === "1" || debugEnv === "true" || debugEnv === "yes";
169
+ let line =
170
+ "[Shield] Error: failed to connect to Shield API. Check your network and baseUrl configuration.\n";
171
+ if (debug && err instanceof Error && err.message.length > 0) {
172
+ line += ` Debug: ${err.message}
173
+ `;
174
+ }
175
+ return line;
176
+ }
177
+ function formatHttpApiKeyRefusal(hostname) {
178
+ return `[Shield] Error: refusing to send API key over unencrypted HTTP to ${hostname}. Use HTTPS or localhost.
179
+ `;
180
+ }
181
+ function readHttpApiKeyRefusalHostname(err) {
182
+ if (!(err instanceof Error) || !err.message.startsWith("HTTP_API_KEY_REFUSED:")) {
183
+ return null;
184
+ }
185
+ return err.message.slice("HTTP_API_KEY_REFUSED:".length);
186
+ }
187
+ async function shieldGetJson(baseUrl, apiKey, reqPath) {
188
+ const root = baseUrl.replace(/\/+$/, "");
189
+ const p = reqPath.startsWith("/") ? reqPath : `/${reqPath}`;
190
+ const u = new URL(`${root}${p}`);
191
+ assertHttpsOrLocalhostForApiKey(u);
192
+ const isHttps = u.protocol === "https:";
193
+ const lib = isHttps ? https__namespace : http__namespace;
194
+ const port = u.port !== "" ? Number(u.port) : isHttps ? 443 : 80;
195
+ return await new Promise((resolve2, reject) => {
196
+ const options = {
197
+ hostname: u.hostname,
198
+ port,
199
+ path: u.pathname + u.search,
200
+ method: "GET",
201
+ headers: {
202
+ [AUTH_HEADER]: apiKey,
203
+ },
204
+ };
205
+ const req = lib.request(options, (res) => {
206
+ const chunks = [];
207
+ res.on("data", (c) => chunks.push(c));
208
+ res.on("end", () => {
209
+ resolve2({
210
+ statusCode: res.statusCode ?? 0,
211
+ bodyText: Buffer.concat(chunks).toString("utf8"),
212
+ });
213
+ });
214
+ });
215
+ req.on("error", reject);
216
+ req.end();
217
+ });
218
+ }
219
+ async function shieldPostJson(baseUrl, apiKey, bodyObj) {
220
+ const root = baseUrl.replace(/\/+$/, "");
221
+ const u = new URL(`${root}/api/v1/actions`);
222
+ assertHttpsOrLocalhostForApiKey(u);
223
+ const payload = JSON.stringify(bodyObj);
224
+ const isHttps = u.protocol === "https:";
225
+ const lib = isHttps ? https__namespace : http__namespace;
226
+ const port = u.port !== "" ? Number(u.port) : isHttps ? 443 : 80;
227
+ return await new Promise((resolve2, reject) => {
228
+ const options = {
229
+ hostname: u.hostname,
230
+ port,
231
+ path: u.pathname + u.search,
232
+ method: "POST",
233
+ headers: {
234
+ "Content-Type": "application/json",
235
+ "Content-Length": Buffer.byteLength(payload, "utf8"),
236
+ [AUTH_HEADER]: apiKey,
237
+ },
238
+ };
239
+ const req = lib.request(options, (res) => {
240
+ const chunks = [];
241
+ res.on("data", (c) => chunks.push(c));
242
+ res.on("end", () => {
243
+ resolve2({
244
+ statusCode: res.statusCode ?? 0,
245
+ bodyText: Buffer.concat(chunks).toString("utf8"),
246
+ });
247
+ });
248
+ });
249
+ req.on("error", reject);
250
+ req.write(payload);
251
+ req.end();
252
+ });
253
+ }
254
+ async function shieldPostJsonFireAndForget(baseUrl, apiKey, bodyObj) {
255
+ await shieldPostJson(baseUrl, apiKey, bodyObj);
256
+ }
257
+
258
+ exports.AUTH_HEADER = AUTH_HEADER;
259
+ exports.assertHttpsOrLocalhostForApiKey = assertHttpsOrLocalhostForApiKey;
260
+ exports.cwdUnderWorkspacePath = cwdUnderWorkspacePath;
261
+ exports.formatHttpApiKeyRefusal = formatHttpApiKeyRefusal;
262
+ exports.formatShieldNetworkError = formatShieldNetworkError;
263
+ exports.isLocalHostname = isLocalHostname;
264
+ exports.loadCodexCliConfig = loadCodexCliConfig;
265
+ exports.readHttpApiKeyRefusalHostname = readHttpApiKeyRefusalHostname;
266
+ exports.redactSecretsForAudit = redactSecretsForAudit;
267
+ exports.resolveCodexCliAgentName = resolveCodexCliAgentName;
268
+ exports.serializeHookAuditFragment = serializeHookAuditFragment;
269
+ exports.shieldGetJson = shieldGetJson;
270
+ exports.shieldPostJson = shieldPostJson;
271
+ exports.shieldPostJsonFireAndForget = shieldPostJsonFireAndForget;
272
+ exports.truncateForAudit = truncateForAudit;
273
+ exports.warnIfConfigWorldReadable = warnIfConfigWorldReadable;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+
3
+ // AUTO-GENERATED from src/hooks/codex-cli-*.ts — do not edit manually. Run pnpm build from the package root to regenerate.
4
+
5
+ // src/openclaw/tool-mapper.ts
6
+ var TOOL_MAP = {
7
+ // OpenClaw built-in tools
8
+ read: { service: "filesystem", permissionLevel: "read" },
9
+ write: { service: "filesystem", permissionLevel: "write" },
10
+ edit: { service: "filesystem", permissionLevel: "write" },
11
+ exec: { service: "terminal", permissionLevel: "execute" },
12
+ browser: { service: "browser", permissionLevel: "execute" },
13
+ message: { service: "messaging", permissionLevel: "write" },
14
+ process: { service: "terminal", permissionLevel: "execute" },
15
+ sessions_spawn: { service: "agents", permissionLevel: "execute" },
16
+ // Common integration tools (MCP servers, skills, etc.)
17
+ // Gmail
18
+ gmail: { service: "gmail", permissionLevel: "execute" },
19
+ gmail_send: { service: "gmail", permissionLevel: "write" },
20
+ gmail_read: { service: "gmail", permissionLevel: "read" },
21
+ // Google Calendar
22
+ google_calendar: { service: "google_calendar", permissionLevel: "execute" },
23
+ calendar: { service: "google_calendar", permissionLevel: "execute" },
24
+ calendar_create: { service: "google_calendar", permissionLevel: "write" },
25
+ calendar_read: { service: "google_calendar", permissionLevel: "read" },
26
+ // Google Drive
27
+ google_drive: { service: "google_drive", permissionLevel: "execute" },
28
+ drive: { service: "google_drive", permissionLevel: "execute" },
29
+ drive_read: { service: "google_drive", permissionLevel: "read" },
30
+ drive_write: { service: "google_drive", permissionLevel: "write" },
31
+ // Slack
32
+ slack: { service: "slack", permissionLevel: "execute" },
33
+ slack_send: { service: "slack", permissionLevel: "write" },
34
+ slack_read: { service: "slack", permissionLevel: "read" },
35
+ slack_message: { service: "slack", permissionLevel: "write" },
36
+ // Payments
37
+ payments: { service: "payments", permissionLevel: "write" },
38
+ payment: { service: "payments", permissionLevel: "write" },
39
+ stripe: { service: "payments", permissionLevel: "write" },
40
+ };
41
+ function mapToolToScope(toolName, command) {
42
+ const normalized = toolName.trim().toLowerCase();
43
+ if (normalized.length === 0) {
44
+ return { service: "unknown", permissionLevel: "execute" };
45
+ }
46
+ const known = TOOL_MAP[normalized];
47
+ if (known !== void 0) {
48
+ return known;
49
+ }
50
+ const integrationPrefixes = {
51
+ gmail: "gmail",
52
+ google_calendar: "google_calendar",
53
+ calendar: "google_calendar",
54
+ google_drive: "google_drive",
55
+ drive: "google_drive",
56
+ slack: "slack",
57
+ payments: "payments",
58
+ payment: "payments",
59
+ stripe: "payments",
60
+ };
61
+ for (const [prefix, service] of Object.entries(integrationPrefixes)) {
62
+ if (normalized.startsWith(prefix + "_") || normalized === prefix) {
63
+ let permissionLevel = "execute";
64
+ if (
65
+ normalized.includes("_read") ||
66
+ normalized.includes("_get") ||
67
+ normalized.includes("_list")
68
+ ) {
69
+ permissionLevel = "read";
70
+ } else if (
71
+ normalized.includes("_write") ||
72
+ normalized.includes("_send") ||
73
+ normalized.includes("_create") ||
74
+ normalized.includes("_update") ||
75
+ normalized.includes("_delete")
76
+ ) {
77
+ permissionLevel = "write";
78
+ }
79
+ return { service, permissionLevel };
80
+ }
81
+ }
82
+ return { service: normalized, permissionLevel: "execute" };
83
+ }
84
+ function isKnownTool(toolName) {
85
+ return Object.hasOwn(TOOL_MAP, toolName.trim().toLowerCase());
86
+ }
87
+
88
+ // src/hooks/codex-cli-tool-map.ts
89
+ var CODEX_DESTRUCTIVE_KEYWORDS = ["rm", "mv", "sudo", "chmod", "chown", "dd", "truncate", "shred"];
90
+ function escapeRegExp(s) {
91
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ }
93
+ function destructiveTokenRegex(keyword) {
94
+ const e = escapeRegExp(keyword);
95
+ return new RegExp(`(^|[^a-zA-Z0-9_])${e}(?![a-zA-Z0-9_-])`, "i");
96
+ }
97
+ function codexIsDestructiveExecCommand(command) {
98
+ const trimmed = command.trim();
99
+ if (/^\s*echo\b/i.test(trimmed) && !/[;&|]/.test(trimmed)) {
100
+ return false;
101
+ }
102
+ const withoutQuotes = trimmed.replace(/'[^']*'/g, " ").replace(/"([^"\\]|\\.)*"/g, " ");
103
+ const normalized = withoutQuotes.toLowerCase();
104
+ return CODEX_DESTRUCTIVE_KEYWORDS.some((kw) => destructiveTokenRegex(kw).test(normalized));
105
+ }
106
+ function extractExecCommand(toolInput) {
107
+ if (toolInput === void 0 || toolInput === null) {
108
+ return void 0;
109
+ }
110
+ if (typeof toolInput === "object") {
111
+ const o = toolInput;
112
+ const c = o["command"];
113
+ if (typeof c === "string") {
114
+ return c;
115
+ }
116
+ }
117
+ if (typeof toolInput === "string") {
118
+ try {
119
+ return extractExecCommand(JSON.parse(toolInput));
120
+ } catch {
121
+ return toolInput;
122
+ }
123
+ }
124
+ return void 0;
125
+ }
126
+ function mapCodexCliToolToShield(toolName, toolInput) {
127
+ const n = toolName.trim().toLowerCase();
128
+ if (n.length === 0) {
129
+ return { service: "unknown", actionType: "execute" };
130
+ }
131
+ if (n === "bash") {
132
+ const cmd = extractExecCommand(toolInput);
133
+ if (cmd !== void 0 && codexIsDestructiveExecCommand(cmd)) {
134
+ return { service: "terminal", actionType: "write" };
135
+ }
136
+ return { service: "terminal", actionType: "execute" };
137
+ }
138
+ if (n === "apply_patch" || n === "edit" || n === "write") {
139
+ return { service: "filesystem", actionType: "write" };
140
+ }
141
+ const scope = mapToolToScope(n);
142
+ let actionType = scope.permissionLevel;
143
+ if (!isKnownTool(n) && actionType === "execute") {
144
+ actionType = "write";
145
+ }
146
+ return { service: scope.service, actionType };
147
+ }
148
+
149
+ exports.codexIsDestructiveExecCommand = codexIsDestructiveExecCommand;
150
+ exports.extractExecCommand = extractExecCommand;
151
+ exports.mapCodexCliToolToShield = mapCodexCliToolToShield;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ var codexCliHooksShared_js = require("./codex-cli-hooks-shared.cjs");
4
+ var codexCliToolMap_js = require("./codex-cli-tool-map.cjs");
5
+
6
+ // AUTO-GENERATED from src/hooks/codex-cli-*.ts — do not edit manually. Run pnpm build from the package root to regenerate.
7
+
8
+ function readStdin() {
9
+ return new Promise((resolve, reject) => {
10
+ const chunks = [];
11
+ process.stdin.setEncoding("utf8");
12
+ process.stdin.on("data", (c) => chunks.push(c));
13
+ process.stdin.on("end", () => {
14
+ resolve(chunks.join(""));
15
+ });
16
+ process.stdin.on("error", reject);
17
+ });
18
+ }
19
+ async function main() {
20
+ let raw;
21
+ try {
22
+ raw = await readStdin();
23
+ } catch {
24
+ process.exit(0);
25
+ }
26
+ const config = codexCliHooksShared_js.loadCodexCliConfig();
27
+ if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
28
+ process.exit(0);
29
+ }
30
+ let hookPayload;
31
+ try {
32
+ hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
33
+ } catch {
34
+ process.exit(0);
35
+ }
36
+ const toolNameRaw =
37
+ (typeof hookPayload["tool_name"] === "string" && hookPayload["tool_name"]) || "";
38
+ const toolInput = hookPayload["tool_input"] !== void 0 ? hookPayload["tool_input"] : void 0;
39
+ const toolResult =
40
+ hookPayload["tool_response"] !== void 0
41
+ ? hookPayload["tool_response"]
42
+ : hookPayload["tool_result"] !== void 0
43
+ ? hookPayload["tool_result"]
44
+ : void 0;
45
+ try {
46
+ void (typeof toolInput === "string"
47
+ ? toolInput
48
+ : JSON.stringify(toolInput === void 0 ? null : toolInput));
49
+ void (typeof toolResult === "string"
50
+ ? toolResult
51
+ : JSON.stringify(toolResult === void 0 ? null : toolResult));
52
+ } catch {
53
+ process.exit(0);
54
+ }
55
+ const { service, actionType } = codexCliToolMap_js.mapCodexCliToolToShield(
56
+ toolNameRaw,
57
+ toolInput,
58
+ );
59
+ const metadata = {
60
+ tool_name: toolNameRaw,
61
+ tool_input: codexCliHooksShared_js.serializeHookAuditFragment(toolInput),
62
+ tool_result: codexCliHooksShared_js.serializeHookAuditFragment(toolResult),
63
+ source: "codex-cli",
64
+ };
65
+ const payload = {
66
+ agent: config.agentName,
67
+ service,
68
+ actionType,
69
+ status: "approved",
70
+ metadata,
71
+ platform: "codex-cli",
72
+ };
73
+ try {
74
+ await codexCliHooksShared_js.shieldPostJson(config.baseUrl, config.apiKey, payload);
75
+ } catch (e) {
76
+ const refusedHost = codexCliHooksShared_js.readHttpApiKeyRefusalHostname(e);
77
+ if (refusedHost !== null) {
78
+ process.stderr.write(codexCliHooksShared_js.formatHttpApiKeyRefusal(refusedHost));
79
+ process.exit(0);
80
+ }
81
+ process.stderr.write("[Shield] Warning: failed to send logs to Shield.\n");
82
+ process.stderr.write(codexCliHooksShared_js.formatShieldNetworkError(e));
83
+ }
84
+ process.exit(0);
85
+ }
86
+ main().catch((e) => {
87
+ const refusedHost = codexCliHooksShared_js.readHttpApiKeyRefusalHostname(e);
88
+ if (refusedHost !== null) {
89
+ process.stderr.write(codexCliHooksShared_js.formatHttpApiKeyRefusal(refusedHost));
90
+ process.exit(0);
91
+ }
92
+ process.stderr.write("[Shield] Warning: failed to send logs to Shield.\n");
93
+ process.stderr.write(codexCliHooksShared_js.formatShieldNetworkError(e));
94
+ process.exit(0);
95
+ });
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+
3
+ var child_process = require("child_process");
4
+ var fs = require("fs");
5
+ var os = require("os");
6
+ var path = require("path");
7
+ var codexCliHooksShared_js = require("./codex-cli-hooks-shared.cjs");
8
+ var codexCliToolMap_js = require("./codex-cli-tool-map.cjs");
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== "default") {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(
18
+ n,
19
+ k,
20
+ d.get
21
+ ? d
22
+ : {
23
+ enumerable: true,
24
+ get: function () {
25
+ return e[k];
26
+ },
27
+ },
28
+ );
29
+ }
30
+ });
31
+ }
32
+ n.default = e;
33
+ return Object.freeze(n);
34
+ }
35
+
36
+ var fs__namespace = /*#__PURE__*/ _interopNamespace(fs);
37
+ var os__namespace = /*#__PURE__*/ _interopNamespace(os);
38
+ var path__namespace = /*#__PURE__*/ _interopNamespace(path);
39
+
40
+ // AUTO-GENERATED from src/hooks/codex-cli-*.ts — do not edit manually. Run pnpm build from the package root to regenerate.
41
+
42
+ var FAST_POLL =
43
+ process.env["NODE_ENV"] === "test" &&
44
+ process.env["MULTICORN_SHIELD_PRE_HOOK_TEST_FAST_POLL"] === "1";
45
+ var POLL_INTERVAL_MS = FAST_POLL ? 1 : 3e3;
46
+ var MAX_APPROVAL_POLLS = FAST_POLL ? 3 : 100;
47
+ function readStdin() {
48
+ return new Promise((resolve, reject) => {
49
+ const chunks = [];
50
+ process.stdin.setEncoding("utf8");
51
+ process.stdin.on("data", (c) => chunks.push(c));
52
+ process.stdin.on("end", () => {
53
+ resolve(chunks.join(""));
54
+ });
55
+ process.stdin.on("error", reject);
56
+ });
57
+ }
58
+ function dashboardOrigin(apiBaseUrl) {
59
+ try {
60
+ const raw = apiBaseUrl.replace(/\/+$/, "");
61
+ const lower = raw.toLowerCase();
62
+ if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
63
+ return "http://localhost:5173";
64
+ }
65
+ const u = new URL(raw);
66
+ if (u.hostname.startsWith("api.")) {
67
+ u.hostname = "app." + u.hostname.slice(4);
68
+ }
69
+ return u.origin;
70
+ } catch {
71
+ return "https://app.multicorn.ai";
72
+ }
73
+ }
74
+ function dashboardHintUrl(apiBaseUrl) {
75
+ return `${dashboardOrigin(apiBaseUrl)}/approvals`;
76
+ }
77
+ function consentUrl(apiBaseUrl, agentName, service, actionType) {
78
+ const origin = dashboardOrigin(apiBaseUrl);
79
+ const params = new URLSearchParams();
80
+ params.set("agent", agentName);
81
+ params.set("scopes", `${service}:${actionType}`);
82
+ params.set("platform", "codex-cli");
83
+ return `${origin}/consent?${params.toString()}`;
84
+ }
85
+ function safeJsonParse(text) {
86
+ try {
87
+ return JSON.parse(text);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ function unwrapData(body) {
93
+ if (typeof body !== "object" || body === null) return null;
94
+ const o = body;
95
+ return o["success"] === true ? o["data"] : null;
96
+ }
97
+ function denyViaStdout(reason) {
98
+ const response = JSON.stringify({
99
+ hookSpecificOutput: {
100
+ hookEventName: "PreToolUse",
101
+ permissionDecision: "deny",
102
+ permissionDecisionReason: reason,
103
+ },
104
+ });
105
+ process.stdout.write(response + "\n");
106
+ }
107
+ function blockedReason(data, service, actionType, approvalsUrl) {
108
+ if (data !== null && typeof data === "object") {
109
+ const d = data;
110
+ const meta = d["metadata"];
111
+ if (typeof meta === "string" && meta.length > 0) {
112
+ try {
113
+ const parsed = JSON.parse(meta);
114
+ if (parsed !== null && typeof parsed === "object" && "block_reason" in parsed) {
115
+ const br = parsed["block_reason"];
116
+ if (typeof br === "string" && br.length > 0) {
117
+ return `Shield blocked: ${br}. Grant access at ${approvalsUrl}`;
118
+ }
119
+ }
120
+ } catch {}
121
+ }
122
+ }
123
+ return `Shield blocked this tool call. Required permission: ${service} (${actionType}). Grant access at ${approvalsUrl}`;
124
+ }
125
+ function consentMarkerPath(agentName) {
126
+ const safe = agentName.replace(/[^a-zA-Z0-9_-]/g, "_");
127
+ return path__namespace.join(os__namespace.homedir(), ".multicorn", `.consent-${safe}`);
128
+ }
129
+ function hasConsentMarker(agentName) {
130
+ try {
131
+ fs__namespace.accessSync(consentMarkerPath(agentName));
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ function writeConsentMarker(agentName) {
138
+ try {
139
+ const marker = consentMarkerPath(agentName);
140
+ fs__namespace.mkdirSync(path__namespace.dirname(marker), { recursive: true });
141
+ fs__namespace.writeFileSync(marker, String(Date.now()), "utf8");
142
+ } catch {}
143
+ }
144
+ function removeConsentMarker(agentName) {
145
+ try {
146
+ fs__namespace.unlinkSync(consentMarkerPath(agentName));
147
+ } catch {}
148
+ }
149
+ function openBrowser(url) {
150
+ try {
151
+ if (process.platform === "win32") {
152
+ child_process.execSync(`start "" ${JSON.stringify(url)}`, {
153
+ shell: process.env["ComSpec"] ?? "cmd.exe",
154
+ stdio: "ignore",
155
+ windowsHide: true,
156
+ });
157
+ } else if (process.platform === "darwin") {
158
+ child_process.execFileSync("open", [url], { stdio: "ignore" });
159
+ } else {
160
+ child_process.execFileSync("xdg-open", [url], { stdio: "ignore" });
161
+ }
162
+ } catch {}
163
+ }
164
+ function sleep(ms) {
165
+ return new Promise((resolve) => setTimeout(resolve, ms));
166
+ }
167
+ async function pollApprovalStatus(config, approvalId) {
168
+ let lastProgressWrite = Date.now();
169
+ for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
170
+ if (i > 0) {
171
+ await sleep(POLL_INTERVAL_MS);
172
+ }
173
+ const now = Date.now();
174
+ if (now - lastProgressWrite >= 3e4) {
175
+ process.stderr.write(
176
+ "[Shield] Waiting for approval... (open the consent screen in your browser)\n",
177
+ );
178
+ lastProgressWrite = now;
179
+ }
180
+ let statusCode;
181
+ let bodyText;
182
+ try {
183
+ const res = await codexCliHooksShared_js.shieldGetJson(
184
+ config.baseUrl,
185
+ config.apiKey,
186
+ `/api/v1/approvals/${approvalId}`,
187
+ );
188
+ statusCode = res.statusCode;
189
+ bodyText = res.bodyText;
190
+ } catch (e) {
191
+ const refusedHost = codexCliHooksShared_js.readHttpApiKeyRefusalHostname(e);
192
+ if (refusedHost !== null) {
193
+ process.stderr.write(codexCliHooksShared_js.formatHttpApiKeyRefusal(refusedHost));
194
+ process.exit(2);
195
+ }
196
+ continue;
197
+ }
198
+ if (statusCode < 200 || statusCode >= 300) {
199
+ continue;
200
+ }
201
+ const parsed = safeJsonParse(bodyText);
202
+ const data = unwrapData(parsed);
203
+ if (data === null || typeof data !== "object") {
204
+ continue;
205
+ }
206
+ const d = data;
207
+ const statusRaw = d["status"];
208
+ const st = (typeof statusRaw === "string" ? statusRaw : "").toLowerCase();
209
+ if (st === "approved") {
210
+ return true;
211
+ }
212
+ if (st === "blocked" || st === "denied" || st === "rejected") {
213
+ const reasonRaw = d["reason"];
214
+ const reason =
215
+ typeof reasonRaw === "string" && reasonRaw.length > 0 ? reasonRaw : "Approval denied.";
216
+ denyViaStdout(`Shield denied this approval request: ${reason}`);
217
+ process.exit(0);
218
+ }
219
+ if (st === "expired") {
220
+ denyViaStdout(
221
+ "Shield approval request expired. Retry the tool call and complete approval when prompted.",
222
+ );
223
+ process.exit(0);
224
+ }
225
+ if (st === "pending") {
226
+ continue;
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ async function handlePendingWithConsentAndPoll(
232
+ config,
233
+ approvalId,
234
+ service,
235
+ actionType,
236
+ approvalsUrl,
237
+ ) {
238
+ if (hasConsentMarker(config.agentName)) {
239
+ process.stderr.write(
240
+ `[Shield] Waiting for approval (up to 5 min)...
241
+ Approve in the Shield dashboard: ${approvalsUrl}
242
+ `,
243
+ );
244
+ const approved2 = await pollApprovalStatus(config, approvalId);
245
+ if (approved2) {
246
+ process.exit(0);
247
+ }
248
+ removeConsentMarker(config.agentName);
249
+ denyViaStdout(
250
+ `Shield approval timed out after 5 minutes. Approve at ${approvalsUrl} and retry.`,
251
+ );
252
+ process.exit(0);
253
+ }
254
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
255
+ writeConsentMarker(config.agentName);
256
+ openBrowser(url);
257
+ process.stderr.write(
258
+ "[Shield] Opening Shield consent screen... Waiting for approval (up to 5 min).\n",
259
+ );
260
+ const approved = await pollApprovalStatus(config, approvalId);
261
+ if (approved) {
262
+ process.exit(0);
263
+ }
264
+ denyViaStdout(`Shield approval timed out after 5 minutes. Approve at ${approvalsUrl} and retry.`);
265
+ process.exit(0);
266
+ }
267
+ async function main() {
268
+ let raw;
269
+ try {
270
+ raw = await readStdin();
271
+ } catch {
272
+ process.stderr.write("[Shield] Warning: could not read stdin. Allowing tool.\n");
273
+ process.exit(0);
274
+ }
275
+ const config = codexCliHooksShared_js.loadCodexCliConfig();
276
+ if (config === null) {
277
+ process.exit(0);
278
+ }
279
+ if (config.apiKey.length === 0 || config.agentName.length === 0) {
280
+ process.exit(0);
281
+ }
282
+ let hookPayload;
283
+ try {
284
+ hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
285
+ } catch {
286
+ process.stderr.write("[Shield] Warning: invalid hook JSON. Allowing tool.\n");
287
+ process.exit(0);
288
+ }
289
+ const toolNameRaw =
290
+ (typeof hookPayload["tool_name"] === "string" && hookPayload["tool_name"]) || "";
291
+ const toolInput = hookPayload["tool_input"] !== void 0 ? hookPayload["tool_input"] : void 0;
292
+ try {
293
+ void (typeof toolInput === "string"
294
+ ? toolInput
295
+ : JSON.stringify(toolInput === void 0 ? null : toolInput));
296
+ } catch {
297
+ process.stderr.write("[Shield] Warning: could not serialize tool input. Allowing tool.\n");
298
+ process.exit(0);
299
+ }
300
+ const { service, actionType } = codexCliToolMap_js.mapCodexCliToolToShield(
301
+ toolNameRaw,
302
+ toolInput,
303
+ );
304
+ const approvalsUrl = dashboardHintUrl(config.baseUrl);
305
+ const metadata = {
306
+ tool_name: toolNameRaw,
307
+ tool_input: codexCliHooksShared_js.serializeHookAuditFragment(toolInput),
308
+ source: "codex-cli",
309
+ };
310
+ const payload = {
311
+ agent: config.agentName,
312
+ service,
313
+ actionType,
314
+ status: "pending",
315
+ metadata,
316
+ platform: "codex-cli",
317
+ };
318
+ let statusCode;
319
+ let bodyText;
320
+ try {
321
+ const res = await codexCliHooksShared_js.shieldPostJson(config.baseUrl, config.apiKey, payload);
322
+ statusCode = res.statusCode;
323
+ bodyText = res.bodyText;
324
+ } catch (e) {
325
+ const refusedHost = codexCliHooksShared_js.readHttpApiKeyRefusalHostname(e);
326
+ if (refusedHost !== null) {
327
+ process.stderr.write(codexCliHooksShared_js.formatHttpApiKeyRefusal(refusedHost));
328
+ process.exit(2);
329
+ }
330
+ process.stderr.write(codexCliHooksShared_js.formatShieldNetworkError(e));
331
+ process.exit(2);
332
+ }
333
+ const parsed = safeJsonParse(bodyText);
334
+ const data = unwrapData(parsed);
335
+ if (statusCode === 202) {
336
+ if (data === null || typeof data !== "object") {
337
+ denyViaStdout("This action needs approval in the Shield dashboard before it can run.");
338
+ process.exit(0);
339
+ }
340
+ const approvalIdRaw = data["approval_id"];
341
+ const approvalId = typeof approvalIdRaw === "string" ? approvalIdRaw : "";
342
+ if (approvalId.length === 0) {
343
+ denyViaStdout("This action needs approval in the Shield dashboard before it can run.");
344
+ process.exit(0);
345
+ }
346
+ await handlePendingWithConsentAndPoll(config, approvalId, service, actionType, approvalsUrl);
347
+ return;
348
+ }
349
+ if (statusCode === 201) {
350
+ if (data === null || typeof data !== "object") {
351
+ const detail = bodyText.length > 500 ? `${bodyText.slice(0, 500)}...` : bodyText;
352
+ process.stderr.write(
353
+ `[Shield] Error: unexpected Shield response, cannot verify permissions.
354
+ Detail: ${detail}
355
+ `,
356
+ );
357
+ process.exit(2);
358
+ }
359
+ const dataObj = data;
360
+ const statusRaw = dataObj["status"];
361
+ const st = (typeof statusRaw === "string" ? statusRaw : "").toLowerCase();
362
+ if (st === "approved") {
363
+ process.exit(0);
364
+ }
365
+ if (st === "blocked") {
366
+ denyViaStdout(blockedReason(data, service, actionType, approvalsUrl));
367
+ process.exit(0);
368
+ }
369
+ process.stderr.write(
370
+ `[Shield] Error: ambiguous Shield status, cannot verify permissions.
371
+ Detail: status=${JSON.stringify(dataObj["status"])}
372
+ `,
373
+ );
374
+ process.exit(2);
375
+ }
376
+ const httpDetail = bodyText.length > 300 ? `${bodyText.slice(0, 300)}...` : bodyText;
377
+ process.stderr.write(
378
+ `[Shield] Error: Shield returned HTTP ${String(statusCode)}, cannot verify permissions.
379
+ Detail: HTTP ${String(statusCode)} body=${httpDetail}
380
+ `,
381
+ );
382
+ process.exit(2);
383
+ }
384
+ main().catch((e) => {
385
+ const refusedHost = codexCliHooksShared_js.readHttpApiKeyRefusalHostname(e);
386
+ if (refusedHost !== null) {
387
+ process.stderr.write(codexCliHooksShared_js.formatHttpApiKeyRefusal(refusedHost));
388
+ process.exit(2);
389
+ }
390
+ process.stderr.write(codexCliHooksShared_js.formatShieldNetworkError(e));
391
+ process.exit(2);
392
+ });