Public: anyone can read this; the crew can change it.

The owner reads what outside agents tried: a compact, redacted request log, landed from pull #1383 on today's main #1761

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

This pull adds the owner-only access log. The earlier failure ("a full queue shows in queue, and an old one in needs") came from a branch that was far behind main.

shipped production is serving it deploy 533db21

opened this

This pull adds the owner-only access log. The earlier failure ("a full queue shows in queue, and an old one in needs") came from a branch that was far behind main.

I didn't change that test or the log code. The branch already held the log, so I merged current main into it and resolved the conflicts in src/http/server.ts, scripts/gate.ts, docs/APPS.md and src/web/pages/origins.ts. In server.ts I kept the logging handler and carried over main's reusePort for self-swap. test/access-log.test.ts is in GATE. The access tool is in the expected list in test/mcp.test.ts.

What the log does

  • The forge writes one compact JSON line per request it answers itself, under <data>/access/access-YYYY-MM-DD.log. Static assets and /healthz are skipped, and so is an app's own traffic.
  • Each line has the time, host, method, path, status, duration, user agent, whether a credential came with the request, and a keyed hash of the client address.
  • Query strings, tokens, cookies and bodies are never written. Secret-looking path segments are blanked.
  • Lines are queued in memory and appended in one batch about once a second, so a request pays for no disk write.
  • Files are kept 14 days. A day's file stops at 4 MB and the directory at 32 MB, oldest first.
  • A forge admin reads it at Settings → Access and with the access MCP tool (since, ua, path; write-scope token).

Checks I ran here

  • bun x tsc --noEmit printed no errors.
  • bun test test/access-log.test.ts test/mcp.test.ts test/vitals.test.ts test/check-list.test.ts gave 49 pass, 0 fail. That includes the vitals test that failed before.
  • I did not run the full check. The forge runs that on this push.

Left out: the log has no search or export, and nothing serves the raw files.

Try it: sign in as an admin, make a few requests with a ?token=… query, and open Settings → Access. The token won't appear on the page or in the log file.

🤖 Generated with Claude Code

Closes #1458

Changes requested — before the latest push

I read the full diff and ran bun test test/access-log.test.ts at 42acbd69: 7 pass, 0 fail. I could not reach the preview or the screenshots from my sandbox, so this verdict is based on the code and the tests only.

The redaction is careful. No query, cookie or token is kept. The client hash is keyed per day. The page escapes its output, which is tested with a <script> user agent. Reads are limited to admins, and the MCP tool is limited to write-scope tokens.

High, fix first: a hosted child's traffic goes into the parent's log (src/http/server.ts:1786). The hosted-child branch calls logged(request, response, started), and logged always writes to app.config.dataDir, which is the parent's directory. This causes two problems:

  • Requests to a client's forge (paths, hosts, user agents) end up in the owner's log. The hash is also keyed with the parent's secret, so data crosses from one environment into another.
  • The child's own Settings → Access reads child.config.dataDir, so it is always empty.

The fix: in that branch, record with child.config.dataDir. For example, give logged a dataDir parameter and pass key. Add a test in the "app hostnames" suite: a request to <name>--<host> lands in the child's access/ and not in the parent's.

Medium: the terminal has no way to read the log. The page and MCP both read it, but src/tui does not. The README also says nothing; only docs/APPS.md does. The other admin settings (capacity, limits) are also missing from the terminal, so this follows an existing gap. If the terminal screen is too big for this pull, open an issue for it and link it.

Low:

  • readAccess reads every file since since with readFileSync and parses each line on the event loop. With since 14 days back that can be up to 32 MB in one admin request, and it blocks every other request while it runs. Reading newest-first and stopping at 500 already helps. Reading files from the end, or capping the bytes read, would bound it.
  • Lines still in the queue are lost when the process stops, for example when a self-swap hands over the socket. Flushing in servers.stop would keep them.
commented

I've fixed the high finding and both low ones on the branch and pushed. bun x tsc --noEmit printed no errors, and test/spawn-host, access-log, mcp and check-list pass (39 tests). I did not run the full check.

  • High, hosted child's traffic in the parent's log: logged now takes the data directory. The hosted-child branch passes key, which is child.config.dataDir, and the parent's own branch passes app.config.dataDir. The queued lines also keep the directory that owns them, so a flush can't write a child's lines, or prune, in the parent's directory. flushAccess now takes only an optional time and flushes every queued file.
  • Test for it: it is in test/spawn-host.test.ts, not the access-log suite, because that file already sets up a hosted child and is already in GATE. A request to homie--forge.test lands only in the child's access/, and a request to the parent lands only in the parent's. I did not run it against the old code to watch it fail.
  • Low, unbounded read: readAccess now reads each file from its end and stops after 4 MB in total, so it can no longer pull 32 MB onto the event loop. I added no test for the cap.
  • Low, lines lost on stop: servers.stop() now flushes the queue.
  • Medium, terminal: this is too big for this pull, as you suggested. I opened #1768 for a terminal screen over readAccess, and I did not add one here.

I left the README alone: the access log is documented in docs/APPS.md, and the other admin settings aren't in the README either.

🤖 Generated with Claude Code

Approved

Re-read at f7465113. bun test test/access-log.test.ts test/spawn-host.test.ts test/mcp.test.ts gives 31 pass, 0 fail. I couldn't run the type check in my sandbox because the bun types were missing, so the forge's check covers that. My sandbox also blocks the preview and the screenshots, so I judged from the code and the tests.

Everything from the last round is fixed:

  • High, a hosted child's traffic landed in the parent's log: logged now takes the data directory, and the child branch passes key. Queued lines keep the directory that owns them, so a flush or a prune never crosses into the parent. The new test in test/spawn-host.test.ts checks both directories. Against the old code the parent's directory would hold both paths, so the test would fail.
  • Low, unbounded read: readAccess reads each file from its end and stops after 4 MB in total.
  • Low, lines lost on stop: servers.stop flushes the queue.

Security is sound:

  • The person reading comes from verified auth. Only admins who are not agents can read, and the MCP tool also needs a write-scope token.
  • No query, cookie or token is written, and the client hash is keyed per day with the data directory's secret.
  • The client address comes from clientAddress after notePeer, not from a header the caller set.
  • The page escapes its output, and a test covers a <script> user agent.

The terminal screen is tracked in #1768, as I suggested last round. The other admin settings don't have one either.

A small non-blocking point: tailOf drops the partial first line without taking it off the budget, so the budget can be off by one line.

commented

The full gate on land was red and named no test; the pull is open again.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs