@mutmutco/codex-plugin 4.3.5 → 4.3.7

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.
@@ -1,115 +0,0 @@
1
- // The files one edit-shaped tool call touches, across BOTH host contracts (#3563).
2
- //
3
- // Claude sends `Edit`/`Write` with `tool_input.file_path`. Codex also sends `apply_patch`, and puts the
4
- // whole patch in `tool_input.command` — there is no `file_path` at all. A gate that reads only
5
- // `file_path` therefore sees nothing to check and exits 0: on Codex the vault-edit gate FAILED OPEN and
6
- // `apply_patch` could create a `.env`, while the guide claimed the path was protected (cross-vendor
7
- // check, openai-sol). Matching `apply_patch` in a host manifest does not rewrite the payload.
8
- //
9
- // One extractor, used by every edit-shaped integration, so a new host contract is taught in one place.
10
-
11
- /** Tool names that mean "this call writes files". `StrReplace` is Cursor's in-place edit tool: it
12
- * carries its target in `tool_input.path` and was outside this set, so even once the adapter matched
13
- * it the gate would have read a non-edit tool and allowed (#5852). */
14
- export const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'apply_patch', 'ApplyPatch', 'StrReplace']);
15
-
16
- export function isEditTool(toolName) {
17
- return typeof toolName === 'string' && EDIT_TOOL_NAMES.has(toolName);
18
- }
19
-
20
- // `*** Add File: path`, `*** Update File: path`, `*** Delete File: path`, and the `*** Move to: path`
21
- // destination of a rename. Case-insensitive on the verb; the path runs to end of line.
22
- const PATCH_TOUCH = /^\*\*\*\s+(add|update|delete)\s+file:\s*(.+?)\s*$/gim;
23
- const PATCH_MOVE = /^\*\*\*\s+move\s+to:\s*(.+?)\s*$/gim;
24
-
25
- function scan(re, patch, onMatch) {
26
- re.lastIndex = 0;
27
- for (let m = re.exec(patch); m; m = re.exec(patch)) onMatch(m);
28
- }
29
-
30
- /** Every path an `apply_patch` body TOUCHES — created, updated, deleted, or renamed onto. */
31
- export function applyPatchTargets(patch) {
32
- if (typeof patch !== 'string' || !patch) return [];
33
- const out = [];
34
- scan(PATCH_TOUCH, patch, (m) => out.push(m[2]));
35
- scan(PATCH_MOVE, patch, (m) => out.push(m[1]));
36
- return out;
37
- }
38
-
39
- /** Only the paths a patch CREATES or WRITES — `Add`, `Update`, and a rename destination. A `Delete File:`
40
- * target is excluded: the no-env floor blocks creating a `.env`, never removing one, and treating every
41
- * extracted target as a write turned a delete-only patch into a denial (cross-vendor check, openai-sol). */
42
- export function applyPatchWriteTargets(patch) {
43
- if (typeof patch !== 'string' || !patch) return [];
44
- const out = [];
45
- scan(PATCH_TOUCH, patch, (m) => { if (m[1].toLowerCase() !== 'delete') out.push(m[2]); });
46
- scan(PATCH_MOVE, patch, (m) => out.push(m[1]));
47
- return out;
48
- }
49
-
50
- /** True when a shell command actually INVOKES `apply_patch` — as the COMMAND of some segment, not merely
51
- * as a word in it.
52
- *
53
- * Patch-looking text is not a patch: `cat <<EOF … *** Add File: .env … EOF` writes nothing. But a bare
54
- * word-boundary test was wrong both ways — `"apply_patch" <<EOF` (quoted) and `/usr/bin/apply_patch`
55
- * slipped through, while `echo apply_patch <<EOF` was denied for running `echo` (cross-vendor check,
56
- * openai-sol). So: split into segments, take each segment's first token, strip quotes and any directory
57
- * prefix, and skip the leading `env`/`command`/`exec`/`sudo` wrappers that only prefix a real command. */
58
- const INVOKE_PREFIXES = new Set(['env', 'command', 'exec', 'sudo', 'nohup', 'time']);
59
- // `bash -lc "apply_patch …"` is how Codex delivers a patch through a shell tool: the real command sits
60
- // inside the wrapper's argument, so the wrapper and its flags are stepped over too.
61
- const SHELL_WRAPPERS = new Set(['bash', 'sh', 'zsh', 'dash', 'pwsh', 'powershell', 'cmd', 'cmd.exe']);
62
-
63
- export function invokesApplyPatch(command) {
64
- if (typeof command !== 'string' || !command) return false;
65
- // Redirects may abut the command word — `apply_patch<<'EOF'` and `apply_patch<<<"$P"` are both valid
66
- // shell, and both hid the command name from a whitespace tokenizer (cross-vendor check).
67
- // Separating the operators first is what makes the command word its own token in every form.
68
- const spaced = command.replace(/([<>]+)/g, ' $1 ');
69
- for (const segment of spaced.split(/\|\||&&|[;&|\n]|<<-?\s*['"]?\w+['"]?/)) {
70
- let wrapped = false;
71
- for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
72
- const bare = raw.replace(/^['"]+|['"]+$/g, '').replace(/\\/g, '/').split('/').pop() ?? '';
73
- if (bare === 'apply_patch') return true;
74
- // Step over wrappers, their env assignments, and — once inside a shell wrapper — its flags.
75
- // Anything else in command position means this segment runs something other than apply_patch.
76
- if (SHELL_WRAPPERS.has(bare.toLowerCase())) { wrapped = true; continue; }
77
- if (INVOKE_PREFIXES.has(bare) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(raw)) continue;
78
- if (wrapped && /^[-/]/.test(raw)) continue;
79
- break;
80
- }
81
- }
82
- return false;
83
- }
84
-
85
- /**
86
- * Every file path a tool call writes to. Returns `[]` for non-edit tools and unreadable payloads —
87
- * callers treat an empty list as "nothing to check", exactly as they treated a missing `file_path`.
88
- * @param {{ tool_name?: unknown, tool_input?: Record<string, unknown> }} input raw tool payload
89
- * @returns {string[]}
90
- */
91
- export function editedPaths(input) {
92
- const toolName = input?.tool_name;
93
- if (!isEditTool(toolName)) return [];
94
- return collectEditedPaths(input?.tool_input ?? {});
95
- }
96
-
97
- /** Every write target in a tool_input, from a direct path AND from any patch body it carries.
98
- *
99
- * Union, never first-match. Returning early on `file_path` meant an `apply_patch` carrying a harmless
100
- * `file_path: "safe.txt"` alongside `*** Add File: .env` reported only `safe.txt`, and the vault gate
101
- * allowed it — a fail-open reachable by adding one benign field (cross-vendor check, openai-sol). A
102
- * gate must see every path a call writes, not the first one it finds. */
103
- export function collectEditedPaths(toolInput) {
104
- const ti = toolInput ?? {};
105
- const out = [];
106
- for (const direct of [ti.file_path, ti.path]) {
107
- if (typeof direct === 'string' && direct) out.push(direct);
108
- }
109
- // Codex `apply_patch`: the patch text arrives as `command` (or `patch`/`input` on some builds).
110
- // WRITE targets only — the shell path already excluded a `*** Delete File:` and this one did not, so a
111
- // delete-only patch was allowed through the shell and denied through the tool (cross-vendor check,
112
- // openai-sol). One rule, both doors.
113
- for (const field of ['command', 'patch', 'input']) out.push(...applyPatchWriteTargets(ti[field]));
114
- return [...new Set(out)];
115
- }