codemode: support dotted provider namespaces and dynamic remote providers#2
codemode: support dotted provider namespaces and dynamic remote providers#2jonastemplestein wants to merge 4 commits intomainfrom
Conversation
|
Took a second pass and reduced the scope substantially. What changed:
So the PR is now focused on the minimal runtime-first behavior:
I think this is a much better tradeoff and closer to the smallest upstreamable shape. |
|
Did one more polish pass:
So the iterate-specific compromise is now:
|
887593d to
262c572
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 262c572. Configure here.
| if (isWorkspaceChangeMessage(parsed)) { | ||
| setWorkspaceRevision((n) => n + 1); | ||
| } | ||
| } |
There was a problem hiding this comment.
Unstable onMessage callback causes repeated WebSocket reconnections
Medium Severity
The onMessage handler passed to useAgent is an inline arrow function, creating a new reference on every render. Right next to it, onMcpUpdate is correctly wrapped in useCallback. If useAgent uses the callback identity in an effect dependency array (as is typical for React hooks managing connections), the unstable onMessage reference would cause the directory WebSocket to tear down and reconnect on every render, breaking sidebar state updates, MCP broadcasts, and workspace change notifications.
Reviewed by Cursor Bugbot for commit 262c572. Configure here.
262c572 to
0c454cc
Compare
|
Closing in favor of #3, which has the correct clean diff on top of iterate/main. |
| const seenNames = new Set<string>(); | ||
| const providerPaths = new Map<string, string[]>(); | ||
| for (const provider of providers) { | ||
| if (RESERVED_NAMES.has(provider.name)) { | ||
| return { | ||
| result: undefined, | ||
| error: `Provider name "${provider.name}" is reserved` | ||
| }; | ||
| } | ||
| if (!VALID_IDENT.test(provider.name)) { | ||
| return { | ||
| result: undefined, | ||
| error: `Provider name "${provider.name}" is not a valid JavaScript identifier` | ||
| }; | ||
| const safePath = sanitizeToolPath(provider.name); | ||
| const pathParts = safePath.split("."); | ||
| for (const part of pathParts) { | ||
| if (RESERVED_NAMES.has(part)) { | ||
| return { | ||
| result: undefined, | ||
| error: `Provider name segment "${part}" is reserved` | ||
| }; | ||
| } | ||
| } | ||
| if (seenNames.has(provider.name)) { | ||
| const providerKey = pathParts.join("."); | ||
| if (seenNames.has(providerKey)) { | ||
| return { | ||
| result: undefined, | ||
| error: `Duplicate provider name "${provider.name}"` | ||
| }; | ||
| } | ||
| seenNames.add(provider.name); | ||
| seenNames.add(providerKey); | ||
| providerPaths.set(provider.name, pathParts); | ||
| } |
There was a problem hiding this comment.
🟡 Single-segment provider proxy shadows all longer-prefixed providers sharing the same root
When a single-segment provider (e.g., name: "mcp") and a multi-segment provider sharing the same root (e.g., name: "mcp.server") are both registered, the single-segment provider's recursive Proxy intercepts all property accesses via its get trap, making the multi-segment provider's proxy permanently unreachable.
Detailed walkthrough of the failure
The generated sandbox code for name: "mcp" does mcp = (() => { /* proxy A */ })(), creating a Proxy with a get trap that captures every property access. The code for name: "mcp.server" then does mcp.server = (() => { /* proxy B */ })() — this set falls through to proxy A's target (a function object) since there's no set trap, but when sandbox code later accesses mcp.server.tool(), proxy A's get trap intercepts "server", returning make(["server"]) which routes to the "mcp" dispatcher, not the "mcp.server" dispatcher.
A plausible scenario:
createCodeTool({
tools: [
{ tools: defaultTools }, // name defaults to "codemode"
dynamicTools({ name: "codemode.extra", callTool: hook }),
],
executor,
});The "codemode.extra" provider's tools are silently unreachable.
The validation loop at executor.ts:276-298 checks for exact duplicate providerKeys but not for prefix conflicts. A provider with key "mcp" and another with "mcp.server" both pass validation, producing silently broken runtime behavior.
Prompt for agents
The provider name validation loop in DynamicWorkerExecutor.execute() (executor.ts lines 276-298) checks for exact duplicate providerKeys but does not detect when one provider's path is a strict prefix of another's. For example, providers with names "mcp" and "mcp.server" both pass validation, but at runtime the single-segment proxy's get trap intercepts all property accesses, making the longer-prefixed provider unreachable.
The fix should add prefix conflict detection after the seenNames check. After building the seenNames set, iterate over all pairs of provider keys and check if any key is a strict prefix of another (i.e., key A equals the first N segments of key B). If so, return an error like 'Provider name "mcp" conflicts with "mcp.server" — a single-segment provider cannot coexist with providers that extend its namespace'.
A simple approach: after populating seenNames, for each providerKey, check if any other key in seenNames starts with providerKey + '.' (or vice versa). This would catch the prefix conflict.
Was this helpful? React with 👍 or 👎 to provide feedback.


Why this exists
The main goal of this follow-up is to make remote tool hosts usable from codemode with as little codemode surgery as possible — especially:
The key constraint was: do not redesign codemode around a whole new remote-provider abstraction if a much smaller runtime-first change can unlock the use case.
In other words, this PR tries to answer:
What changed
This PR adds two small but important capabilities:
Together they allow code like this inside codemode-generated sandbox code:
where codemode does not need a static local
tools: Record<string, Tool>for every callable leaf.Instead, the provider can forward the attempted call to a remote host at runtime.
New API, up front
1) Dedicated dynamic entrypoint
I intentionally moved this off the root codemode entrypoint so the mainstream/static codemode API stays conservative, and the runtime-first escape hatch is opt-in.
2) Dynamic provider authoring
That is the primary intended shape.
Example: Durable Object provider
This is the motivating case I had in mind while shaping the PR.
Then codemode-generated code can do:
At runtime codemode forwards:
"files.read"+[{ path: "README.md" }]"issues.list"+[{ state: "open" }]into the DO instance.
That is the whole point: remote/DO-backed providers can be used without teaching codemode a whole new discovery or transport framework first.
Design summary
A. Dotted tool names are already path-like
The earlier dotted-tool-path work made codemode understand that tool names like:
files.readinternal.sample.pingshould behave like nested sandbox access and nested ambient type declarations.
This PR extends the same idea one level higher to provider names.
So now a provider name like:
name: "mcp.someServer"means sandbox code can access:
B. Dynamic providers trade enumeration for runtime dispatch
Static providers still look like:
Dynamic providers instead look like:
This is the explicit “trust me, try it at runtime” escape hatch.
If sandbox code attempts:
codemode forwards:
"foo.bar"[1, 2]to the provider’s
callTool()hook.C.
typesremains model-facing prompt materialI originally explored async/lazy
types, but after review that version turned out to be too invasive and not worth the complexity.So the final compromise is:
typesThat keeps codemode’s eager description assembly intact and avoids large refactors in
tool.ts/tanstack-ai.ts.Why this is intentionally minimal
I specifically did not add:
All of those may be reasonable later, but they are not required to unblock the real use case.
The smallest useful thing is:
That is enough to make a remote server or Durable Object instance usable as a codemode provider today.
Detailed implementation notes
1) Executor runtime now supports dotted provider namespaces
DynamicWorkerExecutornow builds sandbox globals recursively for dotted provider names.So a provider named:
creates nested proxy access rather than a single flat identifier.
2) Runtime dispatch supports dynamic providers
ResolvedProvidercan now carry either:fns)callTool(name, args)hookToolDispatcherchecks static functions first, then falls back to the dynamic provider hook.3) Ambient declarations support dotted provider namespaces
Type-generation paths now emit nested declarations for dotted provider namespaces so the prompt-side model view matches runtime access.
Example output shape:
4) Prefix conflicts still work
The previous dotted-tool-path work also introduced
$callwhen a name is both:That behavior is preserved.
5) Dedicated entrypoint for dynamic behavior
dynamicTools()now lives at:instead of the root package export.
This keeps the root package surface narrower and makes the dynamic/runtime-first behavior feel intentionally opt-in.
Review-driven changes / corrections
This PR changed shape significantly during review.
The original version tried to do too much, especially around async/lazy
types. Review feedback was right that this created both:I explicitly corrected the following:
Fixed: discarded prompt description bug
The async-doc experiment accidentally computed the final description too late and discarded it, leaving the raw
{{types}}placeholder visible to the model.That design was removed.
Fixed: dotted namespace emit bug
Nested provider declaration output was briefly dropping intermediate segments like
someServer. The declaration tree helpers and callers were corrected so emitted prompt declarations now match runtime access.Fixed: dead runtime conditional
A leftover no-op conditional in the executor proxy path was removed.
Reduced scope substantially
I removed the async/lazy prompt-doc machinery entirely and went back to the smallest shape that actually serves the remote/DO provider use case.
Files of interest
Primary implementation files:
packages/codemode/src/executor.tspackages/codemode/src/dynamic-tools.tspackages/codemode/src/dynamic.tspackages/codemode/src/resolve.tspackages/codemode/src/tool.tspackages/codemode/src/tanstack-ai.tspackages/codemode/src/tool-types.tspackages/codemode/src/json-schema-types.tspackages/codemode/src/type-tree.tspackages/codemode/src/utils.tsTests:
packages/codemode/src/tests/dynamic-tools.test.tspackages/codemode/src/tests/executor.test.tspackages/codemode/src/tests/tool-types.test.tspackages/codemode/src/tests/utils.test.tsPackaging / exports:
packages/codemode/package.jsonpackages/codemode/scripts/build.tsBefore / after mental model
Before
Codemode was best at providers that could eagerly produce:
That is awkward for remote systems where the real implementation lives elsewhere, especially per-instance systems like Durable Objects.
After
Codemode still fully supports the static shape, but now also supports:
That is enough to bridge codemode into remote tool servers with very little new machinery.
Scope / non-goals
This PR is not trying to solve every remote-tools problem.
It does not provide:
Those are intentionally left to the caller / provider implementation.
This PR only provides the minimal codemode runtime surface necessary so those systems can be plugged in cleanly.
Validation
Note
Medium Risk
Touches core execution and dispatch logic (
DynamicWorkerExecutor/ToolDispatcher) and prompt type-generation, which could break existing tool routing or namespaces if edge cases slip through. Changes are additive but affect widely used paths.Overview
Adds an opt-in
@cloudflare/codemode/dynamicentrypoint exposingdynamicTools()for providers whose tool surface is resolved at runtime via acallTool(name, args)hook.Extends runtime execution to support dotted provider namespaces (e.g.
mcp.someServer.*) by generating nested sandbox proxies and mapping dispatchers by sanitized dotted paths;ToolDispatchernow falls back to a provider-levelcallToolwhen no static tool match exists.Updates provider resolution and prompt/type generation to match these namespaces:
ToolProvideris now a static/dynamic union,typesis treated as model-facing documentation, and type emitters (tool-types,json-schema-types,tanstack-ai) now graft tool declarations under dotted namespace roots using the newinsertDeclTree()helper. Includes new/updated tests covering dynamic dispatch and dotted provider access.Reviewed by Cursor Bugbot for commit 0c454cc. Bugbot is set up for automated code reviews on this repo. Configure here.