Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Run a self-hosted agent platform's sandboxes on Tenki.
| Example | Platform |
| --- | --- |
| [DeerFlow](examples/deerflow-tenki/) | Community sandbox provider for ByteDance's SuperAgent harness |
| [OpenHermit](examples/openhermit-tenki/) | Sticky per-agent microVMs for an agent fleet |

### Migrating from another provider

Expand Down Expand Up @@ -117,7 +118,7 @@ These open-source projects ship a Tenki provider or backend out of the box:
| [DeerFlow](https://github.com/bytedance/deer-flow) | Sandbox provider for ByteDance's SuperAgent harness — [example](examples/deerflow-tenki/) |
| [AgentBox](https://github.com/madarco/agentbox) | Provider for running parallel agents in sandboxed VMs — [example](examples/agentbox-tenki/) |
| [ComputeSDK](https://github.com/computesdk/computesdk) | Tenki provider for the multi-provider compute toolkit — [example](examples/computesdk-tenki/) |
| [OpenHermit](https://github.com/HCF-STUDIOS/openhermit) | Sandboxed exec backend for AI agent fleets |
| [OpenHermit](https://github.com/HCF-STUDIOS/openhermit) | Sandboxed exec backend for AI agent fleets — [example](examples/openhermit-tenki/) |

## Contributing

Expand Down
55 changes: 55 additions & 0 deletions examples/openhermit-tenki/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# OpenHermit agents on Tenki sandboxes

[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.

## Point an agent at Tenki

> **Version note:** the Tenki backend merged after OpenHermit's latest npm release (`0.10.0`), so until the next release ships, run from a source checkout — `git clone https://github.com/HCF-STUDIOS/openhermit && cd openhermit && npm install`, then substitute `npm run dev:cli --` wherever `hermit` appears below (see the repo's Development section).

```bash
hermit setup # DATABASE_URL, tokens

echo 'TENKI_API_KEY=tk_...' >> ~/.openhermit/gateway/.env
hermit gateway start

hermit agents create main --no-sandbox # skip the default Docker sandbox
hermit sandbox add --agent main --type tenki \
--config '{"project_id": "default"}'
hermit agents start main
hermit chat --agent main # the agent's shell now runs on Tenki
```

`--no-sandbox` matters: without it, `agents create` auto-provisions the gateway's default sandbox preset (Docker), and the Tenki `sandbox add` then conflicts on the `default` alias.

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):

| Key | Default | Meaning |
| --- | --- | --- |
| `cpu_cores` / `memory_mb` / `disk_size_gb` | `2` / `4096` / `10` | microVM size |
| `agent_home` | `/home/tenki` | workspace mount point and default `cwd` |
| `timeout_ms` | `300000` | per-command timeout (exit 137 on expiry) |
| `workspace_id` | token's own scope | explicit Tenki workspace |
| `base_url` | Tenki cloud | self-hosted / staging endpoint |

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 — or pick it per agent with `hermit agents create main --sandbox tenki-default` (no `--no-sandbox` needed on either path).

## What the backend does with it

- **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.
- **Commands run as `sh -c`** in `agent_home`, with per-agent pass-through secrets injected as env vars on every exec.
- **Skills sync as file uploads** over Tenki's data plane; syncs queued while the sandbox is unreachable replay when it reattaches.

## Verify

```bash
npm install
node verify.mjs
```

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.

## Notes

- The agent runtime requires `TENKI_API_KEY` (or `TENKI_AUTH_TOKEN`) in the **gateway's** environment (`~/.openhermit/gateway/.env`), not per agent.
- Sticky sessions pause rather than terminate when idle; pausing is free on Tenki, and `ensure()` resumes them transparently on the next command.
- Tenki confines file I/O to `/home/tenki` — keep `agent_home` under it.
9 changes: 9 additions & 0 deletions examples/openhermit-tenki/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "openhermit-tenki",
"private": true,
"type": "module",
"description": "Cookbook example — OpenHermit agents on Tenki microVM sandboxes: the sticky-session exec backend, verified.",
"dependencies": {
"@tenkicloud/sandbox": "^0.5.4"
}
}
77 changes: 77 additions & 0 deletions examples/openhermit-tenki/verify.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Proves the Tenki-facing contract OpenHermit's `tenki` exec backend relies on
* (apps/agent/src/core/backends/tenki.ts), with no gateway or PostgreSQL needed:
* a sticky session boots, commands run via `sh -c` with env pass-through, files
* round-trip (the skill-sync path), and — what OpenHermit leans on across
* gateway restarts — the same session reattaches by id via `client.get()`.
* Token/workspace from env (CI) or ~/.config/tenki/config.yaml (local `tenki login`).
* Exits non-zero on any failure.
*/
import { TenkiSandbox } from "@tenkicloud/sandbox";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";

const cfg = (key) => {
try {
const c = readFileSync(`${homedir()}/.config/tenki/config.yaml`, "utf8");
return (c.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1] ?? "").trim();
} catch {
return "";
}
};

const authToken = process.env.TENKI_AUTH_TOKEN || process.env.TENKI_API_KEY || cfg("auth_token");
const workspaceId = process.env.TENKI_WORKSPACE_ID || cfg("current_workspace_id") || undefined;
if (!authToken) {
console.error("No token. Set TENKI_AUTH_TOKEN, or run `tenki login`.");
process.exit(1);
}

const client = new TenkiSandbox({ authToken });
const text = (bytes) => new TextDecoder().decode(bytes);
let session;
try {
// 1) boot a sticky session, as the backend's ensure() does
session = await client.createAndWait({
cpuCores: 1,
memoryMb: 1024,
sticky: true,
metadata: { agentId: "cookbook-openhermit-verify" },
workspaceId,
});

// 2) exec the way TenkiExecBackend.exec() does: sh -c, cwd agent_home, env passed through
const r1 = await session.run(["sh", "-c", 'echo "$GREETING $(python3 -c "print(6*7)")"'], {
cwd: "/home/tenki",
env: { GREETING: "hermit" },
});
if (!(r1.exitCode === 0 && text(r1.stdout).trim() === "hermit 42")) {
throw new Error(`exec: exit ${r1.exitCode}, stdout ${JSON.stringify(text(r1.stdout))}, stderr ${JSON.stringify(text(r1.stderr))}`);
}

// 3) the skill-sync path: mkdir + writeFile, then read back
await session.mkdir("/home/tenki/skills");
await session.writeFile("/home/tenki/skills/demo.md", "# demo skill\n");
if (text(await session.readFile("/home/tenki/skills/demo.md")) !== "# demo skill\n") {
throw new Error("skill file round-trip mismatch");
}

// 4) reattach by id — what the gateway does after a restart (resume if paused)
const again = await client.get(session.id);
if (again.state === "PAUSED") await again.resume();
const r2 = await again.run(["sh", "-c", "cat skills/demo.md"], { cwd: "/home/tenki" });
if (!text(r2.stdout).includes("demo skill")) throw new Error("reattached session lost workspace state");

console.log("✓ openhermit-tenki: sticky session → sh -c exec with env (hermit 42) → skill file sync → reattach by id → dispose");
} catch (e) {
console.error("✗ " + (e?.message ?? e));
process.exitCode = 1;
} finally {
if (session) {
try {
await session.closeIfOpen();
} catch {
/* self-reaps via idle/lifetime caps */
}
}
}
Loading