Step-by-step onboarding

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
Terminal · from a fresh clone
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
Your first success criterion

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.

src/getting-started.ts
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 ProjectWorkspace for 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.

Continue src/getting-started.ts
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() and followUp() 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 in finally.

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.
The plugin graph is fixed

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.

src/echo-plugin.ts
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);
  },
});
Resolve and execute
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();
}
Model-callable website tools

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().

Expose page behavior to the agent
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.

Browser CORS and credential safety are separate constraints

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.json before 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.

Installed packages are untrusted code

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.

Terminal
pnpm build
python3 -m http.server 4173 --directory packages
  1. Open chrome://extensions and enable Developer mode.
  2. Choose Load unpacked and select packages/extension-codex-experimental/dist.
  3. Open the extension options, explicitly enable the experiment, and sign in with ChatGPT or use the device-code fallback.
  4. Copy the extension ID and open the workbench.
  5. Choose direct directory, memory, or persistent OPFS workspace mode.
  6. Import a folder/ZIP or open a project, run a request, inspect changes, then export a ZIP if needed.
WebContainer hosting

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 check before 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.