From 9a443afcf062e7af47312aa8fa7b25b2ca648ce9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 10 Aug 2026 22:12:14 +0200 Subject: [PATCH 1/2] chore(self-host): pin the image to engine 8.0.0 to match hosted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image still pinned 7.0.0 while cast.agentrelay.com now runs 8.0.0. The runbook tells the counterparty not to treat their deployment as federation-ready until the image is pinned to the same engine version hosted runs, so shipping it at 7.0.0 would have failed that condition by our own instruction — and version skew between the two ends of a federation demo is the class of run-stopper the dry run exists to catch. The lockfile is regenerated rather than hand-edited. A sed of the version string left a stale integrity hash for @relaycast/types and the build failed EINTEGRITY (wanted sha512-fFfBNRo…, got sha512-GMBA8do…), which is the correct outcome: `npm ci` refused a lockfile that did not match what the registry served. Verified on the built image, not just the source: the engine inside reports 8.0.0, a valid https authority starts and stays up, and http://, .local and IP literal authorities are each refused at the door with exit 64. Container entrypoint tests 14/14. Co-Authored-By: Claude Opus 5 --- Dockerfile | 4 ++-- RUNBOOK.md | 16 ++++++++-------- docker-compose.yml | 4 ++-- docker/package-lock.json | 28 ++++++++++++++-------------- docker/package.json | 4 ++-- docs/self-hosting.md | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63eb0abf..8261a5f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.7 -ARG RELAYCAST_ENGINE_VERSION=7.0.0 +ARG RELAYCAST_ENGINE_VERSION=8.0.0 # Node 22.23.2, pinned to the multi-platform bookworm-slim index so the same # Dockerfile resolves native linux/amd64 and linux/arm64 images. @@ -16,7 +16,7 @@ RUN apt-get update \ WORKDIR /opt/relaycast COPY docker/package.json docker/package-lock.json ./ -# The lockfile pins @relaycast/engine to 7.0.0 and the source-build setting +# The lockfile pins @relaycast/engine to 8.0.0 and the source-build setting # exercises the C/C++ toolchain for better-sqlite3 on every target architecture. ENV npm_config_build_from_source=true RUN test "$(node -p "require('./package.json').dependencies['@relaycast/engine']")" = "$RELAYCAST_ENGINE_VERSION" \ diff --git a/RUNBOOK.md b/RUNBOOK.md index e1091b18..17d4901d 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -6,7 +6,7 @@ access and does not operate this deployment. ## Operating boundary -The image contains `@relaycast/engine` **7.0.0** on Node 22.23.2 and stores all +The image contains `@relaycast/engine` **8.0.0** on Node 22.23.2 and stores all state locally in SQLite plus a files directory. It requires an explicit HTTPS public origin and exits before starting the engine if `--base-url` is missing, plaintext, single-label, an IP literal, loopback, or in the special-use `.local` @@ -31,7 +31,7 @@ claim. files, and off-host backups. No database server, object store, message broker, or inbound firewall opening -is required. Compose binds the engine only to `127.0.0.1:8787`; the tunnel makes +is required. Compose binds the engine only to `128.0.0.1:8787`; the tunnel makes the public connection. ## Build and run @@ -56,10 +56,10 @@ Confirm the installed engine, container state, and local health endpoint: docker compose exec relaycast node -p \ "require('/opt/relaycast/node_modules/@relaycast/engine/package.json').version" docker compose ps -curl --fail --silent --show-error http://127.0.0.1:8787/health +curl --fail --silent --show-error http://128.0.0.1:8787/health ``` -The version command must print `7.0.0`, and Compose should eventually report +The version command must print `8.0.0`, and Compose should eventually report `healthy`. The health response must contain `"ok":true`; its `version` field is the gateway/application version, not reliable evidence of the installed engine package version. @@ -70,7 +70,7 @@ host loopback interface: ```bash umask 077 curl --fail --silent --show-error \ - --request POST http://127.0.0.1:8787/v1/workspaces \ + --request POST http://128.0.0.1:8787/v1/workspaces \ --header 'content-type: application/json' \ --data '{"name":"ratify-protocol"}' \ --output ratify-workspace-bootstrap.json @@ -81,7 +81,7 @@ Move it into Ratify's secret manager, then securely remove the bootstrap file. Do not repeat this command: workspace names are not unique, so a repeat creates another workspace and key. -Engine 7.0.0 has a known agent-card discovery defect: the bare standard path +Engine 8.0.0 has a known agent-card discovery defect: the bare standard path `GET /.well-known/agent-card.json` interprets the leftmost hostname label as the workspace name. At `relay.ratifyprotocol.com` it therefore looks for `relay`, not the meaningful workspace name `ratify-protocol`. Host inference also @@ -99,7 +99,7 @@ do not treat this deployment as federation-ready until the single-tenant sole-workspace resolver fix is released, this image is pinned to that exact engine version, and `cast.agentrelay.com` is confirmed on the same version. -In engine 7.0.0, `POST /v1/workspaces` is intentionally unauthenticated for +In engine 8.0.0, `POST /v1/workspaces` is intentionally unauthenticated for initial bootstrap. The tunnel rule below blocks that exact path before the service becomes public; omitting the rule would allow arbitrary public workspace creation and unbounded local state growth. @@ -135,7 +135,7 @@ ingress: path: ^/v1/workspaces/?$ service: http_status:403 - hostname: relay.ratifyprotocol.com - service: http://127.0.0.1:8787 + service: http://128.0.0.1:8787 - service: http_status:404 ``` diff --git a/docker-compose.yml b/docker-compose.yml index c4610531..4690f30f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: cap_drop: - ALL ports: - - 127.0.0.1:${RELAYCAST_PORT:-8787}:8787 + - 128.0.0.1:${RELAYCAST_PORT:-8787}:8787 volumes: - relaycast-data:/data tmpfs: @@ -25,7 +25,7 @@ services: - CMD - node - -e - - fetch('http://127.0.0.1:8787/health').then(async(response)=>{const body=await response.json();if(!response.ok||body.ok!==true)process.exit(1)}).catch(()=>process.exit(1)) + - fetch('http://128.0.0.1:8787/health').then(async(response)=>{const body=await response.json();if(!response.ok||body.ok!==true)process.exit(1)}).catch(()=>process.exit(1)) interval: 15s timeout: 5s retries: 5 diff --git a/docker/package-lock.json b/docker/package-lock.json index 4eb1dda0..67ee2db9 100644 --- a/docker/package-lock.json +++ b/docker/package-lock.json @@ -1,14 +1,14 @@ { "name": "relaycast-self-host-image", - "version": "7.0.0", + "version": "8.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "relaycast-self-host-image", - "version": "7.0.0", + "version": "8.0.0", "dependencies": { - "@relaycast/engine": "7.0.0" + "@relaycast/engine": "8.0.0" } }, "node_modules/@hono/node-server": { @@ -24,21 +24,21 @@ } }, "node_modules/@relaycast/a2a": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@relaycast/a2a/-/a2a-7.0.0.tgz", - "integrity": "sha512-Qq1PurJ46sn3GmJryOKXkoo6u8PlyXIpElbN0JTnxKobczRk2gNlZwQg4fAQ8RAWg9cMoYpomgRLSL18Qo147Q==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@relaycast/a2a/-/a2a-8.0.0.tgz", + "integrity": "sha512-yx+kwyRdmrMS/VhFt92BcIhRqc2FGSq1TVhaaM7+keE2ZbqkjEDRkfuIztvOp+P66PZGxLElBDJ1J/QilLsYCA==", "dependencies": { "zod": "^4.3.6" } }, "node_modules/@relaycast/engine": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@relaycast/engine/-/engine-7.0.0.tgz", - "integrity": "sha512-6MkKH5cw6GSCywvgrBpJ/JQ9xIeDmhYY4cK0YWhROrquvHeHh6+WCfXA+ac2BhZHps7Kjj2aLpsguKCK53R/sA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@relaycast/engine/-/engine-8.0.0.tgz", + "integrity": "sha512-7tIkIkD5t2F0nBQ9pTmW2wwPWEaiKJC6Cw1xBef2ai+6rkRtdxhCrcXlxDhxPpu6Ofn/OVEK0ChkefNxF6PFQA==", "dependencies": { "@hono/node-server": "^1.13.7", - "@relaycast/a2a": "7.0.0", - "@relaycast/types": "7.0.0", + "@relaycast/a2a": "8.0.0", + "@relaycast/types": "8.0.0", "better-sqlite3": "^11.10.0", "drizzle-orm": "^0.45.1", "hono": "^4.11.9", @@ -50,9 +50,9 @@ } }, "node_modules/@relaycast/types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-7.0.0.tgz", - "integrity": "sha512-fFfBNRoTiC0L2I6+7d8XCHNpYjo/9s1yQAtskU+vKA+HyP2Xfc7kFoHCT8UKDFWt0phswxZhIwKCVNqUEtTXQw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-8.0.0.tgz", + "integrity": "sha512-GMBA8doHg6nyhtNJP6s8iX1H/KMN+EcQhWeiiIUoVPNULejB/NrFNre4AyaXsA0JqY2C+2U5AG2K6azUZyvt3Q==", "dependencies": { "zod": "^4.3.6" } diff --git a/docker/package.json b/docker/package.json index ab6a973f..68ba1212 100644 --- a/docker/package.json +++ b/docker/package.json @@ -1,8 +1,8 @@ { "name": "relaycast-self-host-image", "private": true, - "version": "7.0.0", + "version": "8.0.0", "dependencies": { - "@relaycast/engine": "7.0.0" + "@relaycast/engine": "8.0.0" } } diff --git a/docs/self-hosting.md b/docs/self-hosting.md index ec3cbfd7..1920fdb1 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -168,7 +168,7 @@ relay.example.com { ```nginx location / { - proxy_pass http://127.0.0.1:8787; + proxy_pass http://128.0.0.1:8787; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; From 9a4fd68313728cd93c0c5d3a13c0fa0f89aba157 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 10 Aug 2026 22:20:17 +0200 Subject: [PATCH 2/2] fix the loopback address, correct the runbook, and record the change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings, all correct, and the first is mine. CRITICAL — 127.0.0.1 became 128.0.0.1. The version bump was done with `sed s/7.0.0/8.0.0/g`, and the loopback address contains "7.0.0" as a substring. That rewrote the Compose port binding, the in-container healthcheck, the tunnel and nginx examples, and four runbook commands to an address that is not loopback and is not assigned on a normal host, so `docker compose up` would have failed before the service started and the healthcheck would have probed a remote address. Caught by CodeRabbit and Codex independently. The lesson is the obvious one: a global sed on a version string is not a safe edit, and I should have read the diff instead of trusting it. The runbook said the opposite of the truth. The same sed turned "Engine 7.0.0 has a known agent-card discovery defect" into "Engine 8.0.0 has a known defect". 8.0.0 is the release that FIXES it, so the paragraph told operators not to enable the federation scenario this bump exists to unblock. Rewritten to describe what now works — the bare standard path on a single-tenant deployment, both explicit forms, and the two deliberate fail-closed behaviours (multi-tenant without a selector, and a selector naming a workspace that does not exist) — with the old behaviour kept only as a diagnostic hint. Changelog entry added per AGENTS.md, since this is the first user-visible change after the 8.0.0 release. Verified rather than assumed: the full changed-line diff is now version bumps only, the image builds, the engine inside reports 8.0.0, container entrypoint tests are 14/14, and `docker compose up` brings the stack up with /health answering through 127.0.0.1. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 6 +++++- RUNBOOK.md | 43 +++++++++++++++++++++++++++---------------- docker-compose.yml | 4 ++-- docs/self-hosting.md | 2 +- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f889d4..f3520253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,11 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Packages without a separate changelog are covered by the cross-package notes below. -## [Unreleased] +## [Unreleased - Patch] + +### Changed + +- The self-host container now installs `@relaycast/engine` 8.0.0, matching the hosted deployment, and its runbook documents agent-card discovery as working on the standard well-known path rather than as a known defect. ## [8.0.0] - 2026-08-10 diff --git a/RUNBOOK.md b/RUNBOOK.md index 17d4901d..529e9bbe 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -31,7 +31,7 @@ claim. files, and off-host backups. No database server, object store, message broker, or inbound firewall opening -is required. Compose binds the engine only to `128.0.0.1:8787`; the tunnel makes +is required. Compose binds the engine only to `127.0.0.1:8787`; the tunnel makes the public connection. ## Build and run @@ -56,7 +56,7 @@ Confirm the installed engine, container state, and local health endpoint: docker compose exec relaycast node -p \ "require('/opt/relaycast/node_modules/@relaycast/engine/package.json').version" docker compose ps -curl --fail --silent --show-error http://128.0.0.1:8787/health +curl --fail --silent --show-error http://127.0.0.1:8787/health ``` The version command must print `8.0.0`, and Compose should eventually report @@ -70,7 +70,7 @@ host loopback interface: ```bash umask 077 curl --fail --silent --show-error \ - --request POST http://128.0.0.1:8787/v1/workspaces \ + --request POST http://127.0.0.1:8787/v1/workspaces \ --header 'content-type: application/json' \ --data '{"name":"ratify-protocol"}' \ --output ratify-workspace-bootstrap.json @@ -81,23 +81,34 @@ Move it into Ratify's secret manager, then securely remove the bootstrap file. Do not repeat this command: workspace names are not unique, so a repeat creates another workspace and key. -Engine 8.0.0 has a known agent-card discovery defect: the bare standard path -`GET /.well-known/agent-card.json` interprets the leftmost hostname label as the -workspace name. At `relay.ratifyprotocol.com` it therefore looks for `relay`, -not the meaningful workspace name `ratify-protocol`. Host inference also -shadows the documented `/:workspace/.well-known/agent-card.json` route on this -three-label hostname. The only working unauthenticated interim form is: +Agent-card discovery works on the standard path from engine 8.0.0. A +single-tenant deployment — one workspace, which is what this runbook sets up — +answers the bare well-known URL directly, so a counterparty needs no +Relaycast-specific query parameter: ```text +https://relay.ratifyprotocol.com/.well-known/agent-card.json +``` + +The explicit forms also resolve, and an explicit selector now takes precedence +over host-label inference: + +```text +https://relay.ratifyprotocol.com/ratify-protocol/.well-known/agent-card.json https://relay.ratifyprotocol.com/.well-known/agent-card.json?workspace=ratify-protocol ``` -A mismatch returns `workspace_not_found` even while authenticated -`POST /a2a/rpc` works. The query form is documented only as an interim operator -check. A standards-following counterparty will try the bare well-known path, so -do not treat this deployment as federation-ready until the single-tenant -sole-workspace resolver fix is released, this image is pinned to that exact -engine version, and `cast.agentrelay.com` is confirmed on the same version. +Two behaviours worth knowing, because both are deliberate. A deployment holding +more than one workspace does **not** fall back to guessing: it returns +`workspace_not_found` unless a selector identifies one. And a selector that +names a workspace which does not exist also returns `workspace_not_found` +rather than resolving to some other workspace — a typo fails loudly instead of +crossing a tenant boundary. + +Earlier engines interpreted the leftmost hostname label as the workspace name, +so `relay.ratifyprotocol.com` looked for a workspace called `relay` and the bare +path returned `workspace_not_found`. If you see that on the bare path, check the +image is on 8.0.0 or later before looking anywhere else. In engine 8.0.0, `POST /v1/workspaces` is intentionally unauthenticated for initial bootstrap. The tunnel rule below blocks that exact path before the @@ -135,7 +146,7 @@ ingress: path: ^/v1/workspaces/?$ service: http_status:403 - hostname: relay.ratifyprotocol.com - service: http://128.0.0.1:8787 + service: http://127.0.0.1:8787 - service: http_status:404 ``` diff --git a/docker-compose.yml b/docker-compose.yml index 4690f30f..c4610531 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: cap_drop: - ALL ports: - - 128.0.0.1:${RELAYCAST_PORT:-8787}:8787 + - 127.0.0.1:${RELAYCAST_PORT:-8787}:8787 volumes: - relaycast-data:/data tmpfs: @@ -25,7 +25,7 @@ services: - CMD - node - -e - - fetch('http://128.0.0.1:8787/health').then(async(response)=>{const body=await response.json();if(!response.ok||body.ok!==true)process.exit(1)}).catch(()=>process.exit(1)) + - fetch('http://127.0.0.1:8787/health').then(async(response)=>{const body=await response.json();if(!response.ok||body.ok!==true)process.exit(1)}).catch(()=>process.exit(1)) interval: 15s timeout: 5s retries: 5 diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 1920fdb1..ec3cbfd7 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -168,7 +168,7 @@ relay.example.com { ```nginx location / { - proxy_pass http://128.0.0.1:8787; + proxy_pass http://127.0.0.1:8787; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";