Skip to content

fix(code-execution): codeExecution.enabled takes only true or false - #485

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/473-code-execution-enabled
Oct 18, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/473-code-execution-enabled

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

codeExecution.enabled takes only true or false, or can be left out, with null counting as left out. create() refuses any other value, and a codeExecution that isn't an object, so a quoted "false" no longer turns code execution on.

!Fixes #473

Problem

On main (e6703634), create() reads config.codeExecution?.enabled by truthiness twice:

  • to decide whether validateCodeExecutionConfig runs (src/framework.ts:2303);
  • to decide whether the tool is synthesized: codeExecutionConfig is the config when the switch is truthy and null otherwise (:2639), and the code_execution tool and its interpreter are gated on that field (:4291, :14991).

So "enabled": "false", as a JSON config or a ${VAR} substitution can write it, gives agents the code_execution tool and a python interpreter on the host. A falsy non-boolean, such as 0, is read as off, and nothing says so. So is a bare codeExecution: true, whose switch reads as undefined. The type and its doc say the switch is a boolean and the tool is "only synthesized when true" (src/types/framework.ts:44-45).

Changes

  • The refusal. validateCodeExecutionConfig refuses an enabled that isn't true, false or null, naming the value's kind rather than the value, as the MCPL server boolean refusals do. A string gets "true or false, unquoted". null counts as not set.
  • The parent. A codeExecution that isn't an object, such as a bare true, is refused too, naming its kind.
  • When it runs. create() runs the validator whenever codeExecution is given, and the validator checks the other settings only once enabled is true. So a configuration that isn't on still has its other settings left unchecked, as before.
  • The gate. The tool is synthesized only for enabled === true.
  • kindOf is exported from src/mcpl/transport.ts for the validator's message. The package entry doesn't re-export it.
  • Text: the enabled doc says what's refused, and a changelog fragment under Fixed.

What callers will notice

  • create() now refuses a codeExecution.enabled such as "false", "true", 0, 1 or an object, with "codeExecution.enabled must be true or false, … not a string" (or the value's kind). Such a config used to start, with code execution on for a truthy value and off for a falsy one.
  • It also refuses a codeExecution that isn't an object, such as true, with "codeExecution must be an object, such as { enabled: true }, not a boolean". It used to start with code execution off.
  • enabled: true, false, null or left out behave as before.
  • Nothing public changes shape: validateCodeExecutionConfig and kindOf aren't exported from the package entry.

What each part is for

  • Refusing rather than reading === true alone. Reading only true as on would make "false" off, but "true" off too, without a word. The refusal says at create() which value was misread. It's the rule fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451 set for an MCPL server's booleans.
  • null as not set. A generated config can write a setting it leaves unset as null, and this keeps such a config working, as fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451 does.
  • The kind, not the value. A substituted value can be a credential.
  • Refusing a codeExecution that isn't an object. Its switch would read as undefined, so it would be off without a word, the same misread one level up.
  • Calling the validator whenever codeExecution is given. Called only when the switch is truthy, as on main, it would never see a falsy non-boolean such as 0.
  • Checking nothing else when it's off. A configuration that isn't on is never read, so refusing its other settings would only refuse what did no harm before.
  • === true at the gate, beside the refusal. With the refusal in front of it, a non-boolean reaches the gate only if the caller changes the config while create() runs, so taking it out alone fails no test. It keeps the gate meaning what the type says.
  • Exporting kindOf rather than copying it, so both refusals word a kind the same way.
  • The tests.
    • One test runs create() with a quoted "false", a token-like string, 0 and an object as the switch. Another runs it with true, "true" and an array as codeExecution. Each checks the refusal's whole message, so no value can show, and that nothing was left at the store's path.
    • A third checks that a null codeExecution, and contradictory limits beside an enabled that is null, false or left out, each create a framework that doesn't offer code_execution.

Tests

At 2e117acf, on Linux, Node 24.22.0 and Python 3.13.5. The pull request's CI runs the whole suite on Node 20 and 24.

  • test/code-execution.test.ts and test/mcpl-config-booleans.test.ts pass 64 and 8 tests.
  • With main's four changed source files, 2 of those fail: the two refusal tests, which get no rejection. In that state, a probe that creates a framework with enabled: "false" finds code_execution offered, and a call to it runs python and prints its version.
  • Each piece taken back out alone fails a test:
    • the switch's refusal: the switch's refusal test;
    • the parent's refusal: the parent's refusal test;
    • null as not set, or leaving the switch out as not set: the off test;
    • checking nothing else when it's off, taken out or narrowed to enabled === false: the off test, whose off configurations are then refused for their limits;
    • the validator's call put back to main's truthy one: both refusal tests;
    • the call letting a null codeExecution through to the validator: the off test;
    • the call moved to after the store opens: both refusal tests, which find a store left behind.
  • The gate's === true, taken out alone, fails no test, as "What each part is for" says (at 8b3acaf3, whose gate is this head's).
  • At this head, the same probe's create() rejects with "codeExecution.enabled must be true or false, unquoted, not a string: true gives agents the code_execution tool, …".

Not verified

  • No live MCPL server or provider took part, and nothing ran on Windows.

Related

Written by Effie-2756 (Claude Opus 5.5) using the Rookery's Pi tooling, on Ada-1017's calls. Naomi-2422 (Claude Opus 5.5) reviewed the change.

Effie-2756 and others added 2 commits October 18, 2026 00:58
…nima-research#473)

create() read config.codeExecution?.enabled by truthiness twice: to decide
whether validateCodeExecutionConfig runs, and to decide whether the tool is
synthesized. So "enabled": "false", as a JSON config or a ${VAR}
substitution can write it, gave agents the code_execution tool and a python
interpreter on the host, and a falsy non-boolean such as 0 was read as off
without a word.

validateCodeExecutionConfig now refuses an enabled that isn't true, false or
null, naming the value's kind rather than the value, worded as the MCPL
server boolean refusals are (kindOf, now exported from transport.ts, which
the package entry doesn't re-export). null counts as not set. create() runs
the validator whenever codeExecution is given, and the validator checks the
rest only once the switch is true, so a configuration that isn't on still
has its other settings left unchecked, as before. The tool is synthesized
only for enabled === true.

Tests: create() refuses a quoted "false", a token-like string, 0 and an
object, each by kind and without the value; a null enabled, and an enabled
false with contradictory limits, both create a framework without the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hten the tests and the text

Answers the internal review of the previous commit.

A codeExecution that isn't an object, such as a bare `true`, read its
switch as undefined and so was off, without a word: the same misread one
level up. validateCodeExecutionConfig now refuses it, naming its kind, as
the switch's refusal does.

Text: the switch's doc and the validator's doc say null or leaving it out
counts as off; scriptTimeLimits' comment says a ceiling below the default
is refused when a framework with code execution on is created; the
changelog fragment names the parent's refusal and, as anima-research#451's does, what a
host that passes the setting on unchecked now gets.

Tests: each refusal's whole message is asserted, so none can show the
value, and nothing may exist at storePath after it. A new test refuses a
codeExecution of true, "true" and an array. The off test creates a
framework with codeExecution null, and with contradictory limits beside an
enabled that is null, false or left out, inside its try.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 18, 2026

Copy link
Copy Markdown

Required label not found on this PR.

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
@nissa-seru
nissa-seru merged commit 9dc163c into anima-research:main Oct 18, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
main moved 71 merges since e670363. Beyond the conflicts, this is where
they meet this branch.

One rule for server ids, this branch's, across both engines:
- A server id is held while a connect or a disconnect of it is in
  flight, or while a restart moves its server to it (mcpServerIdTaken,
  holdMcpServerId). anima-research#469's refuseMcplConnect and anima-research#506's mcplConnecting
  set went, so two rules don't guard the same ids.
- A connect or a restart into an id whose connect is in flight, and
  whose server isn't registered yet, is refused in anima-research#506's words: 'MCPL
  server "<id>" has a connect in flight; try again once it settles'.
- A disconnect made while a connect is in flight ends that connect and
  removes what it registered, where anima-research#506 refused the disconnect.
- anima-research#469's prefixesMeet joins mcplPrefixCollision. It is checked at runtime
  connects and restarts in both engines, and a prefix that meets one a
  restart holds is refused too. create() already checks every configured
  server, a modern one included.
- A tool call while its server connects reads anima-research#506's "connecting" text
  when a connect holds the id or a restart claims it
  (mcplServerAbsentText).

Request errors (anima-research#247, in 0.22.3, and anima-research#506):
- McplRequestError has no code, as 0.22.3 has it. The modern engine's two
  error-response failures pass their data alone, and the code stays in
  the message. The class this branch's first commit added is gone, so
  there's one.
- The legacy engine's request errors take anima-research#506's words. The not-sent
  refusal after a stdio pipe failure says this host can no longer write
  to the server, and the method and the pipe's error go to the log.
- protocolVersionRejection reads the server's message with describeCause,
  bounded, as the initialize error does. A message with no primitive form
  threw out of the handshake's line listener.

Tool results:
- An MCP tool's error result with no words, or only whitespace, reads as
  anima-research#506's text in both readings and on the direct path
  (mcplToolNoWordsText; normalizeStandardToolResult takes it as noWords).
- The direct path's failure is mcplToolFailure's text without the
  server's other blocks, as 0.22.3 gives it, with structured kept by
  presence. A script's structured failure keeps its content.
- anima-research#510's in-process registration: autoFetchReferences takes a mode,
  'register', 'model' or 'script', in place of a positional boolean whose
  meaning differed on each side. executeMcplToolCall registers an MCPL
  peer's references only. The model's dispatch fetches eagerly, and a
  script's reads fetch everything.
- A script's call of a tool that isn't an MCPL server's (a module's)
  materializes the references registered against their servers again, as
  main does. An MCPL server's were fetched at dispatch.
- referenceBlockForScript gives a block the classifier rejects anima-research#506's
  notice (classifyBlock's shown), as the model's lanes do.

Inbound handlers:
- Main's containedInboundHandler replaces this branch's answerOnce and
  handlerFailureText. What a failure records, in its trace and its log
  line, goes through the server's redactor, which wireMcplEvents and a
  queued manifest run (rerunManifestRefresh) pass. It defaults to the text
  as it is.

Transport (anima-research#494, anima-research#499, anima-research#485):
- StdioTransport takes main's constructor, (child, rl, serverId), with
  both pipeFailure and stderrEnded. The handshake's wait for stderr's end
  comes through unchanged.
- The boolean settings check is main's assertBooleanSetting, keeping this
  branch's dialsNetwork skip for inheritEnv.

Elsewhere:
- stop() is main's step()-by-step teardown, with endConnectsInFlight as
  its first step.
- addModule checks the tool prefix, then resetCompletedStop('addModule()')
  (anima-research#532).
- thrownText is main's boundedErrorMessage, with this branch's redactor
  applied before the cut.
- Changelog: this branch's fragments say what changes from 0.22.3. The
  folded mcpl-request-outcome fragment is main's, and anima-research#506's
  connect-in-flight fragment describes the merged rule.
- Tests: main's new stub servers answer protocolVersion. Words and log
  lines follow the merged code. The test stand-ins use the merged
  constructor, and wireMcplEvents gets its config. A close that now waits
  for the child is awaited after its request's rejection is observed. Two
  manifest re-run tests count the configuration read at each run's start.
  anima-research#506's test of refuseMcplConnect now tests the merged rule.

Receipts, on Linux at this commit's tree: the typecheck and the build are
clean, and every test file, each in a process of its own, passes: 232
files and 4,339 tests (4,322 passing, 17 skipped, none failing) on Node
24.22.0, and the same on Node 20.20.2, with CI's stop pair passing on
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

codeExecution.enabled is read by truthiness, so a quoted "false" turns code execution on

1 participant