@mutmutco/codex-plugin 4.3.6 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.3.6",
3
+ "version": "4.3.7",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.3.6",
3
+ "version": "4.3.7",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -91,7 +91,7 @@ Every refusal and every doctor blocker names its `code` and `anchor`: open `docs
91
91
 
92
92
  ## Report
93
93
 
94
- Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), every `workflowRuns` entry with its conclusion and URL — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
94
+ Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), and every workflow run row with its conclusion and evidence — use its URL when emitted, otherwise the receipt's explicit no-run-URL note — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
95
95
 
96
96
  ## Retro
97
97
 
@@ -69,7 +69,7 @@ Every refusal and every doctor blocker names its `code` and `anchor`: open `docs
69
69
 
70
70
  ## Report
71
71
 
72
- Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), every `workflowRuns` entry with its conclusion and URL — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
72
+ Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), and every workflow run row with its conclusion and evidence — use its URL when emitted, otherwise the receipt's explicit no-run-URL note — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
73
73
 
74
74
  ## Retro
75
75
 
@@ -76,7 +76,7 @@ Every refusal and every doctor blocker names its `code` and `anchor`: open `docs
76
76
 
77
77
  ## Report
78
78
 
79
- Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), every `workflowRuns` entry with its conclusion and URL — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
79
+ Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), and every workflow run row with its conclusion and evidence — use its URL when emitted, otherwise the receipt's explicit no-run-URL note — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
80
80
 
81
81
  ## Retro
82
82
 
@@ -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
- }