Skip to content

Commit 388df53

Browse files
opencolinclaude
andcommitted
Add OpenHermit exec backend example
Walkthrough for pointing an OpenHermit agent's sandbox at Tenki (HCF-STUDIOS/openhermit#239): hermit sandbox add --type tenki, config keys, gateway env, and presets. verify.mjs proves the Tenki contract the backend relies on -- sticky session, sh -c exec with env pass-through, skill-file sync, reattach by session id -- against the live API, with no gateway or PostgreSQL needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0ccf76d commit 388df53

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ Give an agent a sandboxed place to run the code it writes.
6262
| [Composio](examples/composio-tenki/) | Tenki tools in a Composio agent |
6363
| [Covalent](examples/covalent-tenki/) | Each workflow task in its own microVM |
6464

65+
### Agent platforms
66+
67+
Run a self-hosted agent platform's sandboxes on Tenki.
68+
69+
| Example | Platform |
70+
| --- | --- |
71+
| [OpenHermit](examples/openhermit-tenki/) | Sticky per-agent microVMs for an agent fleet |
72+
6573
### Migrating from another provider
6674

6775
Each guide has side-by-side code and an API mapping.
@@ -107,7 +115,7 @@ These open-source projects ship a Tenki provider or backend out of the box:
107115
| [DeerFlow](https://github.com/bytedance/deer-flow) | Sandbox provider for ByteDance's SuperAgent harness |
108116
| [AgentBox](https://github.com/madarco/agentbox) | Provider for running parallel agents in sandboxed VMs |
109117
| [ComputeSDK](https://github.com/computesdk/computesdk) | Tenki provider for the multi-provider compute toolkit |
110-
| [OpenHermit](https://github.com/HCF-STUDIOS/openhermit) | Sandboxed exec backend for AI agent fleets |
118+
| [OpenHermit](https://github.com/HCF-STUDIOS/openhermit) | Sandboxed exec backend for AI agent fleets [example](examples/openhermit-tenki/) |
111119

112120
## Contributing
113121

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# OpenHermit agents on Tenki sandboxes
2+
3+
[OpenHermit](https://github.com/HCF-STUDIOS/openhermit) deploys fleets of AI agents as production services — a gateway holds durable state in PostgreSQL, and each agent gets a **sandbox** for its shell and workspace files. Since [openhermit#239](https://github.com/HCF-STUDIOS/openhermit/pull/239), `tenki` is a first-class sandbox type alongside `host`, `docker`, `e2b`, and `daytona`: each agent's workspace becomes one sticky [Tenki](https://tenki.cloud) microVM that survives gateway restarts.
4+
5+
## Point an agent at Tenki
6+
7+
```bash
8+
npm install -g openhermit
9+
hermit setup # DATABASE_URL, tokens
10+
11+
echo 'TENKI_API_KEY=tk_...' >> ~/.openhermit/gateway/.env
12+
hermit gateway start
13+
14+
hermit agents create main
15+
hermit sandbox add --agent main --type tenki \
16+
--config '{"project_id": "default"}'
17+
hermit agents start main
18+
hermit chat --agent main # the agent's shell now runs on Tenki
19+
```
20+
21+
Config keys (all optional except `project_id`, which OpenHermit's schema still requires — current Tenki no longer scopes sandboxes by project, so any identifier satisfies it):
22+
23+
| Key | Default | Meaning |
24+
| --- | --- | --- |
25+
| `cpu_cores` / `memory_mb` / `disk_size_gb` | `2` / `4096` / `10` | microVM size |
26+
| `agent_home` | `/home/tenki` | workspace mount point and default `cwd` |
27+
| `timeout_ms` | `300000` | per-command timeout (exit 137 on expiry) |
28+
| `workspace_id` | token's own scope | explicit Tenki workspace |
29+
| `base_url` | Tenki cloud | self-hosted / staging endpoint |
30+
31+
Operators can also register the same blob as a preset in `gateway.json` (`"sandboxPresets": { "tenki-default": { "type": "tenki", "config": { ... } } }`) and set `"autoProvisionSandbox": "tenki-default"` so every new agent lands on Tenki automatically.
32+
33+
## What the backend does with it
34+
35+
- **One sticky microVM per agent.** The backend creates the session with `sticky: true`, persists the session id, and after a gateway restart reattaches with `client.get(id)` — resuming a paused VM instead of recreating it, workspace intact.
36+
- **Commands run as `sh -c`** in `agent_home`, with per-agent pass-through secrets injected as env vars on every exec.
37+
- **Skills sync as file uploads** over Tenki's data plane; syncs queued while the sandbox is unreachable replay when it reattaches.
38+
39+
## Verify
40+
41+
```bash
42+
npm install
43+
node verify.mjs
44+
```
45+
46+
No gateway or database needed: [`verify.mjs`](verify.mjs) drives the exact Tenki surface the backend is built on — boot a sticky session → `sh -c` exec with env pass-through → skill-file round-trip → **reattach by session id** → dispose — against the live API. The backend's internals (state rows, replay queues) are covered by OpenHermit's own test suite.
47+
48+
## Notes
49+
50+
- The agent runtime requires `TENKI_API_KEY` (or `TENKI_AUTH_TOKEN`) in the **gateway's** environment (`~/.openhermit/gateway/.env`), not per agent.
51+
- Sticky sessions pause rather than terminate when idle; pausing is free on Tenki, and `ensure()` resumes them transparently on the next command.
52+
- Tenki confines file I/O to `/home/tenki` — keep `agent_home` under it.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "openhermit-tenki",
3+
"private": true,
4+
"type": "module",
5+
"description": "Cookbook example — OpenHermit agents on Tenki microVM sandboxes: the sticky-session exec backend, verified.",
6+
"dependencies": {
7+
"@tenkicloud/sandbox": "^0.5.4"
8+
}
9+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Proves the Tenki-facing contract OpenHermit's `tenki` exec backend relies on
3+
* (apps/agent/src/core/backends/tenki.ts), with no gateway or PostgreSQL needed:
4+
* a sticky session boots, commands run via `sh -c` with env pass-through, files
5+
* round-trip (the skill-sync path), and — what OpenHermit leans on across
6+
* gateway restarts — the same session reattaches by id via `client.get()`.
7+
* Token/workspace from env (CI) or ~/.config/tenki/config.yaml (local `tenki login`).
8+
* Exits non-zero on any failure.
9+
*/
10+
import { TenkiSandbox } from "@tenkicloud/sandbox";
11+
import { readFileSync } from "node:fs";
12+
import { homedir } from "node:os";
13+
14+
const cfg = (key) => {
15+
try {
16+
const c = readFileSync(`${homedir()}/.config/tenki/config.yaml`, "utf8");
17+
return (c.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1] ?? "").trim();
18+
} catch {
19+
return "";
20+
}
21+
};
22+
23+
const authToken = process.env.TENKI_AUTH_TOKEN || process.env.TENKI_API_KEY || cfg("auth_token");
24+
const workspaceId = process.env.TENKI_WORKSPACE_ID || cfg("current_workspace_id") || undefined;
25+
if (!authToken) {
26+
console.error("No token. Set TENKI_AUTH_TOKEN, or run `tenki login`.");
27+
process.exit(1);
28+
}
29+
30+
const client = new TenkiSandbox({ authToken });
31+
const text = (bytes) => new TextDecoder().decode(bytes);
32+
let session;
33+
try {
34+
// 1) boot a sticky session, as the backend's ensure() does
35+
session = await client.createAndWait({
36+
cpuCores: 1,
37+
memoryMb: 1024,
38+
sticky: true,
39+
metadata: { agentId: "cookbook-openhermit-verify" },
40+
workspaceId,
41+
});
42+
43+
// 2) exec the way TenkiExecBackend.exec() does: sh -c, cwd agent_home, env passed through
44+
const r1 = await session.run(["sh", "-c", 'echo "$GREETING $(python3 -c "print(6*7)")"'], {
45+
cwd: "/home/tenki",
46+
env: { GREETING: "hermit" },
47+
});
48+
if (!(r1.exitCode === 0 && text(r1.stdout).trim() === "hermit 42")) {
49+
throw new Error(`exec: exit ${r1.exitCode}, stdout ${JSON.stringify(text(r1.stdout))}, stderr ${JSON.stringify(text(r1.stderr))}`);
50+
}
51+
52+
// 3) the skill-sync path: mkdir + writeFile, then read back
53+
await session.mkdir("/home/tenki/skills");
54+
await session.writeFile("/home/tenki/skills/demo.md", "# demo skill\n");
55+
if (text(await session.readFile("/home/tenki/skills/demo.md")) !== "# demo skill\n") {
56+
throw new Error("skill file round-trip mismatch");
57+
}
58+
59+
// 4) reattach by id — what the gateway does after a restart (resume if paused)
60+
const again = await client.get(session.id);
61+
if (again.state === "PAUSED") await again.resume();
62+
const r2 = await again.run(["sh", "-c", "cat skills/demo.md"], { cwd: "/home/tenki" });
63+
if (!text(r2.stdout).includes("demo skill")) throw new Error("reattached session lost workspace state");
64+
65+
console.log("✓ openhermit-tenki: sticky session → sh -c exec with env (hermit 42) → skill file sync → reattach by id → dispose");
66+
} catch (e) {
67+
console.error("✗ " + (e?.message ?? e));
68+
process.exitCode = 1;
69+
} finally {
70+
if (session) {
71+
try {
72+
await session.closeIfOpen();
73+
} catch {
74+
/* self-reaps via idle/lifetime caps */
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)