multicorn-shield 1.8.0 → 1.9.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/dist/index.cjs +1 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/multicorn-proxy.js +227 -2
- package/dist/multicorn-shield.js +226 -2
- package/dist/shield-extension.js +7 -1
- package/package.json +2 -1
- package/plugins/codex-cli/README.md +41 -0
- package/plugins/codex-cli/hooks/scripts/codex-cli-hooks-shared.cjs +273 -0
- package/plugins/codex-cli/hooks/scripts/codex-cli-tool-map.cjs +151 -0
- package/plugins/codex-cli/hooks/scripts/post-tool-use.cjs +95 -0
- package/plugins/codex-cli/hooks/scripts/pre-tool-use.cjs +392 -0
package/dist/shield-extension.js
CHANGED
|
@@ -22360,6 +22360,12 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
|
22360
22360
|
section: "native",
|
|
22361
22361
|
prereqUrl: "https://opencode.ai"
|
|
22362
22362
|
},
|
|
22363
|
+
{
|
|
22364
|
+
slug: "codex-cli",
|
|
22365
|
+
displayName: "Codex CLI",
|
|
22366
|
+
section: "native",
|
|
22367
|
+
prereqUrl: "https://github.com/openai/codex"
|
|
22368
|
+
},
|
|
22363
22369
|
{
|
|
22364
22370
|
slug: "cursor",
|
|
22365
22371
|
displayName: "Cursor",
|
|
@@ -22511,7 +22517,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22511
22517
|
|
|
22512
22518
|
// package.json
|
|
22513
22519
|
var package_default = {
|
|
22514
|
-
version: "1.
|
|
22520
|
+
version: "1.9.1"};
|
|
22515
22521
|
|
|
22516
22522
|
// src/package-meta.ts
|
|
22517
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.
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -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
|
+
});
|