Repository navigation
skills + docs + preflight: tier the two new MCP tools, say what to stop doing, disclose the manual-MCP gap, see every Codex TOML spelling (#91, #86, #139, #143) - #150
Conversation
virtualsteve-star
left a comment
There was a problem hiding this comment.
Automated review of fix/tracker-cheap-five (18 files, based on refactor/identity-single-source — the #144 findings ride underneath). The mechanical parts are in good shape: the full suite passes (448) and gen_identity.py reproduces the committed generated artifacts, so identity and manifests are in sync.
The headline: the #139 fix has a #139-class hole, and it fails in the dangerous direction. I ran the new codex_write_override() against seven configs:
| config | result | wanted |
|---|---|---|
| two tools in one inline table, second loosened | no |
yes ← false green |
| single tool inline, loosened | yes |
yes ✅ |
| dotted key | yes |
yes ✅ |
| section header | yes |
yes ✅ |
| comment mentioning the tool | yes |
no |
single-quoted 'approve' (legal TOML) |
yes |
no |
clean "approve" |
no |
no ✅ |
The three new spellings #139 asked for do work. But a sibling tool in the same inline table masks a loosened one, which is a false "gate ON" on a genuinely loosened gate — the exact failure class the function exists to prevent, and the direction this repo's own docs call the dangerous one. Details inline.
The other two misses are false alarms (safe direction), but they'll train operators to ignore the warning, which erodes the control a different way.
Remaining findings inline. One has no anchor — plugin/skills/soc-investigate/reference/tool-map.md:16: the "read this first" convention block still says every read wraps its object under arg0, while the new bare-string arg0 exception is noted only further down. That ordering matters because SKILL.md's documented recovery ("on a schema error, swap arg0↔arg1 first") can never fix a bare-string mismatch — the agent will swap, fail again, and have no next move. Worth hoisting the exception into the convention block itself.
🤖 Generated with Claude Code
| ctx = sec " " $0 | ||
| if (ctx ~ /exabeam_update_(alert|case)/ && $0 ~ /approval_mode/) { | ||
| s = $0 | ||
| if (match(s, /exabeam_update_(alert|case)/)) { s = substr(s, RSTART); sub(/\}.*/, "", s) } |
There was a problem hiding this comment.
🔴 A sibling tool in the same inline table masks a loosened one — false green, reproduced.
tools = { exabeam_update_alert = { approval_mode = "approve" }, exabeam_update_case = { approval_mode = "never" } }→ codex_write_override returns no, so preflight reports the gate ON while update_case closes cases unattended.
Cause: match() anchors on the first exabeam_update_(alert|case) on the line, then sub(/\}.*/, "", s) truncates at the first } — which is the end of the first tool's table. The scan never sees the second tool. The comment directly above claims the opposite ("scoped to the named tool's own {…}, so a sibling tool's 'approve' cannot mask it"), so this reads as intent that didn't survive the implementation.
A single inline table with one loosened tool is caught, which is presumably how this passed testing. Suggest looping over every exabeam_update_(alert|case) occurrence on the line rather than anchoring on the first — or, given this is now the third TOML-shape bug in this function, parsing with tomllib (python3 is already a hard preflight dependency as of this PR) and treating unparseable as "cannot verify".
| /^[[:space:]]*\[/ { sec = $0; next } | ||
| { | ||
| ctx = sec " " $0 | ||
| if (ctx ~ /exabeam_update_(alert|case)/ && $0 ~ /approval_mode/) { |
There was a problem hiding this comment.
🟠 The section gate was dropped, so prose and legal-but-unquoted spellings trigger false alarms.
ctx = sec " " $0 means any line mentioning a gated tool name and approval_mode counts, regardless of which section it's in — or whether it's even a setting. Both reproduced:
# note: exabeam_update_alert approval_mode should stay at approve→ yes (it's a comment)
[mcp_servers.exabeam.tools.exabeam_update_alert]
approval_mode = 'approve'→ yes (single quotes are legal TOML for the same value the double-quoted form treats as clean)
Both fail safe, so neither is urgent on its own. But an operator whose correctly-configured file keeps reporting "overridden" learns to ignore the warning — and then misses the real one. Skip comment lines, and accept both quote styles in the "approve" test.
|
|
||
| def _detect(tmp_path, toml): | ||
| home = tmp_path / "codex"; home.mkdir(); (home / "config.toml").write_text(toml) | ||
| r = subprocess.run(["bash", "-c", f"source '{PREFLIGHT}'; codex_write_override"], |
There was a problem hiding this comment.
🟡 The subprocess inherits pytest's cwd, so ./.codex/config.toml can leak into every case.
codex_write_override searches two paths, and the second is relative. With the subprocess running in pytest's working directory, a developer (or CI job) that happens to have a .codex/config.toml in the repo root gets it consulted in every test — silently changing results in a suite whose whole job is pinning this function's behavior. Pass cwd=str(tmp_path).
| { | ||
| "name": "mcp:toolCount", | ||
| "value": "21" | ||
| "value": "24" |
There was a problem hiding this comment.
🟡 mcp:toolCount is 24 while tool-map.md's own header says 23.
The count is regex-derived from tool-map.md, and the newly added exabeam_update_analytics_rule — proxy-only, not live-exposed — is picked up by the regex but deliberately excluded from the prose count. The BOM is the provenance artifact, so it should either match the document it derives from or state which basis it uses. Worth excluding proxy-only entries from the count regex, since the same drift will recur with the next proxy tool.
| **all three of socxen's deterministic controls live in the bundled bridge** — input screening of | ||
| telemetry, the write-side neutralizer that masks secrets and de-fangs links in what socxen writes, and | ||
| the audit trail — and **none of them run when Claude Code talks to the remote MCP directly.** The | ||
| dismiss/close gate still applies if you name the server `exabeam` (the hook matches it); nothing else |
There was a problem hiding this comment.
🟠 "Nothing else does" contradicts what permissions.json actually ships.
This line (and security-guardrails.md:81's "Only the dismiss/close gate survives") tells the hand-wired operator that nothing but the dismiss/close gate carries over. But permissions.json emits its deny tier under the manual prefix precisely so the containment block — and now the rule-write block — holds on that path.
The docs undersell a protection that exists, which is the safer direction to be wrong in, but it's still wrong: an operator reading this could reasonably conclude they need to hand-merge containment denials that are already covered.
|
|
||
| ## [Unreleased] | ||
|
|
||
| ### Changed |
There was a problem hiding this comment.
🟡 [Unreleased] now reads Changed / Fixed / Changed — duplicate heading. Merge the two Changed blocks.
|
Review findings addressed in the commit above. The false green is fixed at the root: |
781b648 to
c2046ef
Compare
virtualsteve-star
left a comment
There was a problem hiding this comment.
Re-review of c2046ef1
The three findings from round 1 are fixed. I re-ran the same seven configs against the new codex_write_override:
| config | before | now |
|---|---|---|
| sibling in inline table, second loosened | no ❌ |
yes ✅ |
| comment mentioning the tool | yes ❌ |
no ✅ |
single-quoted 'approve' |
yes ❌ |
no ✅ |
| dotted key / section header / single inline / clean approve | ✅ | ✅ |
The balanced-brace walker is the right structural fix, and skipping comment lines plus accepting both quote styles closes the two false alarms. # loosened trailing comments and both tools loosened also behave correctly.
But the same bug class survives through the quoted-key path, and it is still a false green. Details inline — one new finding, everything else clear.
For the record, one vector I flagged internally and then cleared: a multi-line inline table (tools = { exabeam_update_case = {\n approval_mode = "never" \n} }) does bypass the line-based awk, but tomllib rejects it as invalid TOML 1.0 — inline tables must be single-line — so Codex's parser would reject it too. Not a real risk, no action needed.
🤖 Generated with Claude Code
| next | ||
| } | ||
| s = line # inline table(s) | ||
| while (match(s, /exabeam_update_(alert|case)[[:space:]]*=[[:space:]]*\{/)) { |
There was a problem hiding this comment.
🔴 A quoted key bypasses the brace walker, and a sibling's approve then masks the loosened tool — same false green as round 1, different door.
Both regexes require the tool name to be followed directly by optional whitespace then =. With a quoted key there is a " in between, so match() never fires, the walker never runs, and line 204's guard also fails — sending the line to the whole-line loose() fallback, which any sibling approval_mode = "approve" on that line satisfies.
Reproduced against c2046ef1:
tools = { "exabeam_update_case" = { approval_mode = "never" }, "other" = { approval_mode = "approve" } }→ no (want yes)
tools = {"exabeam_update_case"={approval_mode="never"},"o"={approval_mode="approve"}}→ no (want yes)
I confirmed both are valid TOML 1.0 — tomllib parses each to approval_mode='never', i.e. a genuinely loosened dismiss/close gate that preflight reports as ON. Quoted keys are ordinary TOML that a config generator or a careful hand-editor may well emit.
Note tools = { "exabeam_update_case" = { approval_mode = "never" } } alone returns yes — the fallback catches it when nothing masks it. That's why this survived the round-1 fix: it only fails when a sibling supplies the masking "approve", which is exactly the shape round 1 was about.
Allowing an optional quote in both patterns — ["']?exabeam_update_(alert|case)["']?[[:space:]]*=[[:space:]]*\{ — closes it.
Standing recommendation: this is the fourth bug of this kind in the same function. Every fix so far has been another regex, and each time a different valid TOML spelling slipped past it. Parse the file with tomllib instead — this PR already makes python3 a hard preflight dependency, so it adds nothing — and when the file won't parse, report "cannot verify" rather than "gate ON". That's how the rest of the repo already handles a gate state it can't confirm.
c2046ef to
98e41c3
Compare
…, #143; #104 already fixed) - #143: exabeam_analytics_rule_details -> allow; exabeam_create_analytics_rule (and update_) -> deny on both hosts under both spellings, documented in containment-tools.md beside the containment verbs so the deny-list invariant covers them; tool map at 23 live tools with a "Never" section; the skill, READMEs and remit stop saying "there is no rule-write path". Generated: snippet, manifests, Codex map (38 disabled), AI BOM. - #91: soc-investigate's governance now says what to STOP doing — a declined/unanswered dismiss or close is over (no retry, no other route), and out-of-lane requests are declined and handed back. - #86: the manual-MCP registration path states that screening, neutralization and the audit trail all live in the bridge and do not run there; the guardrails page says the same. - #139: preflight's Codex approval-mode override check sees the section-header, dotted-key and inline-table spellings, scoped to the named tool; fixture-driven tests for five loosenings and four intact configs. Stacked on refactor/identity-single-source (#144) so the tiering goes through the identity generator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd # Conflicts: # plugin/docs/security-guardrails.md # plugin/skills/soc-investigate/reference/tool-map.md # security/aibom.cdx.json # security/aibom.html # tests/test_repo_invariants.py # Conflicts: # security/aibom.cdx.json # security/aibom.html
…ts own braces; comments and quote styles; the deny tier survives the manual path
- codex_write_override walked to the FIRST gated tool on a line and cut at the first '}', so
`tools = { exabeam_update_alert = { approval_mode = "approve" }, exabeam_update_case = { approval_mode = "never" } }`
read as gate ON while update_case ran unattended — a false green, the dangerous direction (found in
review). Every gated tool on the line is now walked inside its own balanced {…}. Comment lines are
skipped and 'approve' in single quotes is approve, so a correct config stops crying wolf. Seven more
fixtures pin it; the test runs in its temp dir so a ./.codex/config.toml in the checkout can't leak in.
- The AI-BOM tool count excludes names denied ahead of the MCP exposing them, states its basis, and the
generator refuses to run when the count disagrees with tool-map.md's own header (24 vs 23).
- The manual-MCP docs no longer say "nothing else survives": the deny tier (containment, rule writes) is
spelled under the manual prefix and holds there once the snippet is merged.
- tool-map's calling-convention block carries the bare-string arg0 exception, where the arg0/arg1 swap
advice cannot reach it. The changelog's duplicate Changed heading is merged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
…ed tool" test uses a name #143 did not classify Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
… not parse is "cannot verify", never gate ON
Fourth regex bypass in one function (review, 2026-09-06): a quoted key ("exabeam_update_case" = {...})
matched neither pattern, so the line fell to the whole-line check and a sibling's "approve" masked the
loosened tool — a false green. Wherever python3 has tomllib the file is now parsed and the tree walked
for any gated tool whose approval_mode is anything but approve (quoted keys, dotted keys, inline and
section spellings all resolve the same way); a file the parser rejects reports "unverifiable", which
preflight shows as a warning. The awk scan stays as the fallback for a host without tomllib, with the
quoted-key form admitted. Every fixture runs through both engines and must agree; the reviewer's two
quoted-key configs and a section-header quoted key are fixtures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
98e41c3 to
a7d742d
Compare
|
Round-2 finding addressed, and taken the way you recommended: Both of your configs, through both engines:
Every fixture is parametrized over both engines and must agree; a test pins that the parser engine is actually the one exercised under pytest. Suite 533 passed. |
… guide and AI-BOM regenerated Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
Closes #91, closes #86, closes #139, closes #143. The four cheap, unowned items from the 2026-09-05 tracker sweep, each a one-file change with a test; #104 turned out to be already fixed and is closed separately.
Stacked on #144 (
refactor/identity-single-source) so the tiering change goes throughgen_identity.py; GitHub retargets todevwhen #144 merges. It also touchesSKILL.md,installation.md,security-guardrails.mdandpreflight.sh, all of which #148 touches in other places — whichever of this and #148 merges second takes a small rebase.exabeam_analytics_rule_details→ allow (a read; note its bare-stringarg0).exabeam_create_analytics_rule→ deny on both hosts under both spellings — it creates a detection rule server-side, andrule-tuningis proposals-only;exabeam_update_analytics_ruledenied ahead of the MCP exposing it. Documented incontainment-tools.mdbeside the containment verbs so the deny-list invariant derives them; tool map at 23 live tools with a Never section; the skill, both READMEs and the remit stop saying "there is no rule-write path". Generated artifacts regenerated (snippet, manifests, Codex map now 38 disabled, AI BOM).test_rule_write_tools_are_denied_and_documented; governed count 21 → 22soc-investigate's governance: a declined or unanswered dismiss/close is over — no retry, no reaching the same outcome by another route (acreate_casewith a closed stage, a note asserting closure) — and out-of-lane requests are declined and handed back. Matches remit R-53 and the lane rule; the c03 grader already checks the workaround path this names.test_skill_says_what_to_stop_doingexabeam. Same statement on the guardrails page.test_manual_mcp_path_discloses_what_it_forgoescodex_write_override()now sees the section-header, dotted-key and inline-table spellings of a per-toolapproval_mode, scoped to the named tool so a sibling's"approve"cannot mask it.tests/test_preflight_codex_override.py: five loosenings detected, four intact configs not flaggedOne thing to settle at the #148 rebase: the hook's deny reason currently reads "…is a containment action"; with rule writes on the deny tier it should say "…is a containment action or a detection-content write". Cosmetic, one string.
🤖 Generated with Claude Code
https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd