From f8d692d195577d30a81f6510b93ab8f3262e5ef8 Mon Sep 17 00:00:00 2001 From: outpoints <12377381+outpoints@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:44:40 -0700 Subject: [PATCH 1/3] fix(site): render the Honcho logo crisply in Safari Safari rasterizes SVG filters at low resolution in small elements, which smeared the header logo on iPhone. Draw the cyan and pink shadow layers as offset copies of the face instead of a drop-shadow filter. The 650px render matches the original apart from hard shadow edges. --- site/public/images/honcho-logo.svg | 39 +++++++++++------------------- site/public/seo/favicon.svg | 39 +++++++++++------------------- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/site/public/images/honcho-logo.svg b/site/public/images/honcho-logo.svg index 6bcb833..406e3d5 100644 --- a/site/public/images/honcho-logo.svg +++ b/site/public/images/honcho-logo.svg @@ -1,28 +1,17 @@ - - - - - - - - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/site/public/seo/favicon.svg b/site/public/seo/favicon.svg index 6bcb833..406e3d5 100644 --- a/site/public/seo/favicon.svg +++ b/site/public/seo/favicon.svg @@ -1,28 +1,17 @@ - - - - - - - - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + From 07700a5f04b76428a6a54b91a88b34f5bd5f314f Mon Sep 17 00:00:00 2001 From: outpoints <12377381+outpoints@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:44:40 -0700 Subject: [PATCH 2/3] fix: read the default Honcho URL at runtime next build inlines NEXT_PUBLIC_HONCHO_BASE_URL into server code too, so the published image ignored HONCHO_BASE_URL: the proxy fell back to http://localhost:8000 and new browsers were seeded with it. - Resolve the default target from runtime env (HONCHO_PROXY_BASE_URL, then the runtime NEXT_PUBLIC_HONCHO_BASE_URL, then the build-time value) for the proxy fallback. - Render the root layout per request and pass the default to the browser in a meta tag, so new browsers and the NEW_INSTANCE form start on the configured Honcho. - Move an untouched default/local seed that still holds the build-time URL to the runtime default. Edited instances are never changed. - Keep every previously allowed proxy target allowed. - Update the compose template, env examples, Dockerfile, README and AGENTS.md. --- .env.example | 5 +- AGENTS.md | 6 ++ README.md | 12 ++- docker-compose-example.yml | 11 +- site/Dockerfile | 9 +- site/scripts/default-target.test.mjs | 113 ++++++++++++++++++++ site/src/app/layout.tsx | 10 +- site/src/components/pages/ConfigPage.tsx | 4 +- site/src/lib/honcho/allowlist.ts | 57 ++++++---- site/src/lib/honcho/config.ts | 47 ++++++++- site/src/lib/honcho/defaultTarget.ts | 127 +++++++++++++++++++++++ 11 files changed, 353 insertions(+), 48 deletions(-) create mode 100644 site/scripts/default-target.test.mjs create mode 100644 site/src/lib/honcho/defaultTarget.ts diff --git a/.env.example b/.env.example index 826c74b..1dd5cb2 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,9 @@ # REQUIRED # ===================================================================== -# Upstream Honcho instance the dashboard talks to. Inlined into the -# browser bundle at build time AND read by the server-side proxy at runtime. +# Upstream Honcho instance the dashboard talks to, read at runtime. The proxy +# uses it as its default and new browsers start on it. Use an address the +# dashboard container can reach; other devices only need to reach the dashboard. # Example: # HONCHO_BASE_URL=http://honcho.local:8000 HONCHO_BASE_URL=http://localhost:8000 diff --git a/AGENTS.md b/AGENTS.md index aed7d8c..5147dfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,12 @@ Auth is `Authorization: Bearer ` header — optional in local dev (`AUTH_ API client lives in `src/lib/honcho/`. **Never hardcode the base URL or token in components** — read from the config store. Config is stored client-side in `localStorage` under `honcho-dashboard:instances` + `honcho-dashboard:activeId` (multi-instance). +`next build` inlines `process.env.NEXT_PUBLIC_*` into server code too, so the published +image cannot get runtime config that way. The default target comes from +`src/lib/honcho/defaultTarget.ts`: the server reads it at request time and `app/layout.tsx` +passes it to the browser in a meta tag. Keep that module's compatibility rules: the implicit +allowlist only grows, and only the untouched `default`/`local` seed is ever rewritten. + Use `@honcho-ai/sdk` 2.5.1+ for scopes, scope-aware search/context, peer scope recall, and workspace chat. The raw scope-list call is reserved for the side-effect-free compatibility probe; do not reintroduce raw 3.1 feature paths. diff --git a/README.md b/README.md index 6a1755a..da23dcb 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,13 @@ The template pulls the prebuilt image from GHCR — update later with `docker compose pull && docker compose up -d`. To build from source instead, uncomment the `build:` block in the Compose file and run `docker compose up --build`. -On first run, open the dashboard and point it at your Honcho server under **CONFIG** — -the prebuilt image ships with a `http://localhost:8000` default baked in (the active -instance is chosen in-app and stored per browser). Set `HONCHO_PROXY_ALLOWED_BASES` to -include that URL so the same-origin proxy will forward to it. Building from source with -`HONCHO_BASE_URL` set instead bakes your instance in as the default. +`HONCHO_BASE_URL` is read when the container starts, so the prebuilt image needs no +rebuild: it becomes the proxy's default upstream and the instance any new browser starts +on. Because the proxy makes the request, use the address the dashboard host can reach; +phones and other devices only need to reach the dashboard. Browsers still on the untouched +`local` → `http://localhost:8000` instance from an older image move to it automatically. +Instances you created or edited under **CONFIG** are never changed. To use other Honcho +servers, add them under **CONFIG** and list them in `HONCHO_PROXY_ALLOWED_BASES`. A `docker-compose-example.yml` template is included at the repo root. Copy it to `docker-compose.yml` (which is gitignored, so your local edits stay local). To wire the diff --git a/docker-compose-example.yml b/docker-compose-example.yml index b943125..e681e98 100644 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -22,10 +22,9 @@ services: # ---- Prefer to build from source instead of pulling? Comment out the # `image:` line above and uncomment this block, then `docker compose up --build`. - # NEXT_PUBLIC_* values are inlined into the browser bundle at build time and - # cannot be changed at runtime, so they live here as build args. (The published - # image bakes a http://localhost:8000 fallback; either way the active instance - # is chosen in-app, so most users can simply pull.) ---- + # The published image reads HONCHO_BASE_URL at runtime (see `environment:`), + # so most users can simply pull. Build args only change the fallback used + # when that runtime value is unset. ---- # build: # context: ./site # dockerfile: Dockerfile @@ -39,8 +38,8 @@ services: environment: # ---- Same-origin proxy (server-side, runtime) ---- - # Default upstream when the browser doesn't pin a specific instance via - # X-Honcho-Base-Url. Must match (or be a subset of) the allowlist below. + # Default upstream, read at runtime: the proxy uses it when a request pins + # no instance, and new browsers start on it. It is implicitly allowlisted. NEXT_PUBLIC_HONCHO_BASE_URL: ${HONCHO_BASE_URL:-http://localhost:8000} # Comma-separated allowlist of upstream Honcho origins the proxy will diff --git a/site/Dockerfile b/site/Dockerfile index 27a0522..09f59f8 100644 --- a/site/Dockerfile +++ b/site/Dockerfile @@ -12,11 +12,10 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -# NEXT_PUBLIC_* vars are inlined into the browser bundle at build time, not -# runtime. Pass --build-arg NEXT_PUBLIC_HONCHO_BASE_URL=... to docker build -# (or use docker-compose `build.args`) so the dashboard ships pointed at your -# Honcho instance. HONCHO_PROXY_BASE_URL / HONCHO_PROXY_ALLOWED_BASES stay -# runtime-only and can be overridden via `-e` flags. +# NEXT_PUBLIC_* vars are inlined at build time. The server also reads +# NEXT_PUBLIC_HONCHO_BASE_URL (and HONCHO_PROXY_BASE_URL / +# HONCHO_PROXY_ALLOWED_BASES) at runtime via `-e`, so the build arg below is +# only the fallback when no runtime value is set. ARG NEXT_PUBLIC_HONCHO_BASE_URL=http://localhost:8000 ARG NEXT_PUBLIC_HONCHO_TOKEN= ENV NEXT_PUBLIC_HONCHO_BASE_URL=$NEXT_PUBLIC_HONCHO_BASE_URL diff --git a/site/scripts/default-target.test.mjs b/site/scripts/default-target.test.mjs new file mode 100644 index 0000000..61627b9 --- /dev/null +++ b/site/scripts/default-target.test.mjs @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + LEGACY_DEFAULT_BASE_URL, + advertisedBaseUrl, + canonicalize, + defaultBaseUrl, + implicitAllowedBases, + readRuntimeEnv, + upgradeLegacySeed, +} from "../src/lib/honcho/defaultTarget.ts"; + +const RUNTIME = "http://honcho.synthetic.invalid:18100"; +const PROXY = "http://proxy.synthetic.invalid:8000"; +const BUILD = "http://build.synthetic.invalid:8000"; + +const seed = (baseUrl, extra = {}) => ({ id: "default", name: "local", baseUrl, ...extra }); +const legacy = { baseUrl: LEGACY_DEFAULT_BASE_URL }; + +test("published image: the runtime NEXT_PUBLIC value beats the localhost build default", () => { + const sources = { runtimePublicBaseUrl: RUNTIME, buildPublicBaseUrl: LEGACY_DEFAULT_BASE_URL }; + assert.equal(defaultBaseUrl(sources), RUNTIME); + assert.equal(advertisedBaseUrl(sources), RUNTIME); +}); + +test("HONCHO_PROXY_BASE_URL keeps precedence over both NEXT_PUBLIC copies", () => { + const sources = { proxyBaseUrl: PROXY, runtimePublicBaseUrl: RUNTIME, buildPublicBaseUrl: BUILD }; + assert.equal(defaultBaseUrl(sources), PROXY); +}); + +test("builds without a runtime value fall back to the build-time copy, as before", () => { + assert.equal(defaultBaseUrl({ buildPublicBaseUrl: BUILD }), BUILD); + assert.equal(defaultBaseUrl({ runtimePublicBaseUrl: " ", buildPublicBaseUrl: BUILD }), BUILD); + assert.equal(defaultBaseUrl({}), undefined); + assert.equal(advertisedBaseUrl({}), null); +}); + +test("an empty HONCHO_PROXY_BASE_URL keeps its old meaning", () => { + assert.equal(defaultBaseUrl({ proxyBaseUrl: "", buildPublicBaseUrl: BUILD }), ""); + assert.deepEqual(implicitAllowedBases({ proxyBaseUrl: "", buildPublicBaseUrl: BUILD }), []); +}); + +test("the implicit allowlist is a superset of every earlier release's", () => { + const values = [undefined, "", PROXY, RUNTIME, BUILD, LEGACY_DEFAULT_BASE_URL]; + for (const proxyBaseUrl of values) { + for (const runtimePublicBaseUrl of values) { + for (const buildPublicBaseUrl of values) { + const sources = { proxyBaseUrl, runtimePublicBaseUrl, buildPublicBaseUrl }; + const before = proxyBaseUrl ?? buildPublicBaseUrl; + const after = implicitAllowedBases(sources); + if (before) assert.ok(after.includes(before), JSON.stringify(sources)); + assert.equal(new Set(after).size, after.length, "no duplicates"); + } + } + } +}); + +test("the runtime NEXT_PUBLIC value is allowlisted without repeating it", () => { + assert.deepEqual( + implicitAllowedBases({ runtimePublicBaseUrl: RUNTIME, buildPublicBaseUrl: LEGACY_DEFAULT_BASE_URL }), + [LEGACY_DEFAULT_BASE_URL, RUNTIME], + ); +}); + +test("runtime env reads use a computed key", () => { + assert.equal(readRuntimeEnv({ NEXT_PUBLIC_HONCHO_BASE_URL: RUNTIME }, "NEXT_PUBLIC_HONCHO_BASE_URL"), RUNTIME); +}); + +test("browsers are only ever told a bare http(s) origin", () => { + assert.equal(advertisedBaseUrl({ proxyBaseUrl: `${PROXY}/api/` }), PROXY); + assert.equal(advertisedBaseUrl({ proxyBaseUrl: "http://user:pw@honcho.synthetic.invalid" }), null); + assert.equal(advertisedBaseUrl({ proxyBaseUrl: "ftp://honcho.synthetic.invalid" }), null); + assert.equal(advertisedBaseUrl({ proxyBaseUrl: "not a url" }), null); + assert.equal(canonicalize("https://honcho.synthetic.invalid#x"), null); +}); + +test("an untouched legacy seed moves to the advertised default", () => { + const other = { id: "inst_a", name: "prod", baseUrl: "https://prod.synthetic.invalid" }; + const next = upgradeLegacySeed([seed(LEGACY_DEFAULT_BASE_URL), other], legacy, RUNTIME); + assert.deepEqual(next, [seed(RUNTIME), other]); +}); + +test("user-owned instances are never rewritten", () => { + const cases = [ + [seed("http://localhost:8001")], + [seed(LEGACY_DEFAULT_BASE_URL, { token: "user-token" })], + [{ ...seed(LEGACY_DEFAULT_BASE_URL), name: "laptop" }], + [{ ...seed(LEGACY_DEFAULT_BASE_URL), id: "inst_b" }], + [], + ]; + for (const instances of cases) { + assert.equal(upgradeLegacySeed(instances, legacy, RUNTIME), null, JSON.stringify(instances)); + } +}); + +test("nothing changes without a different advertised default", () => { + const instances = [seed(LEGACY_DEFAULT_BASE_URL)]; + assert.equal(upgradeLegacySeed(instances, legacy, null), null); + assert.equal(upgradeLegacySeed(instances, legacy, LEGACY_DEFAULT_BASE_URL), null); +}); + +test("a seed carrying the build-time token still counts as untouched", () => { + const withToken = { baseUrl: LEGACY_DEFAULT_BASE_URL, token: "build-token" }; + assert.deepEqual( + upgradeLegacySeed([seed(LEGACY_DEFAULT_BASE_URL, { token: "build-token" })], withToken, RUNTIME), + [seed(RUNTIME, { token: "build-token" })], + ); + assert.equal(upgradeLegacySeed([seed(LEGACY_DEFAULT_BASE_URL)], withToken, RUNTIME), null); +}); + +test("source-built images whose seed already matches are left alone", () => { + assert.equal(upgradeLegacySeed([seed(BUILD)], { baseUrl: BUILD }, BUILD), null); +}); diff --git a/site/src/app/layout.tsx b/site/src/app/layout.tsx index b3bdc51..956bfe1 100644 --- a/site/src/app/layout.tsx +++ b/site/src/app/layout.tsx @@ -1,7 +1,10 @@ import type { Metadata } from "next"; import { JetBrains_Mono, VT323 } from "next/font/google"; +import { connection } from "next/server"; import "./globals.css"; import { ThemeProvider, THEME_INIT_SCRIPT } from "@/lib/theme"; +import { serverAdvertisedBaseUrl } from "@/lib/honcho/allowlist"; +import { DEFAULT_BASE_URL_META } from "@/lib/honcho/defaultTarget"; const jetbrainsMono = JetBrains_Mono({ variable: "--font-jetbrains-mono", @@ -24,9 +27,13 @@ export const metadata: Metadata = { icons: { icon: { url: "/seo/favicon.svg", type: "image/svg+xml", sizes: "any" } }, }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + // Render per request so the default Honcho URL comes from the running + // server's env, not the value frozen into the bundle by `next build`. + await connection(); + const defaultBaseUrl = serverAdvertisedBaseUrl(); return (