Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/static-code-validators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/codemode": minor
---

Add built-in static code validators: `syntaxValidator` and `semanticValidator`. These plug into the pluggable runtime validator API and reject model-generated code that cannot succeed — invalid JavaScript, references to unconfigured connectors, and calls to methods a connector does not expose — before a dynamic Worker execution is created. Detection uses the Oxc parser and semantic analyzer compiled to WebAssembly (`workerd-oxc`), so it runs inside the Workers runtime. Rejections return model-actionable diagnostics with source locations and suggestions.
47 changes: 47 additions & 0 deletions docs/codemode/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,50 @@ Validators reject code or calls; they do not transform them. Perform normalizati
Configured validation hooks fail closed. If a hook throws or returns an invalid result, Codemode logs the original value or exception in the host and blocks the operation. The model receives a generic error that names the validator but does not include thrown details, which could contain private application data.

Validators should perform reads rather than side effects. Upstream APIs should still enforce transactional invariants because validation cannot prevent state from changing between a check and the eventual remote operation.

## Built-in static validators

Codemode ships two `validateCode` validators that reject programs which cannot succeed before a dynamic Worker execution is created. Both use the Oxc parser and semantic analyzer compiled to WebAssembly (`workerd-oxc`), so they run inside the Workers runtime with no external service.

```ts
import {
createCodemodeRuntime,
syntaxValidator,
semanticValidator
} from "@cloudflare/codemode";

const runtime = createCodemodeRuntime({
ctx: this.ctx,
executor,
connectors,
validators: [syntaxValidator(), semanticValidator()]
});
```

### `syntaxValidator(options?)`

Rejects code that is not parseable. The executor runs generated code as plain JavaScript, so the validator parses as JavaScript by default and rejects TypeScript-only syntax that would fail at execution.

| Option | Type | Default | Purpose |
| ----------- | -------------- | ---------- | ------------------------------------------------------------------- |
| `name` | `string` | `"syntax"` | Name attributed on diagnostics. |
| `language` | `"js" \| "ts"` | `"js"` | Language to parse as. Use `"ts"` only if the executor strips types. |
| `maxIssues` | `number` | `10` | Maximum number of syntax diagnostics to report. |

### `semanticValidator(options?)`

Rejects code that parses but references things that do not exist:

- **Unknown connectors** — a bare identifier that is neither a configured connector, the built-in `codemode` provider, nor an ambient JavaScript/Workers global. Such code throws `X is not defined` the moment it runs.
- **Unknown methods** — a call to a method a configured connector does not expose. Enabled by default; disable with `checkMethods: false`. Connectors whose descriptors are empty are skipped because their method set is unknown.

| Option | Type | Default | Purpose |
| ---------------- | ------------------- | ------------ | ---------------------------------------------------------------------------- |
| `name` | `string` | `"semantic"` | Name attributed on diagnostics. |
| `allowedGlobals` | `readonly string[]` | `[]` | Extra injected identifiers to permit, such as the names of custom providers. |
| `checkMethods` | `boolean` | `true` | Also reject calls to methods a connector does not expose. |
| `maxIssues` | `number` | `10` | Maximum number of issues to report. |

The semantic analyzer works on a single file and has no knowledge of the ambient environment, so it treats every free identifier as unresolved. The validator subtracts a curated set of standard JavaScript and Workers globals (exported as `AMBIENT_SANDBOX_GLOBALS`) plus the built-in `codemode` provider before deciding an identifier is an unknown connector. Applications that register custom providers must list those provider names in `allowedGlobals`.

Because these are `validateCode` validators, they run before the executor starts and a rejection creates no execution. They do not implement `validateToolCall`, so they are not recorded as resume-required.
3 changes: 2 additions & 1 deletion packages/codemode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
"dependencies": {
"@types/json-schema": "^7.0.15",
"acorn": "^8.17.0"
"acorn": "^8.17.0",
"workerd-oxc": "^0.6.0"
},
"devDependencies": {
"@modelcontextprotocol/sdk": "1.29.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/codemode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export {
type ToolCallValidationContext,
type CodemodeValidator
} from "./validation";
export {
syntaxValidator,
semanticValidator,
AMBIENT_SANDBOX_GLOBALS,
BUILTIN_PROVIDER_GLOBALS,
type SyntaxValidatorOptions,
type SemanticValidatorOptions
} from "./validators";
export { resolveProvider } from "./resolve";
export {
truncateResponse,
Expand Down
54 changes: 54 additions & 0 deletions packages/codemode/src/runtime-tests/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface Host {
code: string,
options?: { maxExecutions?: number; name?: string }
): Promise<ProxyToolOutput>;
runStatic(code: string): Promise<ProxyToolOutput>;
approve(executionId: string): Promise<ProxyToolOutput>;
approveWithoutValidators(executionId: string): Promise<ProxyToolOutput>;
approveWithoutItems(executionId: string): Promise<ProxyToolOutput>;
Expand Down Expand Up @@ -97,6 +98,59 @@ describe("codemode durable runtime (e2e)", () => {
if (out.status === "completed") expect(out.result).toEqual([]);
});

describe("static validators (syntax + semantic)", () => {
it("rejects a syntax error before any execution is created", async () => {
const h = host();
const out = await h.runStatic(
`async () => { return await items.list_items( }`
);

expect(out.status).toBe("error");
if (out.status !== "error") return;
expect(out.executionId).toBe("");
expect(out.error).toContain("syntax");
// No execution row created -> no dynamic Worker isolate was spun up.
expect(await h.executions()).toEqual([]);
});

it("rejects an unknown connector before any execution is created", async () => {
const h = host();
const out = await h.runStatic(
`async () => await slack.post({ text: "hi" })`
);

expect(out.status).toBe("error");
if (out.status !== "error") return;
expect(out.executionId).toBe("");
expect(out.error).toContain("slack");
expect(await h.executions()).toEqual([]);
});

it("rejects an unknown method before any execution is created", async () => {
const h = host();
const out = await h.runStatic(
`async () => await items.no_such_method({})`
);

expect(out.status).toBe("error");
if (out.status !== "error") return;
expect(out.executionId).toBe("");
expect(out.error).toContain("no_such_method");
expect(await h.executions()).toEqual([]);
});

it("lets valid code through to real execution", async () => {
const h = host();
const out = await h.runStatic(`async () => await items.list_items()`);

expect(out.status).toBe("completed");
if (out.status !== "completed") return;
expect(out.result).toEqual([]);
// A valid run *does* create an execution.
expect((await h.executions()).length).toBe(1);
});
});

it("rejects generated code before creating an execution", async () => {
const h = host();
const out = await h.run(`async () => "INVALID_CODE"`);
Expand Down
25 changes: 25 additions & 0 deletions packages/codemode/src/runtime-tests/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { DynamicWorkerExecutor } from "../executor";
import { createCodemodeRuntime } from "../runtime-handle";
import type { CodemodeValidator } from "../validation";
import { syntaxValidator, semanticValidator } from "../validators";
import {
getCodemodeRuntime,
type ProxyToolInput,
Expand Down Expand Up @@ -239,6 +240,30 @@ export class CodemodeTestHost extends DurableObject<Env> {
return execute({ code }, { toolCallId: "test", messages: [] });
}

/**
* A runtime wired with the real static code validators (syntax + semantic)
* instead of the fake test-policy — used to prove that statically-doomed code
* is rejected before any execution (and therefore any sandbox cost).
*/
#staticRuntime() {
const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER });
return createCodemodeRuntime({
ctx: this.ctx,
executor,
connectors: [this.#items()],
validators: [syntaxValidator(), semanticValidator()]
});
}

async runStatic(code: string): Promise<ProxyToolOutput> {
const codemode = this.#staticRuntime().tool();
const execute = codemode.execute as (
input: ProxyToolInput,
ctx: unknown
) => Promise<ProxyToolOutput>;
return execute({ code }, { toolCallId: "test", messages: [] });
}

approve(executionId: string): Promise<ProxyToolOutput> {
return this.#runtime().approve({ executionId });
}
Expand Down
Loading