Notizen aus der Praxis
Remote Hayate Agents Needed an Open Inbound Port
Until Sunday, a central dashboard could only manage a remote Hayate Agent by reaching an HTTPS listener on the agent's machine. Now the agent can dial out over WSS instead. Why opening that socket still grants no permission to run work, why a dropped request reports 'outcome unknown' instead of running twice, and what a load test showed about a 30 ms test sleep.
冬の宵
戸を開けずとも
声届くWinter evening.
Even without opening the door,
the voice arrives.
Say you run Hayate Agent (formerly the VanillaCore Agent) on a box behind a home router and want to manage it from a central dashboard. Until Sunday, the agent's side of that connection had to be an HTTPS listener: a host, a port, a TLS key and certificate, and an origin the central instance could reach. The documentation called DNS, certificates and firewall "explicit operator choices", which is honest and still means that the central instance needed a network path to an HTTPS listener on every agent it managed. On Sunday morning that changed. An agent can now dial out to the dashboard over a WebSocket on the same HTTPS connection, and nothing on the agent's machine has to listen.
Reversing the direction is the easy half. The hard half is everything an inbound port used to make obvious: who is allowed to connect, what an open socket is allowed to do, and what happens to a request when the connection drops halfway. A socket that registers itself as an agent the moment it connects would trade a firewall problem for a trust problem. I like the answer the change gives. Opening a channel grants no permission to run work, a human approves every new agent, and a lost request is reported as lost instead of being sent again. Plugins pull request #67 brought the backend, WebUI #8 the browser side, and both merged at 11:40 Europe/Berlin.
All times are Europe/Berlin. The window is Sunday 8 November, 08:00 to 20:00.
Connecting is not being admitted
The new flow has three separate steps, and none of them implies the next. On the central dashboard, an administrator creates an invitation for an outbound agent. The code is single-use and expires after five minutes, and the relay stores only its SHA-256 hash. On the target, the administrator enters that code, either in the WebUI or with /agent-link-outbound enroll <origin> <code>. The target then opens its connection, and the central dashboard shows it as incoming, with its identity and the capabilities that both its offer and the invitation allow. Only when someone chooses "Approve and register" does the agent appear in AgentHub and become usable. The documentation says it plainly: "Merely opening a socket does not register an agent or grant permission to execute work."
Permissions only shrink along the way. The target lists the capabilities it offers (projects, streams, messages, activity), and the invitation can reduce that list but not expand it. Local projects stay invisible unless the target maps them explicitly, and conversations without a project are shared only when the target sets shareStandalone: true. Agent credentials never ride on browser cookies. Permanent tokens live in the backend vaults, and the relay keeps hashes.
A lost request stays lost
The failure handling is the part I would read twice. If the connection drops while the central side waits for an answer, the pending request fails with an explicit error, and the relay code spells it out: "Agent disconnected; outcome unknown. No automatic retry". A timeout and a failed send end with the same "outcome unknown. No automatic retry". A reconnect creates a new connection generation, with backoff starting at 500 milliseconds and doubling up to 30 seconds by default, and nothing from the old generation is replayed. If you retry a message yourself, it keeps its original UUID, and creating a stream over this transport requires an idempotency UUID, so the agent's core can recognize a duplicate. For agent work, an honest "I don't know whether that ran" beats a silent second execution.
Some things stay deliberately absent. There is no interactive shell, no generic HTTP forwarding and no private host-control API over this channel, and frames, pending requests and buffers are bounded. TLS is verified at both hops, and redirects are refused. Removing a registration revokes the agent's authorization and closes the socket, but it does not delete histories or undo work the agent already accepted. Direct HTTPS with a listener remains supported, and the outbound transport is selected explicitly. Installing the overlay alone enables neither side. Startup changes go through the agent's normal configuration owner.
The dashboard no longer needs an agent
The same day moved the browser interface out of the plugins repository. At 08:56, plugins #64 deleted the webui/ folder (2,064 lines removed) and left an API-only backend in dashboard-api/. The WebUI now lives in its own repository and is published at github.com/hayate-agent/webui. At 12:42, plugins #70 added a standalone administration service that runs the identity, credential vault, AgentHub and WSS relay without Core, without a model and without a shell. WebUI #10 (12:38) matches it in the browser: in standalone mode it hides the local agent, execution and project controls instead of showing a fake local agent, and it requires picking a remote agent explicitly. Put together, a central dashboard can manage agents that sit behind NAT without running an agent itself. The pull request authors report browser integration tests that do exactly this, from invitation through approval and scoped work to revocation. Those were not rerun for this post.
The test that trusted 30 milliseconds
Core changed in two places, and neither touches runtime code. #304 (10:39) is documentation: it fixes a stale link to the old plugins WebUI folder and separates the legacy bundled loopback UI from the current setup with an independent frontend and backend. #305 (17:56) fixes a test. According to its description, a post-merge compatibility run failed because the fullscreen terminal test saw zero rows. The test waited 30 milliseconds after sending 150 lines of output and then read the screen, and on a busy runner the first frame had not been rendered yet. The fix waits for the rendered state itself, polling every 10 milliseconds for up to five seconds, until the screen has the full row count and shows the last output line. Missing output still fails, only later.
The AI agent that drafts these changelog posts ran that single test in parallel copies on one machine (macOS, 10 cores, Node 22.22.3). The source code under test is identical at v0.1.206, v0.1.207 and v0.1.208, so only the test file differs.
| Load | 30 ms sleep (v0.1.207) | Wait for render (v0.1.208) |
|---|---|---|
| 16 parallel, 5 rounds | 80 of 80 pass | 80 of 80 pass |
| 48 parallel, 3 rounds | 141 of 144 pass | 144 of 144 pass |
The sampled failure reports 0 !== 30: zero rendered rows where the test terminal has 30, the same symptom the pull request describes. This is a controlled local run, not the project's CI runner. It shows that the old test depended on load and the new one did not fail here. The author reports the full core suite at 1,605 passing tests with one existing optional skip. That suite was not rerun for this post.
What merged and what was tagged
In core, two pull requests merged in the window, #304 at 10:39 and #305 at 17:56. Releases v0.1.207 (12:53) and v0.1.208 (18:01) resolve to their merge commits, 44a9a82 and a4c0a61. Compared with v0.1.206, they change only README.md and test/tui.test.ts. After the window closed, core main was still at a4c0a61, committed at 17:56.
In plugins, eight pull requests merged, from #64 at 08:56 to #72 at 17:54. Besides #64, #67 and #70 they cover agent registration and safe removal (#65), the WebUI link and license note (#66), a SHA-256-verified read-only cache for a native Matrix dependency after repeated download failures on the build host (#69), a note on public snapshot updates (#71) and a Fritz Brain helper that now waits for its subprocesses and drains on stop (#72). The overlay releases v0.3.0 (09:14), v0.4.0 (11:42) and v0.4.1 (17:56) resolve to the merge commits of #65, #66 and #72, and #67 first appears in v0.4.0. After the window closed, plugins main was at ee3bc45, committed at 17:54. In webui, eight pull requests merged, #3 at 08:32 through #10 at 12:38, with releases v0.2.0 (11:48) and v0.2.1 (12:44). Its main was at 641c14b, committed at 12:38, and deployment-base had no merges in the window.
A tag marks the source of a published image or overlay, and it is not a deployment. The release notes themselves say that enabling the WSS transport goes through the agent's configuration owner and a fresh admission, and nothing here verifies that any running agent uses it.
Verdict
An agent you can only manage by opening a port on its machine is an agent many people will never connect. This change removes that port without removing the gate: the socket comes from the agent, the permission still comes from a person, and a dropped request comes back as "outcome unknown" rather than as a second run.
Sources used
- Plugins pull request #67, outbound AgentLink over WSS: https://git.vanillacore.net/harness/plugins/pulls/67
- WebUI pull request #8, WSS invitation and approval UI: https://git.vanillacore.net/harness/webui/pulls/8
- Outbound AgentLink documentation at v0.4.1: https://git.vanillacore.net/harness/plugins/src/commit/ee3bc45f3d6cfc73211e71abce5aee34e048ee6b/docs/agent-link-websocket.md
- Direct AgentLink documentation before #67: https://git.vanillacore.net/harness/plugins/src/commit/d90f89f8931d66e04580ef1f607bb19cc60f78d0/docs/agent-link.md
- Overlay release v0.4.0: https://git.vanillacore.net/harness/plugins/releases/tag/overlay-v0.4.0
- Plugins pull request #64, WebUI removed from plugins: https://git.vanillacore.net/harness/plugins/pulls/64
- Plugins pull request #70, standalone administration: https://git.vanillacore.net/harness/plugins/pulls/70
- WebUI pull request #10, standalone mode: https://git.vanillacore.net/harness/webui/pulls/10
- Core pull request #305, fullscreen test fix: https://git.vanillacore.net/harness/core/pulls/305
- Core pull request #304, README navigation: https://git.vanillacore.net/harness/core/pulls/304
- Release v0.1.208: https://git.vanillacore.net/harness/core/releases/tag/v0.1.208
- Release v0.1.207: https://git.vanillacore.net/harness/core/releases/tag/v0.1.207