Build with Loom.js
This path begins with a deterministic, credential-free coding session. It then shows how the full plugin runtime, custom capabilities, secure model transports, project adapters, and browser execution fit around that session.
1. Prepare the repository
Stable Loom packages are MPL-2.0 licensed and configured for public npm publication as
@mgravey/loom-*. Work from this repository until the first authenticated
registry release is complete.
Requirements
- Node.js 20.19 or newer
- pnpm 11.7.0 through Corepack
- Chromium for the browser and extension integration suite
git clone <repository-url> loom.js
cd loom.js
corepack enable
corepack prepare pnpm@11.7.0 --activate
pnpm install --frozen-lockfile
pnpm build
pnpm check
Keep the complete pnpm check gate green before connecting a credential,
browser permission, or live provider. The suite is designed to run against synthetic
transports.
2. Learn the five boundaries
Loom Core is intentionally small. It validates plugins, owns lifecycle, and exposes typed capability and service registries. Product behavior comes from independently replaceable packages around it.
| Boundary | Responsibility | Starting package |
|---|---|---|
| Orchestration | Prompts, transcript, follow-ups, events, cancellation | @mgravey/loom-coding |
| Model | Translate the agent protocol to a provider or secure transport | @mgravey/loom-model-openai-responses |
| Tools | Validate operations and expose the smallest useful authority | @mgravey/loom-tool-workspace |
| Workspace | Own files in memory, OPFS, a selected directory, or another host | @mgravey/loom-workspace-fsa |
| Execution | Run a credential-free project snapshot in an injected runtime | @mgravey/loom-execution-browser |
You can use the lightweight createCodingSession() API directly, or compose
packages through @mgravey/loom-core when your application needs capability
discovery, dependency ordering, configuration, and full lifecycle control.
3. Create a project workspace
Begin in memory. It is deterministic, requires no permission prompt, and implements the
same ProjectWorkspace interface used by browser-backed projects. Declare a
parent directory before adding a nested file.
import {
createMemoryProjectWorkspace,
createProjectWorkspaceTools,
createWorkspaceTools,
workspacePath,
type WorkspaceTool,
} from '@mgravey/loom-tool-workspace';
const workspace = createMemoryProjectWorkspace({
entries: [
{ path: workspacePath('src'), kind: 'directory' },
{
path: workspacePath('src/main.js'),
kind: 'file',
content: 'console.log("Hello from Loom");\n',
},
],
});
const projectTools = createProjectWorkspaceTools(workspace);
const modelTools: readonly WorkspaceTool[] = Object.freeze([
...createWorkspaceTools(workspace),
projectTools.stat,
projectTools.createDirectory,
projectTools.move,
projectTools.remove,
projectTools.applyPatch,
]);
The coding agent receives JSON-safe text and project operations. Binary methods remain available to trusted host code, but are deliberately not exposed as model tools:
const image = await workspace.readBytes(workspacePath('assets/icon.png'));
await workspace.writeBytes(workspacePath('assets/icon-copy.png'), image);
Choose a different authority later
- Memory: disposable tests, uploads, and ZIP round-trips.
- OPFS: persistent browser-owned projects without host-directory access.
- File System Access: immediate writes inside a directory selected by the user.
-
Custom adapter: implement
ProjectWorkspacefor a database, remote repository, or trusted host.
4. Run your first coding session
Use a tiny deterministic model first. It proves session streaming and cleanup without introducing networking. Replace only the model after this path works.
import type { CodexModel } from '@mgravey/loom-agent-codex';
import { createCodingSession } from '@mgravey/loom-coding';
const model: CodexModel = {
async *generate() {
yield { type: 'text', text: 'The project is ready.' };
yield { type: 'completed' };
},
};
const session = createCodingSession({
model,
tools: modelTools,
configuration: {
maxTurns: 8,
instructions: 'Inspect the project and make only requested changes.',
},
});
try {
for await (const event of session.submit('Describe this project.')) {
if (event.type === 'text') process.stdout.write(event.text);
}
for await (const event of session.followUp('Now create a README.')) {
console.log(event);
}
} finally {
await session.dispose();
}
Session rules
- One operation may run at a time.
submit()andfollowUp()stream agent events.subscribe()observes state, transcript, agent, and error events.-
cancel()targets the active model, tool, or adapted runtime operation. dispose()is terminal; put it infinally.
5. Move to Core when composition grows
Core gives a fixed plugin graph deterministic initialization and reverse-order cleanup. Register public behavior as capabilities and private implementation collaboration as services.
import { createCodexAgentPlugin } from '@mgravey/loom-agent-codex';
import { createLoomRuntime } from '@mgravey/loom-core';
import { createProjectWorkspaceToolsPlugin } from '@mgravey/loom-tool-workspace';
const runtime = createLoomRuntime({
plugins: [
createProjectWorkspaceToolsPlugin(workspace),
createCodexAgentPlugin(),
],
});
await runtime.initialize();
await runtime.start();
try {
// Resolve typed capabilities and create application sessions here.
} finally {
await runtime.stop();
await runtime.dispose();
}
| Hook | Use it for |
|---|---|
| initialize | Register capabilities and services. Do not begin work. |
| start | Resolve optional integrations and begin background work. |
| stop | Stop work started by the plugin. |
| dispose | Release resources; tolerate partial initialization and repeated cleanup paths. |
Version 0.1 does not support hot installation, removal, restart, or dependency version negotiation.
6. Register an application tool
A generic tool is a typed capability. Give it a namespaced identity, validate all
untrusted input, honor cancellation, register it only during initialize,
and export one canonical typed key with the package that owns it.
import {
PLUGIN_API_VERSION,
definePlugin,
registerTool,
toolKey,
type Tool,
} from '@mgravey/loom-plugin-api';
import { pluginId, semanticVersion } from '@mgravey/loom-types';
interface EchoInput { readonly text: string }
interface EchoOutput { readonly text: string }
export const ECHO_TOOL_KEY =
toolKey<EchoInput, EchoOutput>('example/echo');
const echoTool: Tool<EchoInput, EchoOutput> = Object.freeze({
name: 'echo',
description: 'Return the supplied text.',
async execute(input, context) {
context?.signal?.throwIfCancelled();
if (
typeof input !== 'object' ||
input === null ||
typeof input.text !== 'string'
) {
throw new TypeError('echo requires string text.');
}
return Object.freeze({ text: input.text });
},
});
export const echoPlugin = definePlugin({
manifest: {
id: pluginId('example/echo-plugin'),
version: semanticVersion('0.1.0'),
apiVersion: PLUGIN_API_VERSION,
},
initialize(context) {
registerTool(context.capabilities, 'example/echo', echoTool);
},
});
const runtime = createLoomRuntime({ plugins: [echoPlugin] });
await runtime.initialize();
await runtime.start();
try {
const echo = runtime.capabilities.get(ECHO_TOOL_KEY);
console.log(await echo.execute({ text: 'Hello' }));
} finally {
await runtime.stop();
await runtime.dispose();
}
Core capability registration and model exposure remain separate decisions. To let the
coding agent use website behavior, pass a CodexTool with a unique
namespaced operation, precise description, JSON input/output schemas, caller policy,
and application-owned executor to createCodingSession().
import type { CodexTool } from '@mgravey/loom-agent-codex';
const selectedProductTool: CodexTool = Object.freeze({
name: 'selected_product',
operation: 'shop/selected-product',
description: 'Read the product selected in the website UI.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
price: { type: 'number' },
},
required: ['id', 'title', 'price'],
additionalProperties: false,
},
allowedCallers: ['direct'],
async execute(input, context) {
context?.signal?.throwIfCancelled();
if (Object.keys(input).length !== 0) {
throw new TypeError('No input is accepted.');
}
return readSelectedProductFromPage();
},
});
const session = createCodingSession({
model,
tools: [...modelTools, selectedProductTool],
});
Custom tools default to direct model calls. Grant programmatic access only when a hosted model program should receive that authority. The executor must still validate exact untrusted input and return JSON-safe output.
7. Connect a real model securely
Replace the deterministic model with a provider adapter. In browser products, inject a credential-free transport that calls your trusted backend. The page must never receive an API key or refresh credential.
import { createOpenAIResponsesModel } from '@mgravey/loom-model-openai-responses';
const model = createOpenAIResponsesModel({
model: 'your-supported-model',
transport: {
async send(request) {
// Forward the credential-free body to your trusted backend.
// The backend owns the API key and POST /v1/responses.
return secureResponsesBackend.send(request);
},
},
});
The documented Platform Responses adapter and the experimental ChatGPT-authenticated extension are separate integrations. The workbench uses the latter to demonstrate an extension-owned credential and cross-origin transport boundary; the Loom library does not require it.
A browser page cannot make the current ChatGPT Codex request directly. Use the extension transport shown by the example or a trusted server. Device-code login alone does not make cross-origin inference requests possible.
8. Execute code in the browser
Browser runtimes receive only a command and a copied project snapshot. They never accept
credentials, cookies, headers, a host fetch, or arbitrary environment
variables.
import { createQuickJsExecutionRuntime } from '@mgravey/loom-execution-browser';
const quickjs = createQuickJsExecutionRuntime({
loadModule: () => import('quickjs-emscripten'),
});
try {
const result = await quickjs.execute(
{
command: 'qjs',
arguments: ['main.js'],
timeoutMs: 10_000,
project: {
files: [{
path: 'main.js',
data: new TextEncoder().encode(
'console.log("Hello from QuickJS");\n',
),
}],
},
},
{
onOutput(output) {
const method = output.channel === 'stderr' ? 'error' : 'log';
console[method](output.chunk);
},
},
);
console.log('Exit code:', result.exitCode);
} finally {
await quickjs.dispose();
}
Available adapters
- QuickJS: small JavaScript execution with a virtual project.
-
WebContainer: Node.js-compatible execution; requires COOP/COEP
hosting headers. The package tool runs npm and restores dependencies from
package.jsonbefore later Node entrypoints. - Pyodide: Python/WASM execution with NumPy and Matplotlib loaded by default, plus compatible package installation through micropip.
The workbench exposes execution and package installation through schema-described
website tools. Pyodide saves open Matplotlib figures as PNG artifacts under
.loom/figures/, and the conversation displays them below the completed tool
row. Node dependency metadata persists, while node_modules and runtime
caches remain outside the project snapshot.
The selected workspace can also keep multiple resumable chats under
.loom/conversations/. Sidebar controls create and switch chats, delete the
selected chat, or clean all .loom metadata after confirmation. Conversation
records are not copied into execution runtimes.
Package inputs are limited to bounded registry requirements, but npm lifecycle scripts and imported Python packages still execute inside their isolated runtime. Pyodide accepts pure-Python or compatible WebAssembly wheels; WebContainer cannot load normal native Node addons.
Prefer a Worker for generated QuickJS or Pyodide code when hard UI responsiveness matters. WebContainer requires the documented cross-origin isolation headers.
9. Run the browser workbench
Build the workspace, load the experimental extension, authenticate from its options page, then open the example. The extension is optional for Loom.js itself and required only for this ChatGPT-backed composition.
pnpm build
python3 -m http.server 4173 --directory packages
- Open
chrome://extensionsand enable Developer mode. -
Choose Load unpacked and select
packages/extension-codex-experimental/dist. - Open the extension options, explicitly enable the experiment, and sign in with ChatGPT or use the device-code fallback.
- Copy the extension ID and open the workbench.
- Choose direct directory, memory, or persistent OPFS workspace mode.
- Import a folder/ZIP or open a project, run a request, inspect changes, then export a ZIP if needed.
The Python static server is enough for the workbench shell, QuickJS, and Pyodide. Use a server that adds Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers before selecting the WebContainer Node.js runtime.
10. Keep the authority narrow
- Never place API keys, refresh tokens, cookies, or account credentials in browser code, files, runtime environments, events, or logs.
- Treat every model response, extension message, imported folder, and ZIP as untrusted input.
- Use Loom’s bounded ZIP validators; never extract uploads with an unbounded convenience API.
- Choose read or read/write access in the sidebar before opening a direct folder. Chrome asks once when the folder opens; later runs follow that policy without repeat prompts. Read-only runtime filesystem changes are discarded.
- Keep hosted model programs read-only. Mutations must be direct, validated tool calls.
- Validate exact custom-tool input shapes, honor cancellation, and return JSON-safe data.
- Prefer dedicated Workers for generated QuickJS or Pyodide code when practical.
- Run
pnpm checkbefore every integration or release change.
Current 0.1 limitations
The documentation is explicit about what is not yet generalized:
- Workspace packages are private and unpublished.
- Generic schema-bearing tools are agent-callable, but each application still owns tool discovery, user authorization, input validation, and UI presentation.
- The browser run-file tool and ProjectWorkspace synchronization are example-owned rather than a general execution policy in the agent package.
- The ChatGPT/Codex extension integration is unsupported compatibility work, not a documented public OAuth contract.
- Production extension packaging, release versioning, licensing, and multi-browser CI are pending.
For deeper design rationale, continue in the repository with
docs/architecture.md, docs/plugin-authoring.md, and the root
DOCUMENTATION.md index.