Skip to content

Commit e465077

Browse files
committed
computerd: Move the watermarks route under /api
The route read sync revisions through rpc.sync.watermarks() but sat at /__computerd/watermarks, alongside runtime info and process memory. Those report on the daemon; this reports the workspace, and it returns exactly what a session would. It is now /api/watermarks, which draws the line where it belongs: /api is the workspace surface, including anything that reads through it, and /__computerd is daemon introspection. Only the exact /api path upgrades, so a handshake aimed at the subpath still answers 404, and the shared secret covers the new path the same way it covered the old one.
1 parent 3074d90 commit e465077

5 files changed

Lines changed: 51 additions & 32 deletions

File tree

docs/07_injected_service.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ backend pins it to `8080`) and serves:
5151
| --- | --- | --- |
5252
| `/health` | `GET`, `HEAD` | Liveness probe; `200 ok\n` as soon as the HTTP server binds. |
5353
| `/__computerd/info` | `GET` | Runtime info: FUSE backend, mount point, port. |
54-
| `/__computerd/watermarks` | `GET` | Sync revisions: `currentRev`, `pushRev`, `fetchCursor`. |
55-
| `/api` | `GET` (upgrade) | WebSocket capnweb transport — the bootstrap stub is `WorkspaceRPC`. A request without an `Upgrade` header gets `400`; an unsupported `Sec-WebSocket-Version` gets `426` and the versions the server speaks. |
54+
| `/api` | `GET` (upgrade) | WebSocket capnweb transport — the bootstrap stub is `WorkspaceRPC`. Only the exact path upgrades. A request without an `Upgrade` header gets `400`; an unsupported `Sec-WebSocket-Version` gets `426` and the versions the server speaks. |
55+
| `/api/watermarks` | `GET`, `HEAD` | Sync revisions: `currentRev`, `pushRev`, `fetchCursor`. The same values `sync.watermarks()` returns, for callers that want a few numbers without holding a session. |
5656
| `/connect` | `POST` | Tells `computerd` to dial *out* to a caller-supplied endpoint and serve a `WorkspaceRPC` session over that outbound WebSocket. Used by the Cloudflare backend (see below). |
5757
| `/` | `GET` | Banner/info page. |
5858

59+
`/api` is the workspace surface: the session itself, plus anything that
60+
reads through it. `/__computerd` is daemon introspection, which is why
61+
runtime info sits there and revisions do not.
62+
5963
The capnweb bootstrap interface is **`WorkspaceRPC`** (defined in
6064
`packages/rpc/`), split into `sync` and `shell` sub-stubs.
6165

packages/computerd/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Current endpoints:
2828
- `GET /__computerd/info` returns JSON with the selected FUSE backend, mount point, and bound port.
2929
- `GET /__computerd/stats` returns JSON with DOFS table row counts, total inline and blob byte sizes, the orphan-blob subset, and process resident memory. Useful for watching how the store grows under load.
3030
- `GET /` returns `200 OK` with an empty JSON object: `{}`.
31-
- `GET /__computerd/watermarks` returns JSON with `currentRev`, `pushRev`, and `fetchCursor`, read through the same `watermarks()` the wire serves. For samplers that want a few numbers without opening a session.
3231
- `GET /api` upgrades to a WebSocket carrying the capnweb RPC surface backed by `@cloudflare/computer-rpc`. This is the container's only RPC carrier. A request without an `Upgrade` header returns `400`; a handshake naming an unsupported `Sec-WebSocket-Version` returns `426` along with the versions the server speaks.
32+
- `GET /api/watermarks` returns JSON with `currentRev`, `pushRev`, and `fetchCursor`, read through the same `watermarks()` the wire serves. For samplers that want a few numbers without opening a session. It sits under `/api` because it reads the workspace surface; `/__computerd` is for daemon introspection.
3333

3434
All other paths and methods return `404`/`405` with a `text/plain` body.
3535

packages/computerd/src/cli/computerd.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ test("/api refuses anything that is not a websocket handshake", async (_ctx) =>
173173
]);
174174
expect(noKey).toMatch(/^HTTP\/1\.1 400 /);
175175

176+
// A subpath under /api is not the session endpoint: only the exact
177+
// path upgrades.
178+
const subpath = await rawRequest(port, [
179+
"GET /api/watermarks HTTP/1.1",
180+
`Host: 127.0.0.1:${port}`,
181+
"Upgrade: websocket",
182+
"Connection: Upgrade",
183+
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
184+
"Sec-WebSocket-Version: 13",
185+
]);
186+
expect(subpath).toMatch(/^HTTP\/1\.1 404 /);
187+
176188
// An unknown path is still a 404, upgrade header or not.
177189
const unknown = await rawRequest(port, [
178190
"GET /nope HTTP/1.1",
@@ -185,14 +197,14 @@ test("/api refuses anything that is not a websocket handshake", async (_ctx) =>
185197
expect(unknown).toMatch(/^HTTP\/1\.1 404 /);
186198
});
187199

188-
test("/__computerd/watermarks reports sync revisions over plain HTTP", async (_ctx) => {
200+
test("/api/watermarks reports sync revisions over plain HTTP", async (_ctx) => {
189201
// Samplers want three numbers on an interval. Opening an RPC session
190202
// per sample is the wrong shape for that.
191203
const port = await getAvailablePort();
192204
const mountPoint = await fs.mkdtemp(path.join(os.tmpdir(), "computerd-watermarks-"));
193205
await startComputerd({ port, mountPoint, env: { FUSE_MOUNT: "none" } });
194206

195-
const res = await fetch(`http://127.0.0.1:${port}/__computerd/watermarks`);
207+
const res = await fetch(`http://127.0.0.1:${port}/api/watermarks`);
196208
expect(res.status).toBe(200);
197209
const body = await res.json();
198210
expect(body).toMatchObject({

packages/computerd/src/cli/computerd.ts

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,35 @@ function createHTTPServer(
184184
return;
185185
}
186186

187+
// /api/watermarks — the same sync revisions the session serves,
188+
// over plain HTTP, for samplers that want a few numbers on an
189+
// interval rather than a session of their own. It reads through
190+
// rpc.sync.watermarks(), so the two cannot drift. Part of the
191+
// workspace API rather than daemon introspection, hence /api
192+
// rather than /__computerd.
193+
if (path === "/api/watermarks") {
194+
if (request.method === "HEAD") {
195+
send(response, 200, "", { "content-type": "application/json; charset=utf-8" });
196+
return;
197+
}
198+
void rpc.sync
199+
.watermarks()
200+
.then((watermarks) => {
201+
send(response, 200, JSON.stringify(watermarks), {
202+
"content-type": "application/json; charset=utf-8",
203+
});
204+
})
205+
.catch((error: unknown) => {
206+
console.error("/api/watermarks failed:", error);
207+
if (!response.headersSent) {
208+
send(response, 500, "internal error\n", {
209+
"content-type": "text/plain; charset=utf-8",
210+
});
211+
}
212+
});
213+
return;
214+
}
215+
187216
// /connect — POST { base, health, api } naming an endpoint the
188217
// host wants us to dial back into. We poll `base + health` until
189218
// it answers, then open a capnweb WebSocket session against
@@ -240,32 +269,6 @@ function createHTTPServer(
240269
return;
241270
}
242271

243-
// Sync revisions over plain HTTP, for samplers that want a few
244-
// numbers on an interval rather than an RPC session. Reads through
245-
// the same watermarks() the wire serves, so the two cannot drift.
246-
if (path === "/__computerd/watermarks") {
247-
if (request.method === "HEAD") {
248-
send(response, 200, "", { "content-type": "application/json; charset=utf-8" });
249-
return;
250-
}
251-
void rpc.sync
252-
.watermarks()
253-
.then((watermarks) => {
254-
send(response, 200, JSON.stringify(watermarks), {
255-
"content-type": "application/json; charset=utf-8",
256-
});
257-
})
258-
.catch((error: unknown) => {
259-
console.error("/__computerd/watermarks failed:", error);
260-
if (!response.headersSent) {
261-
send(response, 500, "internal error\n", {
262-
"content-type": "text/plain; charset=utf-8",
263-
});
264-
}
265-
});
266-
return;
267-
}
268-
269272
if (path === "/__computerd/info") {
270273
const body = request.method === "HEAD" ? "" : JSON.stringify(info);
271274
send(response, 200, body, {

script/computerd-soak.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async function dockerStats(cids) {
183183
// The daemon's revision numbers over plain HTTP. A sampler wanting
184184
// three integers on an interval does not need an RPC session.
185185
async function fetchWatermarks(url) {
186-
const res = await fetch(`${url}/__computerd/watermarks`);
186+
const res = await fetch(`${url}/api/watermarks`);
187187
if (!res.ok) throw new Error(`watermarks HTTP ${res.status}`);
188188
return await res.json();
189189
}

0 commit comments

Comments
 (0)