@memnexus-ai/mx-agent-cli 0.1.196 → 0.1.197
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/dist/__tests__/config-mirror-integrity.test.js +50 -4
- package/dist/__tests__/config-mirror-integrity.test.js.map +1 -1
- package/dist/agent-config/.mx-config-generation.json +1 -1
- package/dist/agent-config/hooks/git-mutation-guard.sh +155 -21
- package/dist/agent-config/settings.json +2 -1
- package/package.json +1 -1
|
@@ -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
|
|
165
|
-
// worktree .claude/settings.json
|
|
166
|
-
//
|
|
167
|
-
// not byte-
|
|
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":
|
|
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
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# --hard"`,
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
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.
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
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" |
|
|
351
|
+
done < <(printf '%s\n' "$SCAN" | strip_sink_quotes)
|
|
218
352
|
|
|
219
353
|
exit 0
|