Skip to content

fix(codescripts): name the real character in a script-name refusal - #1728

Merged
Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/codescripts-invalid-name-message
Oct 22, 2026
Merged

Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/codescripts-invalid-name-message

Conversation

@rudycelekli

Copy link
Copy Markdown
Contributor

Pull Request

Description

The refusal names a character that is not in the name

ValidateName walks the name byte by byte — correctly, that loop is the confinement boundary — but reported the offending character as string(name[i]). Converting a byte to a string yields the code point of the same value, so the first byte of 語 (0xE8) was rendered as è:

before: invalid script name "語": character "è" is not allowed (names are 1-64 characters of A-Z, a-z, 0-9, '-' or '_' …)
after:  invalid script name "語": character "語" is not allowed (names are 1-64 characters of A-Z, a-z, 0-9, '-' or '_' …)

A caller is told to remove a character it never sent. A byte that begins no valid rune is not a character at all, and is now reported as one:

before: invalid script name "a\x80b": character "\u0080" is not allowed …
after:  invalid script name "a\x80b": byte 0x80 is not allowed …

Reachability. codescripts.Resolve / ResolveScoped call ValidateName before any filesystem access (codescripts.go:394), and internal/httpapi/code_exec.go:244 maps *InvalidNameError straight to an HTTP 400 INVALID_SCRIPT_NAME whose message body is invalidName.Error(). So any code_execution call naming a non-ASCII script — i.e. the exact case the validator is there to refuse — returns this message to the agent. The input is one the caller chooses freely; nothing upstream narrows it to ASCII first.

Note the order of the checks: the byte-length check fires first, so the mojibake path needs a short non-ASCII name (語 is 3 bytes, under the 64 limit) and the echo path below needs a long one. Both are ordinary requests.

The echoed name is cut inside a rune

truncateForMessage bounds the echoed name at MaxNameLen+16 bytes with a raw byte slice. For a long non-ASCII name the cut lands inside a rune and %q then renders the dangling bytes as \xNN escapes, so the refusal ends in hex noise instead of the name it is quoting. It now goes through stringutil.UTF8Prefix, the helper added in #1664 (see also #1647, #1665, and #1726 / #1727 which finish that sweep elsewhere).

The byte-wise validation loop, the confinement property and every ASCII refusal are unchanged.

One thing I did not change

len(name) > MaxNameLen is a byte check, while MaxNameLen is documented and reported as a character count ("names are 1-64 characters"). They agree for every name the token can accept, since a valid name is ASCII — but a 30-character Japanese name is refused as "name is 90 characters long", which reads as a contradiction against a 64-character limit. Reporting the rune count instead makes it worse ("name is 30 characters long" against a 64-character limit), because the real divergence is in the check, not the message. That is a product decision about whether the limit is bytes or characters, so I left it exactly as it is rather than guess. Happy to follow up either way.

Testing

  • I have tested these changes locally
  • I have added/updated tests that prove my fix is effective or my feature works
  • All existing tests pass

New tests in internal/codescripts/invalid_name_message_test.go:

Test Asserts
TestInvalidNameErrorNamesTheRealCharacter the refusal contains 語 and does not contain è
TestInvalidNameErrorReportsMalformedByte a stray 0x80 is reported as byte 0x80
TestInvalidNameErrorEchoIsNotCutInsideARune 3 byte offsets inside a 3-byte rune; no \x escape survives in the echo
TestValidateNameASCIIMessagesUnchanged pins the full text of the empty / disallowed-character / oversized ASCII refusals, and that a valid name is still accepted

Mutation check. With internal/codescripts/codescripts.go reverted to the base SHA (14cb288) and the test file kept, the first three fail by name and TestValidateNameASCIIMessagesUnchanged passes — which is what shows the change is behaviour-preserving for ASCII. With the fix applied all four pass.

Commands run (exit status in brackets):

gofmt -l internal/codescripts          [0, no output]
git diff --check                        [0]
go vet ./internal/codescripts           [0]
go test -count=1 ./internal/codescripts [0, whole package]

Not run: -race and ./scripts/run-all-tests.sh (local disk constraints on this machine). Nothing here is concurrent.

ValidateName walks the name byte by byte, and reported the offending
character as string(name[i]). Converting a byte to a string yields the
code point of the same value, so the first byte of "語" (0xE8) was
rendered as "è": a caller asking for a Japanese script name got an
HTTP 400 INVALID_SCRIPT_NAME telling it to remove a character that is
not in the name it sent. A byte that begins no valid rune is not a
character at all and is now reported as a byte.

The byte-wise loop is the confinement boundary and is unchanged; only
the wording of the refusal moves.

truncateForMessage, which bounds the echoed name at MaxNameLen+16,
also cut at a raw byte offset, so a long non-ASCII name left a partial
rune that %q renders as \xNN escapes — the refusal ended in hex noise
instead of the name it quotes. It now goes through
stringutil.UTF8Prefix (smart-mcp-proxy#1647, smart-mcp-proxy#1664, smart-mcp-proxy#1665).

ASCII refusals are byte-for-byte unchanged.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Dumbris Dumbris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified on a merge with main: build, CI lint config, race tests; cross-reviewed clean.

@Dumbris
Dumbris merged commit f321b47 into smart-mcp-proxy:main Oct 22, 2026
47 checks passed
@Dumbris

Dumbris commented Oct 22, 2026

Copy link
Copy Markdown
Member

Merged. Thank you, @rudycelekli! The refusal now names the character that is actually in the script name instead of the first byte of it, which is exactly the detail someone hits first when a name is non-ASCII.

Contributions like this are what keep MCPProxy moving, and they matter a lot to the project.

One small ask, only if you feel like it: a short public note about your change (a tweet, a LinkedIn post, a line in a community you are part of) helps more people discover MCPProxy. No pressure at all, and I would be glad to repost it.

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.

3 participants