商业产业 Hacker News AI 2026-08-17T18:33 正文 ok 4387 words

🔶 Show HN:HarnessRouter——面向 Agent Harness 的统一接口

英文标题:🔶 Show HN: HarnessRouter: Unified interface for agent harnesses

HarnessRouter 是一款可在本机通过 Docker 运行的 Agent Harness 管理工具,使用用户自己的 API 密钥和数据,不需要注册账号、云服务或遥测。其社区版实现了开放的 Unified Harness Protocol(UHP),并支持 Claude Code、Codex 与 Hermes 等后端。项目首次启动会下载约 700 MB 镜像,并安装相关 Agent CLI;用户随后可在控制台连接 Anthropic、OpenAI、Bedrock、OpenRouter 等模型提供商。对开发者而言,它提供了本地化的 Agent 任务执行、运行记录、文件产物管理以及 Starter Kit 示例,并将状态和数据集中保存在 Docker 卷中。
打开原文相关 3 · 机会 1
media36.5 KB media37.1 KB media1.3 KB media3.7 KB media3.1 KB media1.8 KB media1.3 KB media208.2 KB media67.0 KB media230.3 KB media620.9 KB media131.6 KB media451.6 KB media615.5 KB media146.6 KB media482.7 KB media49.6 KB

中文

在你自己的机器上运行 Agent Harness

在你自己的机器上运行 Agent Harness。 一个容器、你自己的 API 密钥、你自己的数据。配置一个 Harness,给它分配工作,然后观察它运行;无需账号、无需云端、无需遥测。社区版实现了 Unified Harness Protocol(UHP,统一 Harness 协议),这也是托管服务所实现的开放标准。

登录后的控制台:三个内置 Harness,已准备好运行


只需六步,完成后你将拥有一个正在运行的实例、一个已登录的控制台,以及一个已经回复你的 Agent。

你需要 Docker、约 4 GB 磁盘空间,以及一个模型提供商的 API 密钥。无需创建账号,也无需注册。整个流程中唯一的凭据是提供商密钥;除了调用其所属提供商外,它绝不会离开本机。

docker pull harnessrouter/harnessrouter

下载量约为 700 MB。

固定版本而非使用 latest

latest 是当前发布版本,再次拉取它即可完成升级。只有当你需要让两台机器运行完全相同的字节内容时,才应固定版本,例如在与团队共享的 compose 文件中指定一个版本号。发布版本列表见 Docker Hub。

请原样复制以下内容。其中没有任何占位符——没有提供商密钥,也没有密码。

docker run -d --name harnessrouter \
  -p 127.0.0.1:3000:3000 \
  -v harnessrouter:/data \
  harnessrouter/harnessrouter

如果 3000 端口已被占用,只需修改左侧的端口号(-p 127.0.0.1:3100:3000),因为容器内部始终监听 3000 端口。

这行命令各部分的作用

-p 127.0.0.1:3000:3000 使控制台只能从本机访问。这也使得使用默认登录凭据启动后再进行修改是安全的。

-v harnessrouter:/data 是所有持久化内容的存储位置:数据库、你的文件,以及首次启动时安装的 Agent CLI。保留该卷可以让之后每次启动都很快。

没有提供商密钥,因为你会在第 5 步从控制台连接提供商。这是更简便的方式:粘贴到表单中的密钥不会因输入错误而写入 shell 历史记录,之后修改密钥也不需要重新创建容器。

没有密码,因为实例会以默认登录凭据启动;第 4 步会提供该凭据并要求你修改它。

在 docker run 时自定义用户名和密码

有两个可选变量。设置它们后将替换默认值;控制台也不会再向你显示默认登录凭据。

docker run -d --name harnessrouter \
  -p 127.0.0.1:3000:3000 \
  -v harnessrouter:/data \
  -e HR_AUTH_USER=you \
  -e HR_AUTH_PASSWORD=the-password-you-chose \
  harnessrouter/harnessrouter

你在 HR_AUTH_PASSWORD= 后填写的内容就是密码——请在第 4 步中使用完全相同的内容登录。开始使用时并不需要这样做,之后从个人资料页面修改密码同样有效;此选项适用于由脚本创建、且不会有人打开浏览器的机器。

改用 Docker Compose

执行 cp .env.example .env,然后执行 docker compose up -d。请先阅读 docker-compose.yml:它会在所有网络接口上发布 3000:3000,而不是仅绑定到回环地址。修改那一行后,其行为就与上面的命令一致。

3. 等待它提示已就绪

暂时不要打开浏览器。 docker run 大约一秒钟后就会把终端提示符还给你,但控制台大约还需要半分钟才能就绪;在此之前,http://localhost:3000 会拒绝连接。这表示首次启动仍在进行,而不是容器损坏。

docker logs -f harnessrouter

等待出现 ready on :3000,然后打开浏览器:

[harnessrouter] installing Claude Code (Anthropic's terms apply)…
[harnessrouter] installing Codex (Apache-2.0)…
[harnessrouter] installing Hermes (check its upstream license before use)…
[harnessrouter] data=/data  backends available: claude codex hermes
[harnessrouter] ready on :3000

每个数据卷只需等待这一次。之后每次启动只需几秒钟,并且不会再打印任何安装日志。

其他日志行,以及为何首次启动较慢

backends available: 会列出实际安装成功的后端,因此安装失败的后端会被明确标出,而不是悄悄消失;其他后端仍可继续工作。

你还会看到以下一行。在第 4 步修改密码前,它会在每次启动时出现。对于仅绑定回环地址的实例,这是一项提醒,而非问题:

[harnessrouter] WARNING: using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.

Agent CLI 会在首次启动时下载,而不是随镜像一同分发;这是出于许可事实,而非打包偏好。Claude Code 根据 Anthropic 自身条款分发,hermes-agent 则声明没有许可证,因此二者都不能被重新分发到公开镜像中。在首次运行时安装它们,意味着你会根据相关条款自行从上游安装;这也是为什么在使用这两个后端之前应先阅读其条款。Codex 使用 Apache-2.0 许可证,也以同样方式安装,因此三者都会落在同一位置。

打开 http://localhost:3000,并使用以下凭据登录:

| | ---|--- 用户名 | harnessrouter 密码 | harnessrouter

登录界面

现在就修改密码:在右上角账户菜单中打开 Profile。保存后会重启控制台,大约需要一秒钟。

如果你在 docker run 时设置了 HR_AUTH_USER 或 HR_AUTH_PASSWORD,请改用这些凭据登录——此时默认凭据会被拒绝。

密码存储在哪里,以及忘记密码时该怎么办

此处公开打印默认凭据,正是它们属于占位符而不是秘密的原因;因此,在你修改密码前,容器会在每次启动时发出密码警告。

个人资料页面会同时要求输入当前密码和新密码,因此无人值守的浏览器标签页不能被用来接管实例。新凭据会存储在数据卷中(/data/selfhost-auth.json:包含用户名、盐值和哈希值,绝不保存密码),并从此优先于环境变量——即使数月前在 docker run 中设置的 HR_AUTH_PASSWORD,也无法悄悄撤销后续的密码修改。修改后,启动日志会变为说明实际密码来源:

[harnessrouter] sign in as 'harnessrouter' (credentials set from the profile page)

保存操作还会让其他所有浏览器退出登录;你当前使用的浏览器仍将保持登录状态。

忘记密码了?这里没有可发送的重置邮件,因此请删除 /data/selfhost-auth.json 并重启。实例会回退到 HR_AUTH_USER / HR_AUTH_PASSWORD。

5. 连接模型提供商

在完成这一步之前,任何内容都不会运行。 镜像中没有内置模型、试用密钥或隐藏的免费套餐。

打开 Integrations,然后点击 Add Integration。系统会询问三项内容:名称、提供商和你的 API 密钥。

在 Integrations 页面添加提供商

该提供商提供哪些模型无需你自行配置:产品会维护该列表,并在提供商发布模型时持续添加。选择提供商、粘贴密钥,该密钥所覆盖的模型就会显示在该行中。

运行多个提供商

Integrations 下方的映射决定由哪一个提供商服务特定模型。只有一个集成时,无需额外设置。

改用环境变量设置,适用于脚本化部署

一个连接定义提供商及其凭据;一项策略定义后端使用哪个连接。当机器由脚本创建且不会有人打开浏览器时,这种方式很有用:

-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'
-e HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE='{"chain":["anthropic"]}'

每个后端都有一个策略变量:…POLICY_CLAUDE、…POLICY_CODEX、…POLICY_HERMES。你自己的 OpenAI 兼容端点可使用同样的一对变量,只需增加 base_url,并使用 "provider":"openai",而不是 .env.example 中仍显示的 "openai-api":

-e HR_SECRET_GLOBAL_HARNESS_CONN_LOCAL='{"name":"local","provider":"openai","api_key":"…","base_url":"https://api.example.com/v1"}'
-e HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX='{"chain":["local"]}'

并非每个提供商都适配每个后端;不适配的组合会静默失败:经过长时间等待后,该轮回复会为空,而不会报错。Integrations 页面不存在这个问题,因为它只会向你提供可用的提供商。

连接 provider 可使用它的后端
anthropic Claude Code、Hermes
openai Codex、Hermes
openrouter Codex、Hermes
azure-foundry Codex、Hermes
bedrock Claude Code、Hermes
tokenrouter Claude Code、Codex、Hermes
vercel Claude Code、Codex、Hermes

未连接任何提供商的后端会如何提示

它会明确告知你情况;如果你完全跳过此步骤,将得到以下信息:

{"error":{"type":"invalid_request_error","code":"invalid_input","message":"no provider configured for backend 'codex'. Add an integration for a provider that serves 'gpt-5.4-mini', or configure a connection policy"}}

6. 给它一些工作

Tasks → New Task。 在左侧切换器中选择一个 Harness,在消息框旁选择一个模型,然后输入任务。回复会实时流式返回:Agent 执行的每一条命令、触碰的每一个文件,以及最终答案。

一项任务:请求、执行的命令、它写入的文件和测试结果

安装至此完成。状态由 SQLite 和一个 Docker 卷中的文件组成。删除该卷,实例就会消失;复制该卷,就等于迁移了实例、Harness、对话记录以及全部内容。

截图中发生了什么

截图中的任务要求创建一个带测试的小型工具。Agent 编写了它,构建了一个包含特意植入重复项的 fixture 文件树,运行了测试套件,并返回 2 tests ran, OK。这是一个你可以验证的答案,而不是只能相信的答案。它生成的所有内容都保留在对话记录中,你可以逐个文件带走,也可以将全部内容打包为 zip 下载。


Starter Kit 是经过实践的示例,用于展示这个工具可以面向哪些目标。

每个 Starter Kit 都是完整的 Agent 产品,而非一段代码片段:包括一个应用、一个被配置为驱动该应用的 Agent,以及教会该 Agent 编写相应格式内容的技能。先使用一个,然后阅读它:每个 Kit 都是开源的。未来会持续增加更多 Kit;你的实例会列出当前拥有的 Kit。

尚未启动任何 Kit 时的 Starter Kits 页面

启动时只会问一个问题:要在哪种运行环境上运行它。

启动对话框传达的信息

每张卡片都会在你启动前标明其使用的基础能力和模型,因此你可以在实际消耗之前了解一个 Kit 将产生什么开销。卡片显示什么取决于你在第 5 步中提供的密钥:上方截图中的实例连接了三个提供商;而只连接一个提供商的实例,会在每张卡片上都推荐该提供商。

没有对应密钥的运行环境也会被列出,但处于禁用状态,并会说明原因:

Hermes · deepseek-v4-pro · 未连接。请添加一个提供此模型的提供商后再使用它。

对话框给出的推荐只是建议,你可以推翻它,而不是必须接受的默认选择。

一个 deck 就是一段对话。请求制作演示文稿后,Agent 会设计它:先确定结构,再建立样式系统,然后逐页制作。它工作时幻灯片会不断出现,因此当整体方向不对时,你可以在只需修改两页时提出意见,而不是等到二十页后再改。

下面这份 deck 来自一句话:“一份面向新工程师、解释什么是容器镜像的 5 页演示文稿。”

Slides 编辑器:对话顶部的句子、下方的运行过程,以及生成的 deck

右侧面板中看到的是什么

那是运行过程,而不是进度条。它先确定结构、构建样式系统、检查画布能够接受的内容、编写 deck,然后进行验证;这些步骤都会在执行过程中显示出来。

这里没有任何内容只是幻灯片图片:画布上的每个元素都是一个真实对象,因此你可以拖动、调整大小、重新输入文字,或者在同一段对话中要求它再优化一次。

行是你的数据。一个 Agent 列会在每一行上运行你的某个 Harness,将左侧的列作为输入,表格会逐单元格自行填充。点击 Run 后,它会按行顺序填充,显示实时计数和 Stop 按钮,因为当一列有一千行时,你应该能够随时改变主意。

这个示例的初始请求是:“help me build a sheet, i wanna use this to map investors in silicon valley. the goal is to provide this one to investors outside of SV the startups invested by investors based SV.”

Sheets 编辑器:对话顶部的请求、真实的投资者行,以及 Agent 列在每一行上的输出

这个表格是如何构建的

根据这句话,Agent 确定了所需列、判断哪些列应由人工填写以及哪些列应由 Agent 填写,并编写了 p

English / 原文

Run agent harnesses on your own machine.

Run agent harnesses on your own machine. One container, your own API keys, your own data. Configure a harness, give it work, watch it run, with no account, no cloud, and no telemetry. Community Edition implements the Unified Harness Protocol (UHP), the open standard the hosted service implements too.

The console after signing in: three built-in harnesses, ready to run


Six steps, and at the end of them you have a running instance, a signed-in console, and an agent that has answered you.

You need Docker, about 4 GB of disk, and an API key from a model provider. There is no account to create and nothing to sign up for. The provider key is the only credential in the story, and it never leaves the box except to call the provider it belongs to.

docker pull harnessrouter/harnessrouter

About 700 MB to download.

Pinning a version instead of latest

latest is the current release, and pulling it again is how you upgrade. Pin a version only when you need two machines to run the same bytes, by naming a version in a compose file you share with a team. Releases are listed on Docker Hub.

Copy this as it is. Nothing in it is a placeholder — no provider key, no password.

docker run -d --name harnessrouter \
  -p 127.0.0.1:3000:3000 \
  -v harnessrouter:/data \
  harnessrouter/harnessrouter

If port 3000 is already busy, change only the left-hand number (-p 127.0.0.1:3100:3000), because the container always listens on 3000 inside.

What each part of that line does

-p 127.0.0.1:3000:3000 keeps the console reachable only from this machine. That is what makes it safe to start on a default login and change it afterwards.

-v harnessrouter:/data is where everything durable lives: the database, your files, and the agent CLIs installed on the first start. Keeping that volume is what makes every later start fast.

No provider key, because you connect a provider from the console in step 5. That is the shorter road: a key pasted into a form cannot be misspelled into a shell history, and changing it later does not mean recreating the container.

No password, because the instance starts on a default login that step 4 gives you and asks you to change.

Choosing your own username and password at docker run

Two optional variables. Set them and they replace the defaults; the console never shows you the default login again.

docker run -d --name harnessrouter \
  -p 127.0.0.1:3000:3000 \
  -v harnessrouter:/data \
  -e HR_AUTH_USER=you \
  -e HR_AUTH_PASSWORD=the-password-you-chose \
  harnessrouter/harnessrouter

Whatever you put after HR_AUTH_PASSWORD= is the password — sign in with exactly that in step 4. You do not need this to get started, and changing the password from the profile page later works just as well; it exists for a box built by a script, where nobody is going to open a browser.

Using Docker Compose instead

cp .env.example .env, then docker compose up -d. Read docker-compose.yml first: it publishes 3000:3000 on every interface rather than on loopback. Change that one line and it behaves like the command above.

3. Wait for it to say it is ready

Do not open the browser yet. docker run gives your prompt back in about a second, but the console needs roughly another half a minute, and until then http://localhost:3000 refuses the connection. That is the first start still working, not a broken container.

docker logs -f harnessrouter

Wait for ready on :3000, then open the browser:

[harnessrouter] installing Claude Code (Anthropic's terms apply)…
[harnessrouter] installing Codex (Apache-2.0)…
[harnessrouter] installing Hermes (check its upstream license before use)…
[harnessrouter] data=/data  backends available: claude codex hermes
[harnessrouter] ready on :3000

This wait happens once per volume. Every start after it takes a few seconds and prints no install lines at all.

The other lines, and why the first start is the slow one

backends available: lists what actually installed, so a backend that failed is named rather than silently missing, and the others still work.

You will also see this line, and it comes back on every start until you change the password in step 4. On a loopback-only instance it is a reminder rather than a problem:

[harnessrouter] WARNING: using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.

The agent CLIs are fetched on the first start rather than shipped in the image, and that is a licensing fact rather than a packaging preference. Claude Code is distributed under Anthropic's own terms and hermes-agent declares no license at all, so neither can be redistributed inside a public image. Installing them on first run means you install them yourself, from upstream, under those terms — which is also why you should read them before you use those two backends. Codex is Apache-2.0 and arrives the same way, so all three land in one place.

Open http://localhost:3000 and sign in with:

|
---|---
Username | harnessrouter
Password | harnessrouter

The sign-in screen

Change the password now , from Profile in the account menu at the top right. Saving restarts the console, which takes about a second.

If you set HR_AUTH_USER or HR_AUTH_PASSWORD at docker run, sign in with those instead — the defaults are then refused.

Where the password lives, and what to do if you forget it

Printing the defaults here is what makes them a placeholder rather than a secret, which is why the container warns about the password on every start until you change it.

The profile page asks for the current password as well as the new one, so an unattended tab cannot be used to take over the instance. New credentials are stored on the data volume (/data/selfhost-auth.json: a username, a salt and a hash, never the password) and take precedence over the environment from then on — an HR_AUTH_PASSWORD set at docker run months ago cannot quietly undo a password change. After one, the start-up line changes to say where the real password came from:

[harnessrouter] sign in as 'harnessrouter' (credentials set from the profile page)

Saving also signs out every other browser. Yours stays signed in.

Forgot it? There is no reset email to send, so delete /data/selfhost-auth.json and restart. The instance falls back to HR_AUTH_USER / HR_AUTH_PASSWORD.

5. Connect a model provider

Nothing runs until you do this. There is no bundled model, no trial key, and no free tier hiding in the image.

Open Integrations and press Add Integration. It asks three things: a name, the provider, and your API key.

Adding a provider on the Integrations page

Which models that provider serves is not your problem to configure: the product keeps that list and adds to it as providers ship models. Pick the provider, paste the key, and the models it covers appear on the row.

Running more than one provider

The mappings underneath the integrations decide which one serves a given model. With a single integration there is nothing to set.

Setting it from the environment instead, for a scripted deploy

A connection names a provider and its credential; a policy says which connection a backend uses. Useful when the box is built by a script and nobody is going to open a browser:

-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'
-e HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE='{"chain":["anthropic"]}'

There is one policy variable per backend: …POLICY_CLAUDE, …POLICY_CODEX, …POLICY_HERMES. An OpenAI-compatible endpoint of your own takes the same pair with a base_url added, and "provider":"openai" rather than the "openai-api" that .env.example still shows:

-e HR_SECRET_GLOBAL_HARNESS_CONN_LOCAL='{"name":"local","provider":"openai","api_key":"…","base_url":"https://api.example.com/v1"}'
-e HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX='{"chain":["local"]}'

Not every provider fits every backend, and a pairing that does not fit fails quietly: the turn comes back empty after a long wait rather than erroring. The Integrations page does not have this problem, because it only offers you providers that work.

Connection provider Backends that can use it
anthropic Claude Code, Hermes
openai Codex, Hermes
openrouter Codex, Hermes
azure-foundry Codex, Hermes
bedrock Claude Code, Hermes
tokenrouter Claude Code, Codex, Hermes
vercel Claude Code, Codex, Hermes
What a backend with nothing connected says

Forthcoming about it, which is what you get if you skip this step entirely:

{"error":{"type":"invalid_request_error","code":"invalid_input","message":"no provider configured for backend 'codex'. Add an integration for a provider that serves 'gpt-5.4-mini', or configure a connection policy"}}

6. Give it something to do

Tasks → New Task. Pick a harness in the switcher on the left, choose a model next to the message box, and type. The turn streams back as it happens: every command the agent runs, every file it touches, and the answer at the end.

A task: the request, the commands, the files it wrote, and the test result

That is the whole install. State is SQLite and files on one Docker volume. Delete the volume and the instance is gone; copy it and you have moved the instance, harnesses, transcripts and all.

What is happening in that screenshot

That one asked for a small utility with tests. The agent wrote it, built a fixture tree with duplicates planted in it, ran the suite, and came back with 2 tests ran, OK, which is an answer you can check rather than one you have to trust. Everything it produced is on the transcript to take away, a file at a time or the lot as a zip.


Starter kits are worked examples, and they are here to show you what this can be pointed at.

Each one is a whole agent product rather than a snippet: an app, an agent configured to drive it, and the skill that teaches that agent the format it writes. Use one, then read it: every kit is open source. More arrive over time; your instance lists the ones it has.

The Starter Kits page, before any kit has been launched

Launching asks one question: what to run it on.

What the launch dialog is telling you

Each card names the base and the model it will run on before you launch it, so you can see what a kit is about to spend before it spends it. What it names depends on the keys you gave it in step 5: the screenshot above is an instance with three providers connected, and an instance with one will recommend that one on every card.

The runtimes you have no key for are listed but disabled, with the reason on them:

Hermes · deepseek-v4-pro · Not connected. Add a provider that serves this model to use it.

What the dialog recommends is a suggestion you can overrule, not a default you have to accept.

A deck is one conversation. Ask for a presentation and the agent designs it: structure first, then a style system, then slide by slide. Slides appear while it works, so when the shape is wrong you can say so while there are two slides to change instead of twenty.

The deck below came from one sentence: "A 5-slide deck explaining what a container image is, for new engineers."

The Slides editor: the sentence at the top of the conversation, the run underneath it, and the deck it produced

What you are looking at in the panel on the right

That is the run, not a progress bar. It settled the structure, built a style system, checked what the canvas would accept, wrote the deck, then validated it, and it says so as it goes.

Nothing here is a picture of a slide: every element is a real object on the canvas, so you can drag it, resize it, retype it, or ask for another pass in the same conversation.

Rows are your data. An agent column runs one of your harnesses on every row, with the columns to its left as input, and the sheet fills itself cell by cell. Press Run and it fills in row order with a live count and a Stop button, because a column of a thousand rows is a thing you should be able to change your mind about.

This one opened with "help me build a sheet, i wanna use this to map investors in silicon valley. the goal is to provide this one to investors outside of SV the startups invested by investors based SV."

The Sheets editor: the request at the top of the conversation, real investor rows, and an agent column's output on each one

How that sheet got built

From that sentence, the agent decided the columns, worked out which of them a person fills in and which one an agent should, and wrote the per-row prompt itself. The rows came from a follow-up, "search some real data and from internet" , and it went and found four real investors with their firm's own profile pages rather than inventing plausible ones.

If the agent-column menu says you have no other agents

An agent column runs one of your other agents, and a sheet will not run itself. So on an instance where Sheets is the only thing you have launched, the column menu has nothing to offer and says so:

Choose an agent… · You have no other agents yet. Create one, then choose it here.

Harnesses → Add Harness is the fix: a base, a model, a name, and it is ready in seconds. The picker then lists it with the model it runs on. If you add one while a sheet is open, reload the sheet first, because the list is read when the page loads.

Say what you want to understand and point it at a database. The agent reads your schema, writes a query per question, picks the chart that answers it, and lays the panels out. Opening the dashboard re-runs every query, so what you see is the database now, not a snapshot from whenever it was built.

Launching the Dashboards kit: what to run it on, and which database to read

This is the one kit with setup, and it is two fields: the connection and the sample-rows switch.

The Dashboards kit: both turns of the conversation on the right, and the live panels they produced

How that dashboard got built, and why its numbers are worth trusting

It opened with "Revenue by month and the top 5 countries by revenue, plus total paid revenue." Both turns of that conversation are in the panel on the right: the first built it, and the second, "Enrich the dashboard like this" with a picture of the layout attached, is where the panels you see came from.

There is nowhere in a dashboard to type a number. Every figure on that page is the result of a query that ran when the page opened, which is the property that makes it worth trusting. Ask for a change and it runs each query before it wires it into a panel, so a panel that renders is a panel whose query works.

Connecting a database

Three things to know before you connect one.

The container has to be able to reach it. If your database is another container, put both on the same user-defined network so the database's name resolves. Docker's default bridge has no DNS, so on it only the container's IP works, and that IP changes:

docker network create hr-net
docker network connect hr-net my-postgres
docker run -d --name harnessrouter --network hr-net \
  -p 127.0.0.1:3000:3000 -v harnessrouter:/data … harnessrouter/harnessrouter

Then my-postgres:5432 works as a host in the connection string. A database on the host machine rather than in a container is reachable at host.docker.internal on Docker Desktop, or via --add-host=host.docker.internal:host-gateway on Linux.

SetHR_SECRET_KEY. Connection strings are encrypted at rest under a key derived from it, and without it the server refuses to store one rather than writing your production credential to disk in plaintext:

-e HR_SECRET_KEY=a-long-random-passphrase

Keep it. Change it and the stored connections can no longer be decrypted, and you reconnect them.

Use a read-only database account. Every statement is checked and only SELECT is allowed, non-SELECT, multiple statements and data-modifying CTEs are refused, and on PostgreSQL the query additionally runs in a READ ONLY transaction. That check is a parser, and a parser is a thing that can be wrong. An account that has been granted SELECT and nothing else is a second defence that does not depend on ours being right:

CREATE USER dashboards WITH PASSWORD '…';
GRANT CONNECT ON DATABASE shop TO dashboards;
GRANT USAGE ON SCHEMA public TO dashboards;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dashboards;

The connection string is resolved at the moment a query runs. The agent's sandbox never receives it. It gets a tool that runs SELECTs, and neither does the browser.

Sample rows are a per-connection switch, on by default: the agent sees a few real rows per table so it can tell a status column from a category one. Turn it off and it sees table and column names and types and no values at all.

Describe a film and get one back: it plans the shots, renders each, lays them out on a canvas you can rearrange, and assembles them into a single video you can download. Clips render in the background, so you keep working while they arrive.

Warning

This is the one kit that spends real money per second of output rather than per turn, because every shot is a generation. Try it once you already know what the console is doing.


An agent harness is the runtime layer around a model; Codex, Claude Code, and Hermes are harnesses. In this repo's API you also create harness objects: a saved configuration whose base is one of those runtimes, plus a model, instructions, and limits. A task is one run of that configuration, a real conversation against a real POSIX workspace with bash and git, streamed back as it happens.

HarnessRouter Community Edition implements UHP for both: an OpenAI Responses-compatible API for running turns, harness CRUD, sessions, streaming, cancellation, and idempotency. The console is a thin client over that API; anything the UI does, you can do from curl.

The console is the hosted product's console. Not a cut-down rebuild: the same pages, the same components, the same API client. Surfaces that need a service a single box doesn't have, such as accounts, billing, and marketplace, are simply not shown.

Supported harnesses: Codex, Claude Code, and Hermes, installed on first run rather than shipped in the image, for the license reasons in step 3. Review each tool's terms before you use it.

Bring your own key. Your provider credentials are read from the environment at start-up and handed to the agent directly. They are never written into the image, never committed, and never sent anywhere but your provider.

  • Your keys, your bills, your data. Nothing leaves the box except calls to your model provider.
  • Real workspaces. Agents get bash, git, and a filesystem, their native environment, not a sandbox emulation.
  • The same API as the hosted product. Not a reduced fork: the same /v1 surface, so anything you build against it keeps working if you later move to the hosted service.
  • Actually self-contained. No control plane to phone home to, no managed database.

The Unified Harness Protocol

This repository is both an implementation and a standard. The protocol the gateway speaks is specified, versioned and testable in protocol/, and documented at unifiedharnessprotocol.org:

|
---|---
Specification | Ten normative chapters, version 2026-08-11
Machine-readable | OpenAPI 3.1 + JSON Schema 2020-12, generated from one source
Conformance suite | passing it is what "conformant" means, and what earns the right to the UHP name
Governance | How the standard changes, and the naming and conformance policy

This edition is the reference implementation. The most recent published run passes at class Full, against 0.3.0. The standard can be implemented without HarnessRouter Cloud : it is an HTTP contract, and nothing in it requires a hosted service. Run the suite against your own server:

pip install -e protocol/conformance
uhp-conformance --base-url https://your-server --api-key "$KEY" --class full

Choosing backends, and building with a browser

Backends are installed into your data volume rather than baked into the image, so which ones you want is a run-time setting:

docker run -e HR_BACKENDS=claude,codex,hermes ...   # the default

Known issue: any value that leaves outhermes makes the container exit immediately with status 1 and no error message. claude, codex and claude,codex all do it, and the last line in the log is the install line for the backend it was working on, so it reads as if the install killed it, which it did not. Until that is fixed, leave HR_BACKENDS unset.

Chromium is genuinely an image layer, so it stays a build flag:

docker build -t harnessrouter --build-arg WITH_BROWSER=1 .
What the entrypoint sets for you Variable Default Why
HR_BACKING local SQLite + files on /data. No external storage.
HR_IDENTITY_MODE off One box, one owner; an accounts system would be ceremony with nothing behind it.
HR_CREDIT_GATE off Metering is a hosted concern.
POOL_MGMT_ENDPOINT http://127.0.0.1:8081 The runner is in this container.
HR_POOL_AUTH none No cloud identity to present to a loopback runner.
HR_SANDBOX_TRUST owner You own the box, the agent and the key, so the key is handed over directly rather than brokered.
HARNESS_WORKSPACE /data/workspaces One directory per session, on the volume, so a restart doesn't discard work in flight.
HR_WORKSPACE_TTL_HOURS 72 Idle session workspaces are removed after this. They rehydrate from their checkpoint, so this costs time, not work. 0 keeps them forever.
HARNESS_INTERNAL_KEY generated Per-container; never leaves the process tree.

The console is optional: it is a thin client over the same API. On a default install that API is reached through the console's own port, and the login gate covers it too , so a call needs the session cookie. Sign in once and keep the cookie:

curl -s -c hr.cookies http://localhost:3000/api/selfhost/login \
  -H 'content-type: application/json' \
  -d '{"username":"harnessrouter","password":"harnessrouter"}'   # your password, not the default
# {"ok":true}

Then run a turn. The gateway speaks the Responses API:

curl -s -b hr.cookies http://localhost:3000/api/harness/v1/responses \
  -H 'content-type: application/json' \
  -d '{"input":"Reply with exactly this and nothing else: it works.",
       "metadata":{"harness_id":"codex"},
       "model":"gpt-5.4-mini",
       "stream":false}'


{"id":"resp_284e450bc2be4de8bea94c4af6030292","object":"response","created_at":1786822334,
 "status":"completed","error":null,"incomplete_details":null,"previous_response_id":null,
 "model":"gpt-5.4-mini",
 "output":[{"id":"msg_3d71e018c6584abbb063ee16d9a36e75","type":"message","status":"completed",
            "role":"assistant",
            "content":[{"type":"output_text","text":"it works.","annotations":[]}]}],
 "store":true,
 "usage":{"input_tokens":10878,"output_tokens":34,"total_tokens":10912},
 "metadata":{"session_id":"hsessa79756fab07a4bf58fa072be24d5ce59"}}

That turn is not a side channel: it appears in the console under Tasks , against the same harness, with its full transcript. The console and the API are the same instance seen twice.

The rest of the surface

harness_id accepts one of the built-in ids the console shows on the Harnesses page: codex, claude-code, hermes, or the id of a harness you created. Harness CRUD (/v1/harnesses), the model catalog (/v1/models), sessions (/v1/sessions/{id}/turns, /cancel) and task listing (/v1/traces) are all on the same prefix. Set "stream":true for server-sent events instead of one response at the end.

If the box is one nobody else can reach, HR_AUTH_DISABLED=1 removes the gate entirely and the same calls work with no cookie at all.

Putting it on a public URL

The console can create harnesses, read every task transcript, and run an agent with your provider key.

Warning

Change the password before anyone else can reach the instance. The defaults are printed right here, which makes them a placeholder, not a secret; the container warns on every start while the default is still in place. HR_AUTH_DISABLED=1 removes the gate entirely, which is only reasonable on a machine nobody else can reach.

For TLS, keep the console on loopback and put a terminating proxy in front. With Caddy that is one file and a real certificate, automatically:

console.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3000 {
        flush_interval -1      # agent turns stream for minutes; never buffer them
    }
}

The flush_interval -1 matters: without it a proxy buffers the event stream and the console looks frozen until the turn ends.

Pin the tag, and do not run 0.1.x or 0.2.0: they have no sign-in gate at all, so an instance running them is open to anyone who can reach the port. 0.3.0 is the first release with one.

Why changing the password restarts the console

Changing it from Profile signs out every other browser and restarts the console. That restart is what makes "signed out everywhere" true rather than merely displayed: the gate reads its signing key once at start-up and cannot be told about a change in place. A task that is mid-turn runs straight through it.

Moving to the hosted service

When a harness is working the way you want, Harnesses → Push to cloud copies it into a hosted HarnessRouter account with your hosted API key.

This is deliberately one-way. Local is where you iterate; once promoted, the hosted copy is the source of truth. There is no "pull from cloud", so a harness can never be live in two places each claiming to be current. Your hosted key is used for that one request and is never stored.

┌─ container ─────────────────────────────────────────────┐
│  UI (Next.js)  :3000  ← the only published port         │
│      │ same-origin proxy                                │
│  Gateway       :8080  Responses API, harness CRUD       │
│      │ loopback                                         │
│  Runner        :8081  one agent CLI per session         │
└─────────────────────────┬───────────────────────────────┘
       /data (volume): SQLite, files, secrets, workspaces

The gateway and the runner listen on loopback inside the container and are not publishable; the console's port is the way in, which is why the login gate ships inside the image rather than in whatever proxy happens to sit in front.

Sessions run concurrently and are isolated: each gets its own workspace directory, its own conversation state, and its own checkpoint. Turn concurrency defaults to the machine's core count. This box cannot scale sandboxes on demand the way the hosted deployment does, so the limit is what it can actually run.

Storage sits behind a small adapter interface for records, files, and secrets. This repo ships the local implementations; the hosted deployment supplies its own against the same interface. That seam is why this is genuinely the same codebase rather than a fork that drifts.

Apache-2.0, see LICENSE. Third-party notices are in NOTICE.

The agent CLIs are not redistributed here; they are installed on first run under their own licenses. Review them before enabling a backend.