Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .changeset/tidy-v1-api-surface.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
"@execbox/core": minor
"@execbox/quickjs": minor
"@execbox/remote": minor
---

Tighten the pre-1.0 public API surface by keeping low-level core helpers out of the main `@execbox/core` entrypoint, removing unsupported QuickJS runner subpath exports, and keeping runner-side remote endpoint types with `@execbox/quickjs/remote-endpoint`.
Tighten the pre-1.0 public API surface by keeping low-level core helpers out of the main `@execbox/core` entrypoint and removing unsupported QuickJS runner subpath exports. The v1 runtime surface is now inline QuickJS plus worker-hosted QuickJS.
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- `execbox` is a Node.js 22+ npm workspace that publishes the `@execbox/*` package family.
- Core source lives under `packages/*/src`, tests live under `packages/*/__tests__`, runnable examples live under `examples/`, and the public docs site lives under `docs/`.
- The workspace currently contains `@execbox/core`, `@execbox/quickjs`, and `@execbox/remote`.
- The workspace currently contains `@execbox/core` and `@execbox/quickjs`.
- Keep changes aligned with existing package boundaries. Prefer changing the owning package instead of introducing cross-package shortcuts.

## Setup Commands
Expand All @@ -30,8 +30,9 @@

- For most code changes, run `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm test`, and `npm run build`.
- If you change package exports, manifest fields, or published type-resolution behavior, also run `npm run package:check`.
- If you change the public API of any entrypoint listed in `scripts/workspace-entrypoints.ts`, including `@execbox/core/runtime` and `@execbox/quickjs/remote-endpoint`, also run `npm run api:check`.
- If you change docs site content, navigation, or VitePress config, also run `npm run docs:build`.
- If you change the public API of any entrypoint listed in `scripts/workspace-entrypoints.ts`, including `@execbox/core/runtime`, also run `npm run api:check`.
- If you change examples or runtime guidance, also run `npm run examples`.
- If you change docs site content, navigation, or Starlight/Astro config, also run `npm run docs:build`.
- If you touch execution boundaries, timeout handling, abort propagation, schema validation, or log/memory controls, also run `npm run test:security`.

## Security Notes
Expand All @@ -51,6 +52,6 @@
## Useful References

- Start with `README.md` for the package map.
- Use `docs/getting-started.md` for install and example expectations.
- Use `docs/security.md` and `docs/architecture/README.md` before changing execution boundaries or runtime claims.
- Use `docs/src/content/docs/getting-started.md` for install and example expectations.
- Use `docs/src/content/docs/security.md` and `docs/src/content/docs/architecture/index.md` before changing execution boundaries or runtime claims.
- For the human-oriented contribution workflow, see `CONTRIBUTING.md`.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ This guide is for both humans and coding agents. Agent-specific operating instru

- General code changes: `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm test`, and `npm run build`
- Package export, manifest, or published type-resolution changes: `npm run package:check`
- Example or runtime guidance changes: `npm run examples`
- Docs site changes: `npm run docs:build`
- Security or execution-boundary changes: `npm run test:security`

Choose the smallest verification set that covers your change, and include the commands you ran in your PR or handoff notes when the context would help reviewers.

- Public API changes to any entrypoint listed in `scripts/workspace-entrypoints.ts`, including `@execbox/core/runtime` and `@execbox/quickjs/remote-endpoint`: run `npm run api:check`
- Public API changes to any entrypoint listed in `scripts/workspace-entrypoints.ts`, including `@execbox/core/runtime`: run `npm run api:check`
- Put security-focused specs under `packages/*/__tests__/security/`; `npm run test:security` runs those grouped suites.

## Changesets
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@ Portable code execution for [Model Context Protocol](https://modelcontextprotoco

[![License](https://img.shields.io/github/license/aallam/execbox?style=flat-square)](https://github.com/aallam/execbox/blob/main/LICENSE)
[![Docs](https://img.shields.io/badge/docs-site-0ea5e9?style=flat-square)](https://execbox.aallam.com)
[![Packages](https://img.shields.io/badge/packages-3-111827?style=flat-square)](#package-map)
[![Packages](https://img.shields.io/badge/packages-2-111827?style=flat-square)](#package-map)

</div>

Execbox turns host tool catalogs into callable guest namespaces, supports MCP wrapping on both sides of the boundary, and lets you place guest JavaScript where it fits your deployment: inline QuickJS, worker-hosted QuickJS, or a remote runner behind your own transport.
Execbox turns host tool catalogs into callable guest namespaces, supports MCP wrapping on both sides of the boundary, and lets you run guest JavaScript through inline QuickJS or worker-hosted QuickJS.

## Package Map

| Package | npm | What it is for |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [`@execbox/core`](./packages/core/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fcore?style=flat-square)](https://www.npmjs.com/package/@execbox/core) | Core execution contract, provider resolution, MCP adapters, and runtime/protocol subpaths |
| [`@execbox/quickjs`](./packages/quickjs/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fquickjs?style=flat-square)](https://www.npmjs.com/package/@execbox/quickjs) | QuickJS executor for inline and worker hosts |
| [`@execbox/remote`](./packages/remote/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fremote?style=flat-square)](https://www.npmjs.com/package/@execbox/remote) | Advanced transport-backed executor for app-owned runtime boundaries |

## Examples

Expand Down
1 change: 0 additions & 1 deletion benchmarks/results.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,3 @@ This suite only measures the parent Node process.
### What this snapshot does not prove

- It does not prove exact throughput rankings for every workload or host. The concurrency and tool-call suites are still sensitive to local scheduler noise.
- It does not measure `RemoteExecutor`, because remote performance depends on the caller-owned transport and remote runtime deployment.
8 changes: 0 additions & 8 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ export default defineConfig({
label: "MCP Provider",
slug: "architecture/execbox-mcp-and-protocol",
},
{
label: "Remote Runner",
slug: "architecture/execbox-remote-workflow",
},
],
},
{
Expand All @@ -64,10 +60,6 @@ export default defineConfig({
label: "Protocol",
slug: "architecture/execbox-protocol-reference",
},
{
label: "Runner Specification",
slug: "architecture/execbox-runner-specification",
},
],
},
],
Expand Down
7 changes: 3 additions & 4 deletions docs/src/content/docs/architecture/execbox-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The resolved provider also carries two maps:

## Guest Code Normalization

Executors do not evaluate arbitrary snippets directly. Runtime implementers import `normalizeCode()` from `@execbox/core/runtime` to turn model- or user-produced text into a consistent async function body.
Executors normalize snippets before evaluation. Runtime implementers import `normalizeCode()` from `@execbox/core/runtime` to turn model- or user-produced text into a consistent async function body.

That normalization handles:

Expand Down Expand Up @@ -129,7 +129,7 @@ interface Executor {
}
```

The core package intentionally does not decide where the code runs. It only defines what the runtime must honor.
The core package defines what every runtime must honor while executor packages decide where code runs.

```mermaid
sequenceDiagram
Expand Down Expand Up @@ -194,12 +194,11 @@ Executors are responsible for their own runtime-specific classification rules, b

## Why the Core Stays Small

The core package does not own QuickJS, worker threads, process boundaries, or transport mechanics. That separation keeps the core useful for:
The core package stays focused on provider, execution, MCP, runtime-helper, and protocol contracts. That separation keeps the core useful for:

- direct in-process runtimes
- worker-backed runtimes
- MCP wrapper servers
- remote execution models

The consequence is deliberate separation between:

Expand Down
49 changes: 18 additions & 31 deletions docs/src/content/docs/architecture/execbox-executors.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
---
title: Execbox Executors
description: Compare inline QuickJS, worker-hosted QuickJS, and remote executor trade-offs.
description: Compare inline QuickJS and worker-hosted QuickJS trade-offs.
---

This page explains how the available executors differ and what trade-offs they make.

## Executor Comparison

| Executor or mode | Runtime boundary | Tool bridge style | Main strengths | Main constraints |
| --------------------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------ | ----------------------------------------------- |
| `QuickJsExecutor` | Fresh in-process QuickJS runtime | Shared runner callback | No native addon, simple install, default backend | Still in-process |
| `QuickJsExecutor` with `host: "worker"` | Worker thread + fresh QuickJS runtime | Shared host session + messages | Hard-stop worker termination, pooled by default | Still same OS process; ephemeral mode is slower |
| `RemoteExecutor` | App-defined remote transport boundary | Shared host session + transport | Same API across a remote boundary | You own transport/runtime deployment |
| Executor or mode | Runtime boundary | Tool bridge style | Main strengths | Main constraints |
| --------------------------------------- | ------------------------------------- | ------------------------------ | ------------------------------------------------ | ----------------------------------------------- |
| `QuickJsExecutor` | Fresh in-process QuickJS runtime | Shared runner callback | No native addon, simple install, default backend | Still in-process |
| `QuickJsExecutor` with `host: "worker"` | Worker thread + fresh QuickJS runtime | Shared host session + messages | Hard-stop worker termination, pooled by default | Still same OS process; ephemeral mode is slower |

```mermaid
flowchart LR
HOST["Host application"]
QJS["QuickJsExecutor"]
QJSRT["QuickJS runtime"]
REM["RemoteExecutor"]
REMRT["Remote runner"]
WQJS["QuickJsExecutor\nhost: worker"]
THREAD["Worker thread"]
RUNNER["core runner semantics"]
PROTO["@execbox/core/protocol<br/>messages + host session"]
PROTO["@execbox/core/protocol<br/>worker messages + host session"]
WQJSRT["QuickJS runtime in worker"]

HOST --> QJS --> QJSRT
HOST --> REM --> REMRT
HOST --> WQJS --> THREAD --> WQJSRT
QJS --> RUNNER
REM --> PROTO
REMRT --> RUNNER
WQJS --> PROTO
THREAD --> RUNNER
```
Expand All @@ -43,11 +37,11 @@ flowchart LR
That design gives QuickJS two useful properties:

- the runtime semantics are centralized in one runner implementation
- the same guest/tool-call model can be reused behind worker-hosted and remote transport boundaries
- the same guest/tool-call model can be reused behind worker-hosted execution

## Worker-Hosted QuickJS

`QuickJsExecutor` with `host: "worker"` uses a worker thread for lifecycle isolation, but it does not invent a second scripting model. It loads the same QuickJS session runner used by the inline QuickJS executor, reuses the shared QuickJS protocol endpoint inside the worker, and uses the shared `@execbox/core/protocol` host session on the parent side. By default it keeps a worker shell warm between executions; `mode: "ephemeral"` switches to a fresh worker per execution.
`QuickJsExecutor` with `host: "worker"` uses a worker thread for lifecycle isolation while keeping the same scripting model as inline QuickJS. It loads the same QuickJS session runner used by the inline QuickJS executor, reuses the shared QuickJS protocol endpoint inside the worker, and uses the shared `@execbox/core/protocol` host session on the parent side. By default it keeps a worker shell warm between executions; `mode: "ephemeral"` switches to a fresh worker per execution.

```mermaid
sequenceDiagram
Expand All @@ -70,19 +64,18 @@ sequenceDiagram

The available executors expose the same public result shape, but they enforce limits differently.

| Concern | QuickJS inline | Remote | QuickJS host: worker |
| ------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Timeout | QuickJS interrupt/deadline handling | Shared host-session timeout + remote cancel + transport teardown | Shared host-session timeout + worker cancellation + worker termination backstop |
| Memory | QuickJS runtime memory limit | Remote runtime decides the hard boundary; execbox still forwards limits | QuickJS memory limit inside worker, optional worker resource limits as backstop |
| Abort to host tools | Abort signal passed through core callback | Abort signal passed through shared host session | Abort signal passed through shared host session |
| Log capture | Captured inside runner | Captured inside the remote runner and returned over the transport boundary | Captured inside worker-side QuickJS runner |
| Concern | QuickJS inline | QuickJS host: worker |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
| Timeout | QuickJS interrupt/deadline handling | Shared host-session timeout + worker cancellation + worker termination backstop |
| Memory | QuickJS runtime memory limit | QuickJS memory limit inside worker, optional worker resource limits as backstop |
| Abort to host tools | Abort signal passed through core callback | Abort signal passed through shared host session |
| Log capture | Captured inside runner | Captured inside worker-side QuickJS runner |

## Security and Operational Trade-offs

- All executor modes provide defense-in-depth measures, not standalone hard hostile-code boundaries.
- All executor modes provide defense-in-depth measures around guest execution.
- QuickJS is the easiest operational default and has the cleanest shared runtime story.
- Remote execution keeps the same executor API while moving the runtime behind an app-defined boundary, but execbox deliberately does not ship the network stack for you.
- Worker-hosted QuickJS improves lifecycle isolation and hard-stop behavior, but not process-level trust isolation.
- Worker-hosted QuickJS improves lifecycle isolation and hard-stop behavior inside the host process.

## Pooled QuickJS Shells

Expand All @@ -99,7 +92,7 @@ Pooling is implemented at the host-shell layer, not at the QuickJS runtime layer
- `@execbox/core/protocol` exposes a small bounded async `createResourcePool()` helper that owns reusable shells, idle eviction, and `prewarm()` / `dispose()` support.
- Worker-hosted `QuickJsExecutor` pools `Worker` shells. Each shell owns one long-lived transport wrapper plus one attached QuickJS protocol endpoint.
- The worker entrypoint only attaches `attachQuickJsProtocolEndpoint(...)` once. That endpoint accepts one active `execute` message at a time and starts a fresh `runQuickJsSession()` for each message.
- Concurrency therefore comes from pool size, not from multiplexing several executions through one shell.
- Concurrency comes from pool size: each shell handles one active execution at a time.

At execution time the flow is:

Expand All @@ -116,7 +109,7 @@ If all shells are busy and the pool is already at `maxSize`, the next `acquire()
### Reuse And Eviction Rules

- Successful executions return the shell to the pool.
- Normal guest/runtime/tool failures also return the shell, because they do not imply a poisoned host shell.
- Normal guest/runtime/tool failures also return the shell, because the host shell remains reusable after those outcomes.
- `timeout` and `internal_error` results evict the shell, because those outcomes mean the worker or transport state may no longer be trustworthy.
- Idle pooled shells are evicted after `idleTimeoutMs`, down to `minSize`.
- `dispose()` tears down the executor-owned pool and any idle shells it still owns.
Expand All @@ -125,13 +118,7 @@ If all shells are busy and the pool is already at `maxSize`, the next `acquire()

In pooled mode, a worker can exit before the host session subscribes to close events. The pooled transport wrappers retain the first close reason and replay it to later `onClose(...)` subscribers, so an early shell death still resolves as `internal_error` instead of hanging the execution.

### What Is Not Pooled

- `QuickJsExecutor` stays in-process and ephemeral because there is no expensive transport shell to reuse.
- `RemoteExecutor` stays transport-factory based and ephemeral because transport ownership belongs to the caller.

## Choosing an Executor

- Choose `QuickJsExecutor` when you want the default backend with the least operational friction.
- Choose `RemoteExecutor` when you want the same execution API but need the runtime to live behind an application-defined process, container, VM, or network boundary.
- Choose `QuickJsExecutor` with `host: "worker"` when you want the QuickJS semantics off the main thread with a hard-stop termination path and low-latency pooled reuse by default.
Loading
Loading