Skip to content
2 changes: 2 additions & 0 deletions src/http/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Surface: [remote-agents.mdx](../../website/src/content/docs/docs/using-devices/r
falls back to `index.html`. An unbuilt console answers 404 and logs one warning.
2. Log middleware — one `request` line per answer (method, path, status, `durationMs`,
`requesterId`). Not for `/v1/healthz`.
Inside it, a JSON `200` to a `GET` gets an `ETag`, and a matching `If-None-Match` turns it
into `304` with no body. Streams and mounts are skipped, so their bodies are never read.
3. `requireAuth` on every route but `GET /v1/healthz`. It asks `src/auth` with the request's
headers: a bearer token, or the JWT the sign-in proxy adds when `http.proxyAuth` is set.
Neither valid → 401 `UNAUTHENTICATED`. A `worker` join token → 403 on every route.
Expand Down
61 changes: 61 additions & 0 deletions src/http/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3229,3 +3229,64 @@ describe("the stream mount's listed route", () => {
await found.body?.cancel();
});
});

describe("ETag on a JSON read under /v1", () => {
it("answers an unchanged JSON read with 304 and no body, and anything else with 200", async () => {
const tokens: { id: string }[] = [{ id: "tok_1" }];
const { app, dispatcher, eventBus } = buildHarness({
mounts: [
{
prefix: "json-mount",
handle: () => Response.json({ ok: true }),
},
] as never,
});
dispatcher.handlers["token.list"] = () => ({ tokens: [...tokens] });
dispatcher.handlers["events.subscribe"] = (_input, session) => ({
subscribed: true,
subscriptionId: session.manageEventSubscription(true),
});
const read = (path: string, headers: Record<string, string> = {}) =>
app.request(path, { headers: { ...operatorAuth, ...headers } });

const first = await read("/v1/tokens");
const tag = first.headers.get("etag");
expect(first.status).toBe(200);
expect(tag).toMatch(/^"[^"]+"$/);

const unchanged = await read("/v1/tokens", { "If-None-Match": tag ?? "" });
expect(unchanged.status).toBe(304);
expect(await unchanged.text()).toBe("");
expect(unchanged.headers.get("etag")).toBe(tag);

const stale = await read("/v1/tokens", { "If-None-Match": '"stale"' });
expect(stale.status).toBe(200);
expect(stale.headers.get("etag")).toBe(tag);

tokens.push({ id: "tok_2" });
const changed = await read("/v1/tokens", { "If-None-Match": tag ?? "" });
expect(changed.status).toBe(200);
expect(changed.headers.get("etag")).not.toBe(tag);

const missing = await read("/v1/leases/lse_none");
expect(missing.status).toBe(404);
expect(missing.headers.get("etag")).toBeNull();

const mount = await read("/v1/json-mount/x");
expect(mount.status).toBe(200);
expect(mount.headers.get("content-type")).toContain("application/json");
expect(mount.headers.get("etag")).toBeNull();

const streamOrStall = await Promise.race([
read("/v1/events/stream"),
new Promise<"stalled">((resolve) => setTimeout(() => resolve("stalled"), 500)),
]);
if (streamOrStall === "stalled") expect.fail("the event stream was buffered, not answered");
const stream = streamOrStall;
expect(stream.headers.get("etag")).toBeNull();
const framesPromise = readSseFrames(stream, 1);
await Promise.resolve();
eventBus.emit("daemon.stopping", { reason: "live" }, "test");
expect((await framesPromise)[0]?.event).toBe("daemon.stopping");
});
});
15 changes: 15 additions & 0 deletions src/http/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import type { Context } from "hono";
import { etag } from "hono/etag";
import { z } from "zod";

import type { EventBus } from "../bus/index.js";
Expand Down Expand Up @@ -211,6 +212,20 @@ export function createHttpApp(deps: HttpGatewayDeps): OpenAPIHono<Env> & HttpApp
});
});

// why: Hono's `etag()` buffers the body to hash it, so it runs only under /v1 (a `*` step, so no `/v1/*` route is registered) on a JSON answer to a GET that is not a mount's; a stream or a mount's own body is never read.
const mountPrefixes = (deps.mounts ?? []).map((mount) => `/v1/${mount.prefix}`);
const etagStep = etag();
app.use("*", async (c, next) => {
await next();
const path = c.req.path;
if (!path.startsWith("/v1/")) return;
const mounted = mountPrefixes.some(
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
);
if (mounted || !c.res.headers.get("content-type")?.startsWith("application/json")) return;
await etagStep(c, () => Promise.resolve());
});

app.openapi(healthzRoute, (c) => c.json({ ok: true as const }, 200));

app.openapi({ ...statusRoute, middleware: [agentAuth] }, async (c) =>
Expand Down
2 changes: 2 additions & 0 deletions website/src/content/docs/docs/using-devices/remote-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export SIMLOCK_TOKEN=slk_d2FybS1wb29sLWFnZW50LXRva2VuLWV4YW1wbGUtMDM

An `Idempotency-Key` header on `POST /v1/lease-requests` (1 to 200 characters) makes the request safe to send again: the same key returns the stored request instead of queueing a second one, for 10 minutes after it finished.

A `GET` under `/v1` that answers `200` with JSON carries an `ETag` header. Send it back as `If-None-Match` on the next read of the same route: while the answer is unchanged, the reply is `304 Not Modified` with no body. Event streams carry no `ETag`.

## Through a gateway

Send the same calls to the gateway, with a token created on the gateway. The gateway picks a worker for you and forwards renew, release and exec to it.
Expand Down
Loading