Skip to content

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

Merged
virtualsteve-exa merged 5 commits into
devfrom
fix/tracker-cheap-five
Sep 6, 2026
Merged

virtualsteve-exa merged 5 commits into
devfrom
fix/tracker-cheap-five

Conversation

@virtualsteve-exa

Copy link
Copy Markdown
Collaborator

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 through gen_identity.py; GitHub retargets to dev when #144 merges. It also touches SKILL.md, installation.md, security-guardrails.md and preflight.sh, all of which #148 touches in other places — whichever of this and #148 merges second takes a small rebase.

Issue Change Test
#143 two ungoverned MCP tools exabeam_analytics_rule_details → allow (a read; note its bare-string arg0). exabeam_create_analytics_rule → deny on both hosts under both spellings — it creates a detection rule server-side, and rule-tuning is proposals-only; exabeam_update_analytics_rule denied ahead of the MCP exposing it. Documented in containment-tools.md beside 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 → 22
#91 no decline / abandon clause Two paragraphs in soc-investigate's governance: a declined or unanswered dismiss/close is over — no retry, no reaching the same outcome by another route (a create_case with 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_doing
#86 manual-MCP path undisclosed The "wire it manually" section now says that screening, the neutralizer and the audit trail all live in the bridge and none run on a direct registration; only the dismiss/close gate survives, and only for a server named exabeam. Same statement on the guardrails page. test_manual_mcp_path_discloses_what_it_forgoes
#139 preflight TOML spellings codex_write_override() now sees the section-header, dotted-key and inline-table spellings of a per-tool approval_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 flagged

One 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

@virtualsteve-star virtualsteve-star left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

https://claude.ai/code/session_01DujmYs2KT2YspSKkW1G1Hp

Comment thread plugin/preflight.sh Outdated
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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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".

Comment thread plugin/preflight.sh Outdated
/^[[:space:]]*\[/ { sec = $0; next }
{
ctx = sec " " $0
if (ctx ~ /exabeam_update_(alert|case)/ && $0 ~ /approval_mode/) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).

Comment thread security/aibom.cdx.json Outdated
{
"name": "mcp:toolCount",
"value": "21"
"value": "24"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread plugin/docs/installation.md Outdated
**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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 "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.

Comment thread CHANGELOG.md

## [Unreleased]

### Changed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Unreleased] now reads Changed / Fixed / Changed — duplicate heading. Merge the two Changed blocks.

@virtualsteve-exa

Copy link
Copy Markdown
Collaborator Author

Review findings addressed in the commit above. The false green is fixed at the root: codex_write_override now walks every gated tool on a line inside its own balanced {…} instead of anchoring on the first match and cutting at the first }; comment lines are skipped and single-quoted 'approve' is approve. Your seven configs plus the nested-table case are fixtures now (all pass in the intended direction), and the test runs in its temp dir. AI-BOM tool count is 23 with a stated basis and a header cross-check in the generator; the manual-MCP docs now say the deny tier survives under the manual prefix; the bare-string arg0 exception is in the convention block; the duplicate Changed heading is merged. Suite 462 passed.

@virtualsteve-star virtualsteve-star left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

https://claude.ai/code/session_01DujmYs2KT2YspSKkW1G1Hp

Comment thread plugin/preflight.sh Outdated
next
}
s = line # inline table(s)
while (match(s, /exabeam_update_(alert|case)[[:space:]]*=[[:space:]]*\{/)) {

@virtualsteve-star virtualsteve-star Sep 6, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

virtualsteve-exa and others added 5 commits September 6, 2026 10:55
…, #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
@virtualsteve-exa

Copy link
Copy Markdown
Collaborator Author

Round-2 finding addressed, and taken the way you recommended: codex_write_override now parses the file with tomllib wherever python3 has it and walks the tree for any gated tool whose approval_mode is anything but approve — quoted keys, dotted keys, inline tables and section headers all resolve the same way, so there is no spelling left for a regex to miss. A file the parser rejects returns unverifiable, which preflight reports as a warning ("cannot verify"), never as gate ON. The awk scan stays only as the fallback for a host without tomllib, with the quoted-key form admitted.

Both of your configs, through both engines:

config parser awk fallback
tools = { "exabeam_update_case" = { approval_mode = "never" }, "other" = { approval_mode = "approve" } } yes yes
tools = {"exabeam_update_case"={approval_mode="never"},"o"={approval_mode="approve"}} yes yes
both gated tools in one table, second loosened (round 1) yes yes
clean section-header approve no no

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.

@virtualsteve-exa
virtualsteve-exa merged commit 2573301 into dev Sep 6, 2026
5 checks passed
virtualsteve-exa added a commit that referenced this pull request Sep 6, 2026
… guide and AI-BOM regenerated

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q9UohWfy2scPhr4pavhqd
@virtualsteve-exa
virtualsteve-exa deleted the fix/tracker-cheap-five branch September 6, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants