Files
openworker/docs/harness/SERVICE-ARCHITECTURE.html

78 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>OpenWorker — Deployment architecture</title><style>
:root{color-scheme:light;--ink:#18202d;--muted:#596578;--line:#dce2eb;--blue:#2761cc}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:#fff;color:var(--ink);font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header,main,footer{max-width:1160px;margin:auto;padding:0 32px}header{padding-top:65px}h1{font-size:clamp(36px,5vw,60px);letter-spacing:-2px;line-height:1.1;margin:14px 0 20px}h2{font-size:32px;line-height:1.2;letter-spacing:-1px;margin:0 0 15px}h3{font-size:19px;line-height:1.4;margin:28px 0 12px}p{max-width:850px;margin:12px 0}.brand{font-weight:650;font-size:19px}.intro,.lead{color:var(--muted);font-size:18px}.status{font-size:14px;color:#515b6c;border-left:3px solid #a9c1ef;padding-left:14px;margin:24px 0}nav{display:flex;flex-wrap:wrap;gap:8px 24px;padding:20px 0 26px;border-bottom:1px solid var(--line)}a{color:#244d88;text-decoration:none}a:hover{text-decoration:underline}section{padding:58px 0;border-bottom:1px solid var(--line);scroll-margin-top:25px}.diagram{border:1px solid var(--line);border-radius:18px;margin-top:28px;overflow:auto;padding:8px;background:#fff}svg{width:100%;min-width:760px;display:block;font-family:inherit}.node-title{font-size:17px;font-weight:650;fill:#1e293b}.node-sub{font-size:13px;fill:#586578}.edge-label{font-size:12px;fill:#40506a}.group-label{font-size:14px;fill:#53627b;font-weight:600}.legend{display:flex;gap:28px;margin:14px 4px 28px;font-size:12px;color:var(--muted)}.legend span{display:flex;align-items:center;gap:8px}.legend i{width:28px;border-top:2px solid var(--blue)}.legend i.data{border-top:2px dashed #167c74}.flow{display:grid;grid-template-columns:repeat(4,1fr);gap:22px;list-style:none;counter-reset:flow;padding:0;margin:28px 0 34px}.flow li{counter-increment:flow;font-size:14px;color:var(--muted)}.flow li:before{content:counter(flow);display:block;background:#edf2fc;color:var(--blue);font-size:13px;font-weight:650;width:27px;height:27px;line-height:27px;text-align:center;border-radius:50%;margin-bottom:10px}.flow b{color:var(--ink);display:block}details{border:1px solid var(--line);border-radius:12px;margin:18px 0;overflow:hidden}summary{padding:15px 20px;cursor:pointer;font-weight:600;font-size:15px;background:#f7f8fb}pre{margin:0;padding:24px;background:#17202d;color:#e5edf7;font:13px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:auto;tab-size:2}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.91em}pre code{font-size:inherit}.note{font-size:14px;color:var(--muted);max-width:none}.callout{background:#f1f5fc;border:1px solid #d8e3f4;padding:18px 22px;border-radius:12px;margin:24px 0}.callout p{margin:0;max-width:none}table{width:100%;border-collapse:collapse;font-size:14px;margin:24px 0}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:14px 12px}th{color:#586578;font-size:12px;font-weight:600;background:#f7f9fc}footer{padding-top:25px;padding-bottom:50px;font-size:13px;color:var(--muted)}.bootstrap{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:26px 0}.bootstrap article{border:1px solid var(--line);padding:20px;border-radius:12px;background:#f7f9fc}.bootstrap strong{display:block;margin-bottom:10px}.bootstrap p{font-size:14px;margin:0;color:var(--muted)}@media(max-width:760px){header,main,footer{padding-left:20px;padding-right:20px}.flow,.bootstrap{grid-template-columns:1fr 1fr}h2{font-size:27px}table{display:block;overflow:auto}pre{font-size:12px}}@media print{nav{display:none}header{padding-top:20px}section{break-before:page;padding-top:20px}details{break-inside:avoid}svg{min-width:0}pre{white-space:pre-wrap;font-size:10px}.flow{gap:12px}body{font-size:13px}h1{font-size:36px}h2{font-size:26px}a{color:inherit}}.overview{display:block;padding:8px 0 28px}.overview ul{margin:0;padding-left:22px;max-width:980px}.overview li{padding:8px 0 8px 3px}.overview li::marker{color:#8795aa}.overview a{font-weight:600}.overview span{display:block;color:var(--muted);font-size:15px;line-height:1.6;margin-top:2px}@media print{.overview{display:block}.overview li{padding-top:4px;padding-bottom:4px}}
.overview h2{font-size:24px;letter-spacing:-.5px;margin:12px 0}.overview-intro{font-size:16px;color:var(--muted);max-width:900px;margin:0 0 22px}
.use-case{max-width:930px;font-size:16px;line-height:1.7;margin:18px 0 24px;color:var(--muted)}.use-case strong{color:var(--ink);font-weight:600}
</style></head><body><header><div class="brand">OpenWorker Service</div><h1>One agent API.<br>Three deployment choices.</h1><p class="intro">Choose where the harness runs and where its tools execute.</p><p class="status">Architecture review · 27 October 2026. Local SDK calls below exist today; <code>Harness.connect()</code>, RuntimeProfiles and service management are the proposed contract. No implementation change is implied.</p><nav class="overview" aria-labelledby="overview-title">
<h2 id="overview-title">What this document covers</h2>
<p class="overview-intro">This guide walks through three ways to run OpenWorker, with diagrams and minimal SDK examples for each. It also explains how agents can share a workspace and how to establish the first administrator when installing the service.</p>
<ul>
<li><a href="#local">Local process</a><span>Your application starts and manages the harness as a child process.</span></li>
<li><a href="#colocated">Same-node service</a><span>Your application connects to a service whose harness worker and tool sandbox run on the same node.</span></li>
<li><a href="#separate">Separate execution</a><span>The harness worker runs in the service cluster; tools execute on a separate node, cluster or provider.</span></li>
<li><a href="#sharing">Shared workspace</a><span>Multiple agents share an execution environment, using separate directories or worktrees when needed.</span></li>
<li><a href="#bootstrap">First administrator</a><span>A trusted installer establishes the first administrator for a cloud or local deployment.</span></li>
</ul>
</nav></header><main><section id="local"><h2>1. Local managed harness process</h2><p class="lead">Your backend owns a Rust child process. The browser talks to your application; the harness has no public API port.</p><p class="use-case"><strong>Start with a self-contained application.</strong> For a prototype, an internal tool, or a coding lab with one container per learner, the application can run its own harness and tools together. You get a working agent without operating a separate agent service. This fits when the application and its agent workload can be deployed and scaled as one unit.</p><div class="diagram"><svg viewBox="0 0 1080 455" role="img" aria-label="Local managed process: backend uses private stdio; harness calls tools and stores session state."><defs><marker id="a1" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#2761cc"/></marker><marker id="d1" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#167c74"/></marker></defs><rect x="20" y="20" width="1040" height="320" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="40" y="50" class="group-label">One application host / dedicated development container</text><rect x="45" y="100" width="250" height="110" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="170.0" y="129" class="node-title" text-anchor="middle">Application backend</text><text x="170.0" y="152" class="node-sub" text-anchor="middle">TypeScript SDK</text><text x="170.0" y="172" class="node-sub" text-anchor="middle">Owns the child process</text><rect x="415" y="100" width="250" height="110" rx="12" fill="#eaf1ff" stroke="#b9cef5"/><text x="540.0" y="129" class="node-title" text-anchor="middle">Harness process</text><text x="540.0" y="152" class="node-sub" text-anchor="middle">Agent loop · policy · sessions</text><rect x="785" y="100" width="250" height="110" rx="12" fill="#eaf7f3" stroke="#afd9ca"/><text x="910.0" y="129" class="node-title" text-anchor="middle">Local ToolRunner</text><text x="910.0" y="152" class="node-sub" text-anchor="middle">File / shell operations</text><text x="910.0" y="172" class="node-sub" text-anchor="middle">Workspace files</text><path d="M295 124 L415 124" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a1)" /><text x="355.0" y="108" class="edge-label" text-anchor="middle">private stdio</text><path d="M415 183 L295 183" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d1)" /><text x="355.0" y="204" class="edge-label" text-anchor="middle">events / results</text><path d="M665 124 L785 124" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a1)" /><text x="725.0" y="108" class="edge-label" text-anchor="middle">tool request</text><path d="M785 183 L665 183" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d1)" /><text x="725.0" y="204" class="edge-label" text-anchor="middle">tool result</text><rect x="415" y="250" width="250" height="70" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="540.0" y="279" class="node-title" text-anchor="middle">Session storage</text><text x="540.0" y="302" class="node-sub" text-anchor="middle">Outside the tool workspace</text><path d="M540 210 L540 250" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a1)" /><rect x="785" y="360" width="250" height="75" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="910.0" y="389" class="node-title" text-anchor="middle">Model endpoint</text><text x="910.0" y="412" class="node-sub" text-anchor="middle">Cloud or local provider</text><path d="M665 210 L850 360" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a1)" /><text x="757.5" y="325" class="edge-label" text-anchor="middle">model API</text></svg></div><div class="legend"><span><i></i>Commands / control</span><span><i class="data"></i>Results / event data</span></div><ol class="flow"><li><b>Start</b>The SDK launches the harness and connects over private stdio.</li><li><b>Submit</b>Your backend creates an agent/session and sends a message.</li><li><b>Execute</b>The harness calls the model and ToolRunner, recording execution state.</li><li><b>Return</b>Events and results flow back through the SDK to your UI.</li></ol><details open><summary>Minimal TypeScript</summary><pre><code>import { Harness } from &quot;@openworker/harness&quot;;
const harness = await Harness.start({
stateDir: &quot;/state/openworker&quot;,
sandbox: &quot;off&quot;, // Explicit dedicated-container development profile.
modelProviders: [primaryProvider],
});
const coder = harness.createAgent({
name: &quot;coder&quot;,
instructions: &quot;Build and test browser applications.&quot;,
tools: [&quot;coding.read_file&quot;, &quot;coding.write_file&quot;, &quot;coding.run_shell&quot;],
defaultModel: selectedModel,
});
const session = await coder.newSession({ workspace: &quot;/workspace&quot; });
const run = await session.send(&quot;Build and test a quiz app.&quot;);
for await (const event of run.events()) {
forwardToUI(event); // Text, tools, plans and pending interactions.
}
const result = await run.result();</code></pre></details><p class="note">Linux development example: <code>primaryProvider</code> and <code>selectedModel</code> are application-supplied configuration. Model keys stay in the backend. <code>sandbox: "off"</code> adds no OS isolation; use a dedicated container or select a supported sandbox for the workload. The app supervises the child, and shutdown stops its execution resources.</p></section><section id="colocated"><h2>2. Service cluster: compute and execution on one node</h2><p class="lead">Your backend connects to the service. A harness worker and its OpenShell sandbox share a node, while remaining separate execution boundaries.</p><p class="use-case"><strong>Scale agent capacity independently of your web application.</strong> As more users submit tasks, your web servers and agent workers may need different amounts of capacity. A service cluster lets several application backends submit work to a shared worker pool. Keeping each worker and its sandbox on the same node gives you a straightforward execution layout while you scale the pool.</p><div class="diagram"><svg viewBox="0 0 1080 500" role="img" aria-label="Service cluster with co-located harness worker and OpenShell tool execution."><defs><marker id="a2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#2761cc"/></marker><marker id="d2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#167c74"/></marker></defs><rect x="25" y="40" width="245" height="105" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="147.5" y="69" class="node-title" text-anchor="middle">Application backend</text><text x="147.5" y="92" class="node-sub" text-anchor="middle">Authenticated service SDK</text><text x="147.5" y="112" class="node-sub" text-anchor="middle">UI / CLI use the same API</text><rect x="390" y="15" width="665" height="150" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="410" y="45" class="group-label">Service control plane — not tied to one worker</text><rect x="410" y="68" width="245" height="75" rx="12" fill="#eaf1ff" stroke="#b9cef5"/><text x="532.5" y="97" class="node-title" text-anchor="middle">REST API + SSE</text><text x="532.5" y="120" class="node-sub" text-anchor="middle">Admission and event replay</text><rect x="790" y="68" width="245" height="75" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="912.5" y="97" class="node-title" text-anchor="middle">PostgreSQL</text><text x="912.5" y="120" class="node-sub" text-anchor="middle">Commands · leases · events</text><path d="M270 76 L410 76" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a2)" /><text x="340.0" y="62" class="edge-label" text-anchor="middle">REST</text><path d="M410 121 L270 121" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d2)" /><text x="340.0" y="145" class="edge-label" text-anchor="middle">SSE</text><path d="M655 91 L790 91" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a2)" /><text x="722.5" y="81" class="edge-label" text-anchor="middle">admit / read</text><path d="M790 125 L655 125" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d2)" /><text x="722.5" y="150" class="edge-label" text-anchor="middle">durable state</text><rect x="25" y="224" width="1030" height="257" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="45" y="254" class="group-label">One execution node — separate worker and sandbox boundaries</text><rect x="45" y="310" width="245" height="112" rx="12" fill="#eaf1ff" stroke="#b9cef5"/><text x="167.5" y="339" class="node-title" text-anchor="middle">Harness worker</text><text x="167.5" y="362" class="node-sub" text-anchor="middle">Agent loop · model calls</text><text x="167.5" y="382" class="node-sub" text-anchor="middle">Tool policy and approvals</text><rect x="660" y="310" width="375" height="112" rx="12" fill="#eaf7f3" stroke="#afd9ca"/><text x="847.5" y="339" class="node-title" text-anchor="middle">OpenShell sandbox</text><text x="847.5" y="362" class="node-sub" text-anchor="middle">ToolRunner + workspace</text><text x="847.5" y="382" class="node-sub" text-anchor="middle">Persistent storage binding</text><rect x="350" y="335" width="240" height="75" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="470.0" y="364" class="node-title" text-anchor="middle">OpenShell gateway</text><text x="470.0" y="387" class="node-sub" text-anchor="middle">Private authenticated channel</text><path d="M290 335 L350 355" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a2)" /><path d="M590 355 L660 335" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a2)" /><text x="625.0" y="321" class="edge-label" text-anchor="middle">tool request</text><path d="M660 390 L590 393" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d2)" /><path d="M350 393 L290 390" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d2)" /><text x="320.0" y="415" class="edge-label" text-anchor="middle">results</text><path d="M900 143 V195 H14 V337 H45" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a2)"/><text x="560" y="187" class="edge-label" text-anchor="middle">Worker claims commands; persists events before API replay</text><path d="M45 398 H30 V212 H940 V143" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d2)"/><text x="540" y="466" class="node-sub" text-anchor="middle">Gateway placement is independent. Provisioning/attachment use the registered provider adapter.</text></svg></div><div class="legend"><span><i></i>Commands / control</span><span><i class="data"></i>Results / event data</span></div><ol class="flow"><li><b>Admit</b>The API checks caller, agent release, RuntimeProfile and idempotency key.</li><li><b>Schedule</b>PostgreSQL records the request. A compatible worker claims the session.</li><li><b>Attach</b>The profile places the sandbox beside the worker; tools run inside it.</li><li><b>Observe</b>The worker records events. Any API replica can replay them over SSE.</li></ol><details open><summary>Minimal TypeScript</summary><pre><code>import { Harness } from &quot;@openworker/harness&quot;;
const service = await Harness.connect({
url: process.env.OPENWORKER_URL,
namespace: &quot;my-product&quot;,
credentials: { tokenEnv: &quot;OPENWORKER_TOKEN&quot; },
});
const coder = await service.agent(&quot;coder@v1&quot;); // Already deployed + active.
const session = await coder.newSession({
runtimeProfile: &quot;openshell-colocated&quot;,
idempotencyKey: &quot;project-42-coder&quot;, // Reuse only for this creation intent.
});
await saveProjectEnvironment(session.environmentId);
const run = await session.send(&quot;Build and test a quiz app.&quot;);
for await (const event of run.events()) {
forwardToUI(event); // Text, tools, plans and pending interactions.
}
const result = await run.result();</code></pre></details><p class="note">Proposed service API. An administrator has already deployed <code>coder@v1</code>, configured its model provider, and granted access to the profile. Strict same-node placement needs provider/scheduler support and verification; it must not be inferred from “same cluster” or silently relaxed.</p></section><section id="separate"><h2>3. Service cluster: execution on separate compute</h2><p class="lead">The agent loop stays on service workers. ToolRunner and files live on a separate execution node, cluster or provider.</p><p class="use-case"><strong>Give workspaces their own resources and lifetime.</strong> An app builder may keep a project workspace around between conversations, while some builds need far more CPU or memory than the agent loop. Separate execution lets you size sandbox capacity independently and retain project storage when execution stops. It also gives you a place to add specialized or external execution providers without moving your application backend.</p><div class="diagram"><svg viewBox="0 0 1080 500" role="img" aria-label="Service cluster with separate harness worker and OpenShell tool execution."><defs><marker id="a3" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#2761cc"/></marker><marker id="d3" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10" fill="#167c74"/></marker></defs><rect x="25" y="40" width="245" height="105" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="147.5" y="69" class="node-title" text-anchor="middle">Application backend</text><text x="147.5" y="92" class="node-sub" text-anchor="middle">Authenticated service SDK</text><text x="147.5" y="112" class="node-sub" text-anchor="middle">UI / CLI use the same API</text><rect x="390" y="15" width="665" height="150" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="410" y="45" class="group-label">Service control plane — not tied to one worker</text><rect x="410" y="68" width="245" height="75" rx="12" fill="#eaf1ff" stroke="#b9cef5"/><text x="532.5" y="97" class="node-title" text-anchor="middle">REST API + SSE</text><text x="532.5" y="120" class="node-sub" text-anchor="middle">Admission and event replay</text><rect x="790" y="68" width="245" height="75" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="912.5" y="97" class="node-title" text-anchor="middle">PostgreSQL</text><text x="912.5" y="120" class="node-sub" text-anchor="middle">Commands · leases · events</text><path d="M270 76 L410 76" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a3)" /><text x="340.0" y="62" class="edge-label" text-anchor="middle">REST</text><path d="M410 121 L270 121" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d3)" /><text x="340.0" y="145" class="edge-label" text-anchor="middle">SSE</text><path d="M655 91 L790 91" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a3)" /><text x="722.5" y="81" class="edge-label" text-anchor="middle">admit / read</text><path d="M790 125 L655 125" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d3)" /><text x="722.5" y="150" class="edge-label" text-anchor="middle">durable state</text><rect x="25" y="224" width="285" height="257" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="45" y="254" class="group-label">Service worker node</text><rect x="620" y="224" width="435" height="257" rx="20" fill="#f5f7fb" stroke="#d8dee9"/><text x="640" y="254" class="group-label">Separate execution node / cluster / VPC</text><rect x="45" y="310" width="245" height="112" rx="12" fill="#eaf1ff" stroke="#b9cef5"/><text x="167.5" y="339" class="node-title" text-anchor="middle">Harness worker</text><text x="167.5" y="362" class="node-sub" text-anchor="middle">Agent loop · model calls</text><text x="167.5" y="382" class="node-sub" text-anchor="middle">Tool policy and approvals</text><rect x="660" y="310" width="375" height="112" rx="12" fill="#eaf7f3" stroke="#afd9ca"/><text x="847.5" y="339" class="node-title" text-anchor="middle">OpenShell sandbox</text><text x="847.5" y="362" class="node-sub" text-anchor="middle">ToolRunner + workspace</text><text x="847.5" y="382" class="node-sub" text-anchor="middle">Persistent storage binding</text><rect x="350" y="335" width="240" height="75" rx="12" fill="#fff" stroke="#d1d9e6"/><text x="470.0" y="364" class="node-title" text-anchor="middle">OpenShell gateway</text><text x="470.0" y="387" class="node-sub" text-anchor="middle">Private authenticated channel</text><path d="M290 335 L350 355" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a3)" /><path d="M590 355 L660 335" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a3)" /><text x="625.0" y="321" class="edge-label" text-anchor="middle">tool request</text><path d="M660 390 L590 393" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d3)" /><path d="M350 393 L290 390" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d3)" /><text x="320.0" y="415" class="edge-label" text-anchor="middle">results</text><path d="M900 143 V195 H14 V337 H45" fill="none" stroke="#2761cc" stroke-width="2" marker-end="url(#a3)"/><text x="560" y="187" class="edge-label" text-anchor="middle">Worker claims commands; persists events before API replay</text><path d="M45 398 H30 V212 H940 V143" fill="none" stroke="#167c74" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#d3)"/><text x="540" y="466" class="node-sub" text-anchor="middle">Gateway placement is independent. Provisioning/attachment use the registered provider adapter.</text></svg></div><div class="legend"><span><i></i>Commands / control</span><span><i class="data"></i>Results / event data</span></div><ol class="flow"><li><b>Select</b>The application chooses an approved remote RuntimeProfile.</li><li><b>Bind</b>The service provisions or verifies the original execution and storage identities.</li><li><b>Execute</b>The provider channel carries tool requests/results across the boundary.</li><li><b>Recover</b>A lost result becomes an explicit unknown outcome; no blind tool replay.</li></ol><details open><summary>Minimal TypeScript</summary><pre><code>import { Harness } from &quot;@openworker/harness&quot;;
const service = await Harness.connect({
url: process.env.OPENWORKER_URL,
namespace: &quot;my-product&quot;,
credentials: { tokenEnv: &quot;OPENWORKER_TOKEN&quot; },
});
const coder = await service.agent(&quot;coder@v1&quot;); // Already deployed + active.
const session = await coder.newSession({
runtimeProfile: &quot;openshell-remote&quot;,
idempotencyKey: &quot;project-42-coder&quot;, // Reuse only for this creation intent.
});
await saveProjectEnvironment(session.environmentId);
const run = await session.send(&quot;Build and test a quiz app.&quot;);
for await (const event of run.events()) {
forwardToUI(event); // Text, tools, plans and pending interactions.
}
const result = await run.result();</code></pre></details><p class="note">OpenShell first. External-provider adapters require separate qualification. Model credentials stay on workers; execution receives only scoped tool credentials. A lost execution connection may leave a process running. Switching profiles does not migrate live files/processes.</p></section><section id="sharing"><h2>One workspace, multiple agents.</h2><p class="lead">The returned environment ID hides provider details. A coder and verifier can share a sandbox while keeping separate conversations and session memory.</p><p class="use-case"><strong>Let a coding agent and verifier work on the same project.</strong> A coding agent can implement a change while a verifier checks the resulting revision in another worktree within the same sandbox. Other teammates can use separate directories on that machine. They share the project environment while keeping their conversations separate. This is a sharing choice within a deployment, not a fourth deployment mode; the selected provider and RuntimeProfile must support it.</p><pre><code>const coding = await coder.newSession({
runtimeProfile: &quot;team-workspace&quot;, // Explicit shared profile.
workingDirectory: &quot;worktrees/coder&quot;,
});
const verification = await verifier.newSession({
environmentId: coding.environmentId,
workingDirectory: &quot;worktrees/verifier&quot;,
});</code></pre><p class="note">Proposed service API. Directories/worktrees must already exist through authorized setup. The service checks access, sharing policy and compatibility on every attachment. Directory separation is not a security boundary, and agents must coordinate ports and edits.</p><table><thead><tr><th>Service identity</th><th>Provider execution</th><th>Provider storage</th></tr></thead><tbody><tr><td><code>environmentId</code><br>Stable application reference</td><td>Verified sandbox/pod identity<br>Binding revision and generation</td><td>Workspace/volume references<br>Mounts, ownership and retention</td></tr></tbody></table><div class="callout"><p>Compute and storage have separate lifetimes. Replacing a pod must not silently replace its retained workspace. Cancelling one agent must not destroy another agent’s attachment.</p></div><h3>Data and control use separate durable paths</h3><table><thead><tr><th>Information</th><th>Local process</th><th>Service cluster</th></tr></thead><tbody><tr><td>Message / new run</td><td>Private stdio request</td><td>Authenticated REST → durable admission → worker</td></tr><tr><td>Text, tools, plans, results</td><td>SDK event stream</td><td>Worker → durable journal → API SSE → app/UI</td></tr><tr><td>Approval, answer, cancellation</td><td>Typed SDK control request</td><td>REST → priority control path → active worker</td></tr><tr><td>Workspace files</td><td>Configured local workspace</td><td>Bound execution storage; not the API filesystem</td></tr><tr><td>Reconnect</td><td>Resume the saved session/reference</td><td>Any API replica + session reference and event cursor</td></tr></tbody></table><p class="note">Closing a remote SDK connection does not cancel work. A control receipt means “accepted”, not “applied”. Service-generated session/run/environment IDs identify resources; an optional caller <code>idempotencyKey</code> deduplicates mutations. <code>requestId</code> traces a single HTTP attempt.</p></section><section id="bootstrap"><h2>Who authorizes the first administrator?</h2><p class="lead">The trusted installer. This works on cloud infrastructure, local Kubernetes and individual machines.</p><div class="bootstrap"><article><strong>1. Install privately</strong><p>The operator uses existing cloud, Kubernetes or host-admin permissions.</p></article><article><strong>2. Bootstrap once</strong><p>A private job creates an administrator record and a random, expiring token. Only its hash enters the auth registry.</p></article><article><strong>3. Deliver securely</strong><p>The token goes to a restricted secret store or owner-only file. The installer retrieves it using existing authority.</p></article><article><strong>4. Log in normally</strong><p>The CLI presents the token over TLS. Set up normal identities, test another administrator, then revoke bootstrap.</p></article></div><table><thead><tr><th>Deployment</th><th>Installer authority</th><th>Token destination</th></tr></thead><tbody><tr><td>Cloud Kubernetes</td><td>Cloud / Kubernetes credentials</td><td>Restricted secret manager or Kubernetes Secret</td></tr><tr><td>Local Kubernetes</td><td>Kubernetes administrator credentials</td><td>Restricted Kubernetes Secret</td></tr><tr><td>Docker / bare metal</td><td>Host administrator access</td><td>Owner-only private local file</td></tr></tbody></table><pre><code># Proposed CLI; token was delivered privately by installation.
ows login --url https://agents.example.com --token-file /private/bootstrap-token</code></pre><div class="callout"><p>No public “first caller becomes admin” endpoint. Repeating installation must not replace the administrator. Bootstrap retries reconcile the original operation; expired credentials do not reopen registration.</p></div><p class="note">Terraform configures infrastructure, certificate integration and secret-store access—not a long-lived admin token in its state. Local clusters use trusted TLS certificates too; only explicit loopback development allows HTTP. Service tokens, model credentials and internal mTLS identities are separate.</p><h3>Who controls cluster size?</h3><table><thead><tr><th>Layer</th><th>Owns</th></tr></thead><tbody><tr><td>Terraform / infrastructure operator</td><td>Cluster, node types, node-count bounds, IAM, storage and database</td></tr><tr><td>Helm</td><td>Installed service, worker pools, UI and controllers</td></tr><tr><td>Kubernetes autoscalers</td><td>Capacity adjustments within configured bounds</td></tr><tr><td><code>ows</code> / REST / management UI</td><td>Worker capacity, admission quotas, RuntimeProfiles and configuration revisions</td></tr></tbody></table><p class="note">Customers may bring an existing cluster; Terraform is optional. A pool has one scaling authority—manual, autoscaled or GitOps—to avoid competing writes. The UI uses the same REST API, with no direct database or cluster-admin access.</p></section></main><footer><a href="OPENWORKER-SERVICE.md">Read the complete service specification →</a><p>Review artifact only. OpenShell integration and the server reference already provide foundations; remote SDK, management APIs and placement policies need the implementation and acceptance gates in the spec.</p></footer></body></html>