@memnexus-ai/mx-agent-cli 0.1.196 → 0.1.198

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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Guards for the CLI's agent-config bundler (scripts/bundle-config.mjs).
3
+ *
4
+ * Two regressions this locks down (security review of #4304 Phase 1):
5
+ *
6
+ * 1. The exclusion filter must test the path RELATIVE to the source root, not
7
+ * the absolute path cpSync hands it. Otherwise a checkout under a directory
8
+ * whose path contains a `local`/`node_modules`/`.git` segment (e.g.
9
+ * `/usr/local/src/...`, `~/.local/share/...`) matches the checkout-location
10
+ * PREFIX and excludes the ENTIRE agent-config tree — silently shipping an
11
+ * empty dist/agent-config/ (no settings.json, no git-mutation-guard hook,
12
+ * no agents).
13
+ *
14
+ * 2. A post-build assertion must FAIL the build if the copy produced an empty
15
+ * or partial bundle, rather than printing a success line unconditionally.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=bundle-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bundle-config.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Guards for the CLI's agent-config bundler (scripts/bundle-config.mjs).
3
+ *
4
+ * Two regressions this locks down (security review of #4304 Phase 1):
5
+ *
6
+ * 1. The exclusion filter must test the path RELATIVE to the source root, not
7
+ * the absolute path cpSync hands it. Otherwise a checkout under a directory
8
+ * whose path contains a `local`/`node_modules`/`.git` segment (e.g.
9
+ * `/usr/local/src/...`, `~/.local/share/...`) matches the checkout-location
10
+ * PREFIX and excludes the ENTIRE agent-config tree — silently shipping an
11
+ * empty dist/agent-config/ (no settings.json, no git-mutation-guard hook,
12
+ * no agents).
13
+ *
14
+ * 2. A post-build assertion must FAIL the build if the copy produced an empty
15
+ * or partial bundle, rather than printing a success line unconditionally.
16
+ */
17
+ import { describe, it, expect, afterEach } from 'vitest';
18
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
19
+ import { tmpdir } from 'os';
20
+ import { join } from 'path';
21
+ // @ts-expect-error — plain ESM script, no type declarations.
22
+ import { makeConfigFilter, assertBundleNonEmpty } from '../../scripts/bundle-config.mjs';
23
+ describe('makeConfigFilter — relative-path exclusion', () => {
24
+ // Simulate a checkout whose location prefix contains excluded segments.
25
+ const SRC = '/usr/local/src/x/agent-config';
26
+ const filter = makeConfigFilter(SRC);
27
+ it('keeps the source root itself', () => {
28
+ expect(filter(SRC)).toBe(true);
29
+ });
30
+ it('keeps in-tree files despite a `local` segment in the prefix', () => {
31
+ // Regression: previously the `/usr/local/` prefix matched and excluded these.
32
+ expect(filter(`${SRC}/settings.json`)).toBe(true);
33
+ expect(filter(`${SRC}/hooks/git-mutation-guard.sh`)).toBe(true);
34
+ expect(filter(`${SRC}/agents/bar-raiser.md`)).toBe(true);
35
+ expect(filter(`${SRC}/skills/some-skill/SKILL.md`)).toBe(true);
36
+ });
37
+ it('still excludes the in-tree local/ overlay', () => {
38
+ expect(filter(`${SRC}/local`)).toBe(false);
39
+ expect(filter(`${SRC}/local/agents/internal.md`)).toBe(false);
40
+ });
41
+ it('still excludes in-tree node_modules and .git', () => {
42
+ expect(filter(`${SRC}/node_modules`)).toBe(false);
43
+ expect(filter(`${SRC}/node_modules/pkg/index.js`)).toBe(false);
44
+ expect(filter(`${SRC}/.git`)).toBe(false);
45
+ expect(filter(`${SRC}/.git/config`)).toBe(false);
46
+ });
47
+ it('still excludes co-located hook test suites (*.test.sh)', () => {
48
+ expect(filter(`${SRC}/hooks/git-mutation-guard.test.sh`)).toBe(false);
49
+ });
50
+ it('works when the source root has no excluded segment in its prefix', () => {
51
+ const cleanSrc = '/srv/build/agent-config';
52
+ const cleanFilter = makeConfigFilter(cleanSrc);
53
+ expect(cleanFilter(`${cleanSrc}/settings.json`)).toBe(true);
54
+ expect(cleanFilter(`${cleanSrc}/local/x.md`)).toBe(false);
55
+ });
56
+ });
57
+ describe('assertBundleNonEmpty — post-build guard', () => {
58
+ const dirs = [];
59
+ function tmp() {
60
+ const d = mkdtempSync(join(tmpdir(), 'bundle-assert-'));
61
+ dirs.push(d);
62
+ return d;
63
+ }
64
+ afterEach(() => {
65
+ while (dirs.length)
66
+ rmSync(dirs.pop(), { recursive: true, force: true });
67
+ });
68
+ it('throws on an empty output dir', () => {
69
+ expect(() => assertBundleNonEmpty(tmp())).toThrow(/missing or empty/);
70
+ });
71
+ it('throws when settings.json is empty', () => {
72
+ const d = tmp();
73
+ writeFileSync(join(d, 'settings.json'), '');
74
+ mkdirSync(join(d, 'hooks'));
75
+ writeFileSync(join(d, 'hooks', 'guard.sh'), '#!/bin/sh\n');
76
+ expect(() => assertBundleNonEmpty(d)).toThrow(/settings\.json is missing or empty/);
77
+ });
78
+ it('throws when hooks/ is missing', () => {
79
+ const d = tmp();
80
+ writeFileSync(join(d, 'settings.json'), '{}');
81
+ expect(() => assertBundleNonEmpty(d)).toThrow(/hooks.*missing or empty/);
82
+ });
83
+ it('throws when hooks/ is empty', () => {
84
+ const d = tmp();
85
+ writeFileSync(join(d, 'settings.json'), '{}');
86
+ mkdirSync(join(d, 'hooks'));
87
+ expect(() => assertBundleNonEmpty(d)).toThrow(/hooks.*missing or empty/);
88
+ });
89
+ it('passes for a populated bundle', () => {
90
+ const d = tmp();
91
+ writeFileSync(join(d, 'settings.json'), '{"ok":true}');
92
+ mkdirSync(join(d, 'hooks'));
93
+ writeFileSync(join(d, 'hooks', 'guard.sh'), '#!/bin/sh\n');
94
+ expect(() => assertBundleNonEmpty(d)).not.toThrow();
95
+ });
96
+ });
97
+ //# sourceMappingURL=bundle-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-config.test.js","sourceRoot":"","sources":["../../src/__tests__/bundle-config.test.ts"],"names":[],"mappings":"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"}
@@ -161,10 +161,17 @@ const MIRROR_PAIRS = [
161
161
  '.claude/hooks/coordination-brief.test.sh',
162
162
  'mx-agent-system/agent-config/hooks/coordination-brief.test.sh',
163
163
  ],
164
- // NOTE: agent-config/settings.json is intentionally NOT a mirror pair — the
165
- // worktree .claude/settings.json is COMPOSED/merged from the source, not a
166
- // byte-identical copy, so its SessionStart wiring is verified by composition,
167
- // not byte-identity.
164
+ // NOTE: agent-config/settings.json is intentionally NOT a byte-identity mirror
165
+ // pair — the worktree .claude/settings.json has its SessionStart/hook wiring
166
+ // COMPOSED/merged from the source at deploy time (see claude.ts), so the two
167
+ // are not byte-identical by design. But the SECURITY-SENSITIVE part — the
168
+ // `permissions` allow/deny/ask lists and the experimental-teams flag — must
169
+ // never drift, because config-sync/`mx-agent new` ship agent-config's copy
170
+ // into worktrees verbatim. A prior drift (agent-config/settings.json lagged
171
+ // active by the "Monitor" allow entry from #4275, and the git-mutation-guard
172
+ // #4289 hardening) shipped a weakened config to consumer worktrees (#4294).
173
+ // The `settings.json permissions parity` describe below guards that surface
174
+ // semantically rather than by byte-identity.
168
175
  ];
169
176
  describe('config mirror integrity', () => {
170
177
  for (const [claudeRel, agentConfigRel] of MIRROR_PAIRS) {
@@ -183,4 +190,43 @@ describe('config mirror integrity', () => {
183
190
  });
184
191
  }
185
192
  });
193
+ /**
194
+ * settings.json is composed (SessionStart/hook wiring is merged into the worktree
195
+ * copy at deploy time), so it is not a byte-identity mirror pair above. But its
196
+ * `permissions` block and the experimental-teams flag are shipped verbatim into
197
+ * consumer worktrees by config-sync/`mx-agent new`; drift there ships a weakened
198
+ * security posture. This suite asserts semantic parity of exactly those fields
199
+ * between the active runtime copy and the agent-config source of truth so a
200
+ * regression like #4294 (agent-config lagging the active allow-list) fails
201
+ * locally before it ships.
202
+ */
203
+ describe('settings.json permissions parity', () => {
204
+ const claudeSettingsAbs = join(REPO_ROOT, '.claude/settings.json');
205
+ const agentConfigSettingsAbs = join(REPO_ROOT, 'mx-agent-system/agent-config/settings.json');
206
+ const bothPresent = existsSync(claudeSettingsAbs) && existsSync(agentConfigSettingsAbs);
207
+ const loadPerms = (abs) => {
208
+ const parsed = JSON.parse(readFileSync(abs, 'utf-8'));
209
+ return parsed;
210
+ };
211
+ (bothPresent ? it : it.skip)('permissions.allow is identical between active and agent-config', () => {
212
+ const active = loadPerms(claudeSettingsAbs);
213
+ const template = loadPerms(agentConfigSettingsAbs);
214
+ expect(template.permissions?.allow).toEqual(active.permissions?.allow);
215
+ });
216
+ (bothPresent ? it : it.skip)('permissions.deny is identical between active and agent-config', () => {
217
+ const active = loadPerms(claudeSettingsAbs);
218
+ const template = loadPerms(agentConfigSettingsAbs);
219
+ expect(template.permissions?.deny).toEqual(active.permissions?.deny);
220
+ });
221
+ (bothPresent ? it : it.skip)('permissions.ask is identical between active and agent-config', () => {
222
+ const active = loadPerms(claudeSettingsAbs);
223
+ const template = loadPerms(agentConfigSettingsAbs);
224
+ expect(template.permissions?.ask).toEqual(active.permissions?.ask);
225
+ });
226
+ (bothPresent ? it : it.skip)('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS flag is identical between active and agent-config', () => {
227
+ const active = loadPerms(claudeSettingsAbs);
228
+ const template = loadPerms(agentConfigSettingsAbs);
229
+ expect(template.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toEqual(active.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS);
230
+ });
231
+ });
186
232
  //# sourceMappingURL=config-mirror-integrity.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-mirror-integrity.test.js","sourceRoot":"","sources":["../../src/__tests__/config-mirror-integrity.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-mirror-integrity.test.js","sourceRoot":"","sources":["../../src/__tests__/config-mirror-integrity.test.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  {
2
- "generation": 1,
2
+ "generation": 2,
3
3
  "description": "Monotonic agent-config generation. Bump when shipping agent-config changes that external projects should pull via `mx-agent config-sync`. The CLI stamps this into a project's config on init/config-sync and warns on `mx-agent start` when a project's generation is older than the CLI's bundled generation. Platform v2.35 Phase 3 (#4017)."
4
4
  }
@@ -41,17 +41,15 @@
41
41
  # (bash/sh/zsh/dash/ksh/eval/source/`.`) or pipes the body into one (`| ...sh`)
42
42
  # is KEPT and still scanned, because those bodies ARE executed and a destructive
43
43
  # git verb in them is real (e.g. `bash <<EOF ... git reset --hard ... EOF`).
44
- # KNOWN FALSE-POSITIVE CLASS (accepted friction, fails safe, GIT_EXPERT=1 clears
45
- # it): a blocked git verb quoted on a command line — e.g. `echo "run git reset
46
- # --hard"`, or a `gh issue create --body "... git reset --hard && git stash drop
47
- # ..."` filing whose body mentions destructive git after a separator still
48
- # matches and still blocks. Quoted content is scanned as-is: fixing this without
49
- # opening a bypass proved harder than the fix was worth. A quote-stripping pass
50
- # (item 4, #3934) was built and CUT after two adversarial security reviews found
51
- # fail-open regressions (executor-payload blanking; per-line sink scoping that
52
- # allowed `echo "hi" && ssh h 'git reset --hard'`); the redesign is tracked
53
- # separately. WORKAROUND for the issue/PR-body case: pass the prose via
54
- # `--body-file <path>` instead of an inline `--body "..."`. Known bypass classes
44
+ # QUOTED-CONTENT HANDLING (per-segment sink scoping, #3938): commands whose
45
+ # quoted text merely MENTIONS destructive git verbs (e.g. `echo "run git reset
46
+ # --hard"`, `gh issue create --body "... git stash ..."`) are correctly allowed.
47
+ # Sink commands (echo, printf, gh --body, git commit -m, etc.) have their quoted
48
+ # content blanked before scanning. Executor commands (bash, ssh, su, etc.) have
49
+ # their quoted content extracted and scanned as separate segments their args
50
+ # are commands, not data. Pipe chains propagate executor status backwards, so
51
+ # `echo "git reset --hard" | bash` is still blocked. Unknown commands keep
52
+ # quotes intact (fail safe = blocked if a pattern matches). Known bypass classes
55
53
  # (out of scope, NOT defended): indirection via a script file, shell
56
54
  # variable/`eval` assembly, or an alias can run a blocked op without a literal
57
55
  # match. The guard deters accidental discards; it does not stop a determined
@@ -110,6 +108,144 @@ strip_heredocs() {
110
108
  '
111
109
  }
112
110
 
111
+ # --- per-segment sink-scoped quote handling (#3938) ---------------------------
112
+ # Sink commands (echo, printf, gh --body, git commit -m, etc.) have their
113
+ # quoted content blanked so prose mentioning destructive git verbs doesn't
114
+ # false-positive. Executor commands (bash, ssh, su, etc.) have their quoted
115
+ # content EXTRACTED and emitted as additional segments for scanning (their
116
+ # quoted args are commands, not data). Per-segment scoping prevents the
117
+ # round-2 bypass where a sink on one segment blanked an executor's payload
118
+ # on the next. Pipe chains propagate executor status backwards: if stage N
119
+ # is an executor and connected by pipe to stage N-1, stage N-1's quotes
120
+ # are not blanked (its output feeds the executor). Fail-safe: unknown
121
+ # commands keep quotes intact (no blanking, no extraction).
122
+ strip_sink_quotes() {
123
+ awk '
124
+ function get_lead_cmd(seg, s) {
125
+ s = seg; gsub(/^[[:space:]]+/, "", s)
126
+ while (match(s, /^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+/))
127
+ s = substr(s, RSTART + RLENGTH)
128
+ while (match(s, /^[0-9]*[<>][>&]?[^[:space:]]*[[:space:]]+/))
129
+ s = substr(s, RSTART + RLENGTH)
130
+ if (match(s, /^[^[:space:]]+/)) return substr(s, RSTART, RLENGTH)
131
+ return ""
132
+ }
133
+ function is_exec(cmd) {
134
+ return (cmd == "bash" || cmd == "sh" || cmd == "zsh" || cmd == "dash" || \
135
+ cmd == "ksh" || cmd == "eval" || cmd == "source" || cmd == "ssh" || \
136
+ cmd == "su" || cmd == "doas" || cmd == "nsenter" || \
137
+ cmd == "sudo" || cmd == "xargs")
138
+ }
139
+ function check_sink(seg, cmd, s, sc) {
140
+ cmd = get_lead_cmd(seg)
141
+ if (is_exec(cmd)) return 0
142
+ if (cmd == "echo" || cmd == "printf" || cmd == "cat" || cmd == "tee" || cmd == "write")
143
+ return 1
144
+ if (cmd == "gh" && (seg ~ /--body/ || seg ~ /--title/ || seg ~ /--comment/))
145
+ return 1
146
+ if (cmd == "git") {
147
+ s = seg; gsub(/^[[:space:]]+/, "", s); sub(/^git[[:space:]]+/, "", s)
148
+ while (match(s, /^(-C[[:space:]]+[^[:space:]]+|-c[[:space:]]+[^[:space:]]+|--[a-z][-a-z0-9]*(=[^[:space:]]*)?)[[:space:]]+/))
149
+ s = substr(s, RSTART + RLENGTH)
150
+ if (match(s, /^[^[:space:]]+/)) {
151
+ sc = substr(s, RSTART, RLENGTH)
152
+ if ((sc == "commit" || sc == "tag" || sc == "notes") && \
153
+ (seg ~ /-[a-zA-Z]*m[[:space:]]/ || seg ~ /--message/))
154
+ return 1
155
+ }
156
+ }
157
+ return 0
158
+ }
159
+ function blank_quotes(seg, n, i, c, out, isq, idq) {
160
+ n = length(seg); out = ""; isq = 0; idq = 0
161
+ for (i = 1; i <= n; i++) {
162
+ c = substr(seg, i, 1)
163
+ if (isq) {
164
+ if (c == "'"'"'") { isq = 0; out = out c } else out = out " "
165
+ continue
166
+ }
167
+ if (idq) {
168
+ if (c == "\\") { out = out " "; i++; continue }
169
+ if (c == "\"") { idq = 0; out = out c } else out = out " "
170
+ continue
171
+ }
172
+ if (c == "'"'"'") { isq = 1; out = out c; continue }
173
+ if (c == "\"") { idq = 1; out = out c; continue }
174
+ out = out c
175
+ }
176
+ return out
177
+ }
178
+ function extract_quotes(seg, n, i, c, ct, isq, idq, res) {
179
+ n = length(seg); isq = 0; idq = 0; ct = ""; res = ""
180
+ for (i = 1; i <= n; i++) {
181
+ c = substr(seg, i, 1)
182
+ if (isq) {
183
+ if (c == "'"'"'") {
184
+ isq = 0
185
+ if (ct != "") { if (res != "") res = res "\n"; res = res ct }
186
+ ct = ""
187
+ } else ct = ct c
188
+ continue
189
+ }
190
+ if (idq) {
191
+ if (c == "\\") { ct = ct substr(seg, i+1, 1); i++; continue }
192
+ if (c == "\"") {
193
+ idq = 0
194
+ if (ct != "") { if (res != "") res = res "\n"; res = res ct }
195
+ ct = ""
196
+ } else ct = ct c
197
+ continue
198
+ }
199
+ if (c == "'"'"'") { isq = 1; continue }
200
+ if (c == "\"") { idq = 1; continue }
201
+ }
202
+ return res
203
+ }
204
+ {
205
+ line = $0; n = length(line)
206
+ isq = 0; idq = 0; num = 0; seg_s = 1
207
+ for (i = 1; i <= n; i++) {
208
+ c = substr(line, i, 1)
209
+ if (isq) { if (c == "'"'"'") isq = 0; continue }
210
+ if (idq) { if (c == "\\") { i++; continue }; if (c == "\"") idq = 0; continue }
211
+ if (c == "'"'"'") { isq = 1; continue }
212
+ if (c == "\"") { idq = 1; continue }
213
+ sp = 0; sl = 1; st = ""
214
+ if (c == ";") { sp = 1; st = "seq" }
215
+ else if (c == "&" && i < n && substr(line, i+1, 1) == "&") { sp = 1; sl = 2; st = "seq" }
216
+ else if (c == "|" && i < n && substr(line, i+1, 1) == "|") { sp = 1; sl = 2; st = "seq" }
217
+ else if (c == "|") { sp = 1; st = "pipe" }
218
+ if (sp) {
219
+ num++; segs[num] = substr(line, seg_s, i - seg_s); stypes[num] = st
220
+ seg_s = i + sl; if (sl == 2) i++
221
+ }
222
+ }
223
+ num++; segs[num] = substr(line, seg_s); stypes[num] = ""
224
+ # classify each segment
225
+ for (s = 1; s <= num; s++) { snk[s] = check_sink(segs[s]); pxe[s] = 0 }
226
+ # backward-propagate executor status through pipes
227
+ for (s = num; s >= 2; s--) {
228
+ if (stypes[s-1] == "pipe") {
229
+ cmd = get_lead_cmd(segs[s])
230
+ if (is_exec(cmd) || pxe[s]) { snk[s-1] = 0; pxe[s-1] = 1 }
231
+ }
232
+ }
233
+ # emit processed segments
234
+ for (s = 1; s <= num; s++) {
235
+ if (snk[s]) { print blank_quotes(segs[s]) }
236
+ else {
237
+ print segs[s]
238
+ cmd = get_lead_cmd(segs[s])
239
+ if (is_exec(cmd) || pxe[s]) {
240
+ ex = extract_quotes(segs[s])
241
+ if (ex != "") print ex
242
+ }
243
+ }
244
+ }
245
+ }
246
+ '
247
+ }
248
+
113
249
  SCAN=$(printf '%s' "$COMMAND" | strip_heredocs)
114
250
 
115
251
  refuse() {
@@ -161,21 +297,19 @@ refuse() {
161
297
  # mx-guard-hook.sh. Applied per-segment to the heredoc- and quote-stripped SCAN.
162
298
  # GIT_EXPERT=1 clears any accepted false positive. `echo` is deliberately NOT a
163
299
  # wrapper word, so `echo "use timeout 5 git reset --hard"` stays allowed.
164
- GITSEG='(^|[;&|({]|\s(then|do|else)\s)\s*([0-9]*[<>][^[:space:]]*\s+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*\s+|(sudo|env|nice|nohup|time|timeout|ionice|stdbuf|command|xargs|setsid)\s+([0-9]+\s+)?)*git(\s+(-C\s+[^[:space:];&|]+|-c\s+[^[:space:];&|]+|--(git-dir|work-tree|namespace|super-prefix)(=[^[:space:];&|]*|\s+[^[:space:];&|]+)|--[A-Za-z][A-Za-z0-9-]*=[^[:space:];&|]*|--[A-Za-z][A-Za-z0-9-]*|-[A-Za-z]))*\s+'
300
+ GITSEG='(^|[;&|({`]|\s(then|do|else)\s)\s*([0-9]*[<>][^[:space:]]*\s+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*\s+|(sudo|env|nice|nohup|time|timeout|ionice|stdbuf|command|xargs|setsid|doas|nsenter)\s+([0-9]+\s+)?)*git(\s+(-C\s+[^[:space:];&|]+|-c\s+[^[:space:];&|]+|--(git-dir|work-tree|namespace|super-prefix)(=[^[:space:];&|]*|\s+[^[:space:];&|]+)|--[A-Za-z][A-Za-z0-9-]*=[^[:space:];&|]*|--[A-Za-z][A-Za-z0-9-]*|-[A-Za-z]))*\s+'
165
301
 
166
- # --- per-segment evaluation (item 3, #3934) ---------------------------------
302
+ # --- per-segment evaluation (item 3, #3934; sink scoping, #3938) --------------
167
303
  # Each allow-check (stash list|show; restore --staged-without-worktree) must be
168
304
  # scoped to the SAME command segment as the mutating verb it whitelists.
169
305
  # Whole-command allow-checks let a read-only first segment green-light a
170
306
  # destructive second one: `git stash list && git stash drop` or
171
307
  # `git restore --staged f && git restore f` slipped through because the
172
- # allow-form matched elsewhere on the line. Split SCAN on the shell separators
173
- # `;` `&` `|` and newlines (heredoc bodies were already emitted line-per-line by
174
- # strip_heredocs) and run the full verb logic on each segment independently.
175
- # `&&`/`||` collapse to empty segments (skipped). refuse() exits, so the first
176
- # offending segment blocks. NOTE: a separator inside a quoted string still
177
- # splits here (quoted content is scanned as-is — see the KNOWN FALSE-POSITIVE
178
- # CLASS note in the header); that fails safe (blocks), which is intentional.
308
+ # allow-form matched elsewhere on the line. strip_sink_quotes splits SCAN on
309
+ # unquoted shell separators (`;` `&&` `||` `|`), blanks quoted content in sink
310
+ # segments, extracts quoted content from executor segments as additional lines,
311
+ # and emits one segment per line. refuse() exits, so the first offending
312
+ # segment blocks.
179
313
  check_segment() {
180
314
  local seg="$1"
181
315
 
@@ -214,6 +348,6 @@ check_segment() {
214
348
  while IFS= read -r _seg; do
215
349
  [[ -n "$_seg" ]] || continue
216
350
  check_segment "$_seg"
217
- done < <(printf '%s\n' "$SCAN" | tr ';&|' '\n')
351
+ done < <(printf '%s\n' "$SCAN" | strip_sink_quotes)
218
352
 
219
353
  exit 0
@@ -136,7 +136,8 @@
136
136
  "WebFetch",
137
137
  "WebSearch",
138
138
  "Task",
139
- "NotebookEdit"
139
+ "NotebookEdit",
140
+ "Monitor"
140
141
  ],
141
142
  "deny": [
142
143
  "Bash(rm -rf /*)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memnexus-ai/mx-agent-cli",
3
- "version": "0.1.196",
3
+ "version": "0.1.198",
4
4
  "description": "CLI for creating and managing AI agent teams",
5
5
  "type": "module",
6
6
  "bin": {