You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Changed
The Guardrails card is the active workspace's, and no longer offers a
client picker (#790). The card added in #786 carried a <select> of
clients above the currency dropdown on a backend that reported two or
more, and set one client's STRATEGY.md at a time. That was the wrong
shape at both ends. A single-workspace install has exactly one
STRATEGY.md, so the picker, the /api/reports/clients fetch and the
per-client query parameter existed for a case it never has; and where
there IS a roster, one dropdown reads as "this setting has one value"
while silently belonging to whichever client it happens to be on. The
card now reads and writes the active workspace's file and nothing else,
and a backend that declares a client roster is served no card at all —
the markup is cut out of app.html before the response leaves the
server (new mureo/web/app_html.py), so a script that fails to load
cannot leave a live control on screen writing the operator's own ambient
workspace, which is no client's file. Per-client currency belongs beside
the other per-client settings, on the client's own edit form. The GET / POST /api/strategy/currency routes are unchanged and still
honour client=: that is the seam the multi-client layer resolves each
client's own STRATEGY.md through.
Fixed
A secret pasted into an ordinary tool argument was written verbatim to JOURNAL.jsonl and plugin_audit.jsonl (#779). mask_arguments masked
by KEY name only: api_key as a key became ***, but the same
credential sitting inside a free-text value — an action_log entry's reason, a note, an item in a list — survived intact in both trails,
while that very sentence WAS scrubbed on its way into STATE.json. Two
stores, two rules. Every surviving string value now goes through the
shared scrub_text as well, before it is truncated: Basic <base64>
matches only base64, so a credential cut by the truncation marker would be
unrecognisable and written in cleartext. Key masking, the 512-character
cap, the list cap and the depth guard are unchanged. Note for
callers of the public mask_arguments: a top-level bare string used
to pass through untouched and is now scrubbed like any nested one.
The code= rule no longer fires on tool arguments (#779). It was
written for error prose, where code= with no space is the query-string
shape an authorization code leaks in; applied to arguments it rewrote the
landing page an agent actually submitted
(…?promo_code=SUMMER2026 → …?promo_code=*** in a final_url). The
rule is unchanged everywhere it was meant to apply — reason, rationale, decision records and every scrubbed error string still lose
an authorization code, including a reason that arrives as a tool
argument.
Scrubbing is bounded (#779). Every recorded string is cut to 512
characters, but the scrubber used to read all of it first: ~250 ms per
megabyte, on the event loop for arguments and on the request thread for
the configure wizard, for output that was 512 characters either way. It
now reads 512 + STRADDLE_MARGIN (64) characters and no more — 2 MB
drops from ~500 ms to ~0.15 ms. The margin is what makes the window safe
rather than merely cheap: a credential only leaves something behind if it
STARTS before the cut, and the longest shape the scrubber must see to
recognise one is Basic plus its 16-character minimum value. Applies to
argument values, to a journal reason / rationale, to a plugin error, and to the wizard's Amazon failure detail. A journal reason
is the case that forced it: an invalid_args reason is a jsonschema
message, and jsonschema embeds the rejected instance in it, so that
string was as long as the arguments the caller sent.
client, tool and source are capped at 512 characters in JOURNAL.jsonl and plugin_audit.jsonl (#779). client is the MCP
client's self-declared clientInfo — unbounded external input on an
append-only, line-oriented file. An unreported client still records as null. This bounds every field, not the line: the number of argument
keys is still uncapped, so 20,000 ordinary arguments write a ~10 MB line
as they did before. Capping the key count changes what is recorded and is
left to its own change.
An argument named private_key was journalled in cleartext (#779).
The key-name masker matches as a substring, which hid the gap: app_secret
and even appsecret_proof were covered all along via secret, but private_key matched nothing — api_key needs the literal api. That is
the field name in a Google service-account JSON and its value is a PEM
private key. private_key / privateKey, pwd (not a substring of passwd) and signature now mask like every other credential field. sig is deliberately not a root: substring matching would collapse design, assign and signal to ***, value and all.
The scrubber's key list now matches the one the argument masker uses
(#779). The KEY path masks an argument whose name merely contains token / secret / password / credential / cookie; the VALUE path
knew seven exact spellings, so the identical credential was redacted as an
argument key and written in cleartext inside a string. app_secret —
mureo's own Meta credential field — appSecret, secret_key, private_key, auth_token, authToken, id_token, a bare token=, passwd, pwd, credential(s), signature, aws_secret_access_key, bearer and Set-Cookie: all leaked; they no longer do. A root matches
the END of a key and never consumes the prefix, so app_secret=… still
reads app_secret=***. key is deliberately not a root (monkey=, keyword=) and neither is sig (design=); bare token carries a
minimum value length so token limit: 128000 and max_tokens=4096
survive. Expect more over-masking in free text — a long nextPageToken
or a prose credentials: … now becomes ***. That is the trade this
scrubber is declared to make: over-masking costs legibility, under-masking
costs a credential. Inside a tool ARGUMENT the one-word keys need machine
punctuation (see the next entry), so a space-padded Set-Cookie: sid=…
is redacted in an error message but not in an argument value; an argument namedcookie is still *** from the key path alone.
The scrubber no longer rewrites ad copy (#779). Adding those one-word
keys made secret, password, signature and the rest fire on a
space-padded colon, and a colon is how a sentence is punctuated: headline, description, primary_text and campaign name are the
most-written arguments in this product. "The secret: better ROAS" was
recorded as "The secret: ***", "Password: A Novel" as "Password: ***", and the journal stopped being able to answer what the agent
submitted. scrub_text now takes an explicit mode — PROSE for an
error message, a reason or a rationale, ARGUMENT for a tool argument
value — and in ARGUMENT mode a one-word key needs an = or a quoted
dict key ("password": "…"). Compound keys (client_secret, api_key, developer-token, …) are unchanged in both modes: nobody writes client_secret: in a headline. Prose scrubbing is byte-for-byte what the
rest of this fix produces — verified over every distinct string literal
in the repository (27,534 of them at this commit; the corpus is the
repository's own source, so the figure moves). Against v0.21.1 it
redacts more in 72 of them, always more and never less, and that is the
key coverage above rather than the mode split.
A reason argument was scrubbed by two different rules (#779). Three
built-in tools declare a reason parameter of their own
(mureo_state_action_log_append and the two not_collected_set tools),
and for those the sentence stays in args instead of becoming the
journal's rationale. It was therefore scrubbed as an ARGUMENT on the
journal line and as PROSE on its way into STATE.json — the same
sentence, two stores, two answers, which is the divergence this issue
exists to close. mask_arguments now scrubs text under reason or rationale as prose, whatever tool it came from, including a plugin's,
and the mode carries down the recursion. A plugin is free to declare reason as a list or an object, and reading the mode off the immediate
value's type left {"reason": ["password: hunter2hunter2"]} recorded as
submitted while the identical sentence one level up was masked.
SECURITY.md overstated the scrubber's guarantee (#779). It said a
credential pasted into an ordinary free-text argument "is redacted", full
stop. It is redacted when it takes a shape the scrubber recognises; a
credential in prose with no recognisable key and no prefix ("the api key
is …") is not detected. The section now says so, and also documents what
it never had: the prose/argument split, the code= rule, and the
16-character minimum on a Basic value. A reader has to be able to
predict what their own JOURNAL.jsonl will contain.
The three Amazon error paths scrub a bounded prefix too (#791). #779
bounded four call sites and missed these: the failure body the bridge hands
the agent — on the MCP dispatch path, where a 2 MB body cost ~480 ms of
regex work and now costs ~4 ms — and the str(exc) reported by the session
credential seam and by mureo amazon refresh-manifest. The two error-text
paths are cut to 512 characters, the budget the configure wizard already
applies to the very same failures. The bridge's is different in kind: its
redactor must run before the body is flattened into API error: <code>: <message>, because flattening deletes the "code":
anchor the authorization-code rule keys on — so the cap lands on the raw
JSON, at 16000 characters, 4x the 4000-character budget of the text
flattening renders out of it (flattening only ever shortens). Every body
that flattened before still flattens. One past the cap is cut before it can
be parsed, and instead of passing that fragment off as Amazon's own wording
it now arrives as Amazon returned an oversized error body; scrubbed prefix: ….