Start the harness
OpenWorker runs the agent loop, handles approvals and keeps sessions across restarts. Start it in your desktop app. Generate a starter using the source-checkout commands below. It wires the SDK into your desktop app.
import { Harness } from "@openworker/harness";
const harness = await Harness.start({
sandbox: "off", // Explicit host callbacks; no OS isolation.
});use openworker_sdk::{Harness, Sandbox};
let harness = Harness::builder()
.sandbox(Sandbox::Off)
.start().await?;Session data stays in your app’s directory under ~/.openworker/.
Customize storage, scratch space and runtime options
These settings are optional. Supply absolute, app-specific directories in your paths configuration.
const harness = await Harness.start({
executable: paths.runtime, // Optional: override the bundled executable.
stateDir: paths.state,
sessions: {
persist: true,
directory: paths.sessions,
},
scratch: { directory: paths.scratch },
compaction: { triggerAtContextFraction: 0.8 },
});let harness = Harness::builder()
.state_dir(&paths.state)
.persist_sessions(true)
.session_dir(&paths.sessions)
.scratch_dir(&paths.scratch)
.compact_at_context_fraction(0.8)
.start()
.await?;Sessions persist by default. Set persistence to false for in-memory sessions without restart recovery. Scratch directories are allocated only when a session requests one. Compaction summarizes older context as the conversation approaches the configured fraction of the model’s context window.
Session persistence is separate from long-term memory across sessions, logs and scratch files. Turning it off does not erase those other stores. In Rust, the harness runs inside your app’s process by default. In TypeScript, the SDK starts and manages the bundled Rust harness over private stdio. Either way, your users install and open one desktop app. Rust can also use a managed harness process when you need that separation.
Connect a model
Add a provider now or later, when the user opens Settings. Give the connection a name, choose its API, and select a model.
Connect to OpenAI with a key supplied by your user.
// Ask for the key through your app’s Settings screen.
const apiKey = await ui.requestApiKey("OpenAI");
await harness.setModelProvider({
name: "OpenAI",
api: "responses",
baseUrl: "https://api.openai.com/v1",
credentials: { apiKey, storage: "secure" },
});
// Read the model ID the user selected in Settings.
const selectedModel = { provider: "OpenAI", name: settings.modelId };use openworker_sdk::{Credentials, Model, ModelProvider};
// Ask for the key through your app’s Settings screen.
let api_key = ui.request_api_key("OpenAI").await?;
harness.set_model_provider(
ModelProvider::responses("OpenAI")
.base_url("https://api.openai.com/v1")
.credentials(Credentials::secure(api_key))
).await?;
// Read the model ID the user selected in Settings.
let selected_model = Model::new("OpenAI", &settings.model_id);Connect to Ollama running on this computer. Choose a downloaded model that supports tool calling.
// Start Ollama on this computer before connecting.
await harness.setModelProvider({
name: "Ollama",
api: "chat-completions",
baseUrl: "http://127.0.0.1:11434/v1",
});
// Use the exact model ID exposed by that server.
const selectedModel = { provider: "Ollama", name: settings.modelId };use openworker_sdk::{Model, ModelProvider};
// Start Ollama on this computer before connecting.
harness.set_model_provider(
ModelProvider::chat_completions("Ollama")
.base_url("http://127.0.0.1:11434/v1")
).await?;
// Use the exact model ID exposed by that server.
let selected_model = Model::new("Ollama", &settings.model_id);The local Ollama endpoint does not require a cloud API key. If your server requires a key, supply credentials in the same way as the cloud example. OpenWorker connects to Ollama; it does not download or launch the model here. Ollama API reference ↗
ui and settings are starter app helpers, not harness APIs. Pass keys to the harness for secure storage. Never put them in prompts, logs or browser storage.
Environment keys, secure storage and other providers
await harness.setModelProvider({
name: "OpenAI",
api: "responses",
baseUrl: "https://api.openai.com/v1",
credentials: {
apiKeyEnv: "OPENAI_API_KEY",
storage: "memory", // Supply again after a process restart.
},
});harness.set_model_provider(
ModelProvider::responses("OpenAI")
.base_url("https://api.openai.com/v1")
.credentials(Credentials::from_env("OPENAI_API_KEY")
.memory_only())
).await?;Secure storage uses the platform’s secret store and is the default for supplied credentials. Memory-only keys must be supplied again after a harness restart. If secure storage is unavailable, configuration fails rather than saving plaintext.
Select responses, chat-completions or anthropic for your provider’s API. Supply the matching endpoint and exact model ID. Changing a key is separate from changing the provider or model pinned to an existing session.
Create agent
Let’s build Arcade: an app that turns an idea into a playable game. Register a preview tool, give the agent its instructions, then send the first message.
await harness.registerTools({
namespace: "game",
tools: {
show_preview: {
description: "Show a playable HTML game in the app preview.",
inputSchema: {
type: "object",
properties: { html: { type: "string" } },
required: ["html"], additionalProperties: false,
},
execute: async ({ html }, context) => {
await preview.show(html, { signal: context.signal });
return { shown: true };
},
},
},
});use openworker_sdk::Tool;
use serde_json::json;
harness.register_tools("game", [
Tool::new("show_preview")
.description("Show a playable HTML game in the app preview.")
.input_schema(json!({
"type": "object",
"properties": { "html": { "type": "string" } },
"required": ["html"], "additionalProperties": false
}))
.handler(move |args, context| {
let preview = preview.clone();
async move {
preview.show(args, context.cancellation()).await?;
Ok(json!({ "shown": true }))
}
}),
]).await?;Here preview represents your app’s adapter for its isolated HTML view. Your callback runs in your app. The SDK handles its connection to the agent.
const builder = harness.createAgent({
name: "builder",
instructions: "Build self-contained HTML games. Show each game in the preview.",
defaultModel: selectedModel,
tools: ["game.show_preview"],
});
const session = await builder.newSession();
const run = await session.send("Build a neon Snake game.");
for await (const event of run.events()) {
ui.onAgentEvent(event); // Forward messages and activity to your UI.
}
const result = await run.result();use openworker_sdk::Agent;
let builder = harness.create_agent(
Agent::new("builder")
.instructions("Build self-contained HTML games. Show each game in the preview.")
.default_model(selected_model)
.tools(["game.show_preview"])
).await?;
let session = builder.new_session().await?;
let run = session.send("Build a neon Snake game.").await?;
let mut events = run.events();
while let Some(event) = events.next().await {
ui.on_agent_event(event?); // Forward messages and activity to your UI.
}
let result = run.result().await?;Both provider examples define the selected model used here. The starter routes approval requests and questions to its UI. Waiting for input is not a completed task.
Session overrides, recovery and tool permissions
const session = await builder.newSession({
model: { provider: "Ollama", name: settings.localModelId },
scratch: true, // Allocate a private workspace for this session.
});
// Save this reference in your app's project metadata.
const reference = session.reference();
// After restarting and configuring the harness:
const restored = await builder.resumeSession(reference);use openworker_sdk::SessionOptions;
let session = builder.new_session_with(
SessionOptions::default()
.model(Model::new("Ollama", &settings.local_model_id))
.scratch(true)
).await?;
// Save this reference in your app's project metadata.
let reference = session.reference().clone();
// After restarting and configuring the harness:
let restored = builder.resume_session(reference).await?;defaultModel is the agent’s default, not a permanent restriction. A session can override it. Configure the referenced provider first. If neither supplies a model, session creation returns an error. The resolved model stays with the session across restarts.
Only the tools listed on the agent become available to it. Harness policy and approvals apply before invocation. Application callbacks run with your app’s privileges; they are not automatically moved into a sandbox. Keep model-generated HTML isolated from credentials, Node access and privileged app commands.
Persist the session reference before starting work. Re-register callbacks after restarting; resuming a session does not repeat its last action. Stopping an event stream does not cancel execution. Use the session’s cancel operation for a Stop button, and shut down the harness when the app exits.
Rust event iteration uses futures_util::StreamExt, included by the starter. Adapt the illustrative preview handle to your app; validate JSON input before forwarding HTML to its isolated preview.
Build your app
The starters connect chat, model settings, approvals and a game preview. Use the examples above in the generated app, then customize the experience.
# From the repository checkout; build on the target OS.
cd harness
cargo build --release --locked -p openworker-sdk --bin openworker-harness
npm --prefix typescript-sdk ci
npm --prefix typescript-sdk run build
cd ..
node harness/desktop-starters/create.mjs ../my-arcade --template electron-game-builder
cd ../my-arcade
npm install
npm run dev
# Build the desktop package.
npm run package# From the repository checkout; build on the target OS.
cd harness
cargo build --release --locked -p openworker-sdk --bin openworker-harness
npm --prefix typescript-sdk ci
npm --prefix typescript-sdk run build
cd ..
node harness/desktop-starters/create.mjs ../my-rust-arcade --template tauri-game-builder
cd ../my-rust-arcade
npm install
npm run dev
# Build the desktop package.
npm run packageBuild on the target OS: Windows on Windows, Mac on macOS. The package bundles your app and native helper; validate platform runtime dependencies and signing before distribution.
Build requirements and distribution
TypeScript uses React + Electron. Rust uses React + Tauri. Both use Node.js for the frontend build; the Tauri starter also needs Rust and the platform’s native build tools. End users do not need those developer toolchains.
The ui, settings, preview and paths names illustrate your app adapters; they are not SDK exports. The generated starter contains complete UI/preview wiring that you can adapt. Callback tools execute in your app with its privileges. Sandboxed tool execution has separate platform prerequisites.
Set your app name and identity, configure code signing and macOS notarization, and test the installer on a clean machine before distributing it.
Advanced
Extend the same foundation without adding another agent loop.
Connect MCP tools
Use an existing MCP service alongside your application callbacks. Connect it to the harness, then select its qualified tool names in an agent.
await harness.connectMcpServer({
name: "design",
transport: "streamable-http",
url: settings.mcpEndpoint,
});
// Select "design.<tool_name>" in your agent's tools list.use openworker_sdk::McpServer;
harness.connect_mcp_server(
McpServer::streamable_http("design", &settings.mcp_endpoint)
).await?;
// Select "design.<tool_name>" in your agent's tools list.Connecting a server does not expose every tool to every agent. The service stays outside the harness sandbox unless separately isolated. This local MCP profile does not provide remote HTTP OAuth; select integrations supported by the configured transport. To launch a separate MCP executable that the harness owns, use startMcpServer instead.
Sandbox tool execution with NVIDIA OpenShell
Choose OpenShell when tools need an isolated execution environment. Configure its filesystem and network policy for the work you want to allow.
const harness = await Harness.start({
sandbox: { provider: "openshell" },
});use openworker_sdk::Sandbox;
let harness = Harness::builder()
.sandbox(Sandbox::OpenShell)
.start()
.await?;Provision Docker and NVIDIA OpenShell before enabling this profile. For Windows, the OpenShell deployment needs a supported Linux environment; it is not a native Windows sandbox. Qualify the setup on your target platform.
This setting governs tools routed into that execution environment. It does not isolate application callbacks running in your app, or external MCP services. Sandbox limits and human approval are independent controls.
Enable the auto-approver
Let a separate reviewer model evaluate proposed tool calls against the user’s intent. Choose a configured cloud or local model for the reviewer, independently of the working agent.
const builder = harness.createAgent({
name: "builder",
instructions: "Build self-contained HTML games and show the preview.",
defaultModel: selectedModel,
tools: ["game.show_preview"],
approvals: {
mode: "auto",
reviewerModel: selectedReviewerModel,
},
});use openworker_sdk::Approvals;
let builder = harness.create_agent(
Agent::new("builder")
.instructions("Build self-contained HTML games and show the preview.")
.default_model(selected_model)
.tools(["game.show_preview"])
.approvals(Approvals::auto(selected_reviewer_model))
).await?;selectedReviewerModel is a provider-and-model reference from Settings. If the reviewer cannot confidently approve, the harness pauses for a human. Hard restrictions still apply. Automatic review helps assess alignment; it is not a guarantee of safety or a bypass mode.