Skip to content

feat(cli): keel open — the way back into a console nobody watched start (#756) - #759

Open
eaitbrahim wants to merge 1 commit into
mainfrom
feat-keel-open
Open

feat(cli): keel open — the way back into a console nobody watched start (#756)#759
eaitbrahim wants to merge 1 commit into
mainfrom
feat-keel-open

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Step 3 of #756. Step 4 — the four com.keel.serve.*.plist files — is unusable without it, which is why it comes first.

$ keel open --port 8765
Opening the keel console at:

    http://127.0.0.1:8765/?token=ab12...

This address carries the running server's session token. Stopping keel revokes it.

⚠️ This writes a session token to disk. Read this part.

security.py layer 3 rests on the token being ephemeral, and serve prints the claim every run: "the token is new every run and is never written to disk." A lockfile makes that sentence false. So it is written under exactly one condition:

The record exists only when the URL is not being shown to a human.

sys.stdout.isatty() decides.

  • Attached to a terminal — nothing is written, the posture is byte-for-byte what it was, and the sentence is still printed because it is still true.
  • Detached (launchd, a pipe) — a different sentence is printed, naming the file, its mode and keel open. Telling an operator their live credential is not on disk while writing it there is the class of false safety assurance _session_banner exists to refuse.

The argument is not that persisting a token is fine. It is that on the detached path the token is already in StandardOutPath at the daemon's umask. The real choice is between that log line and a 0600 file in a 0700 directory, deleted on shutdown and checked for staleness before it is offered. The second is strictly less exposure, and it is the only one that makes unattended serve usable.

security.py layer 3 now says which of the two runs it is describing.

If you'd rather have uniform behaviour — always write, or never write and drop keel open — that is a one-line change and I'd rather you made it deliberately than inherited mine.

The file is a hint, never the authority

forget covers the clean stop; a SIGKILL or power cut does not. live_record re-checks the pid with signal 0 before offering anything — handing over a URL that refuses the browser reads as keel being broken rather than stopped. pid <= 0 is refused before the call, because os.kill(0, 0) signals the process group and would report a bad record as live.

Written through a temporary and renamed, so no reader sees a half-written record. Corrupt JSON reads as absent rather than raising. One file per port#756 counts four deployments, and one shared file would make the last server to start the only reachable one. The token is never in the filename: a listing is readable by anything that can list the directory, even when the file is not.

Three absences, three sentences

No server, a server started from a terminal, and a server that was killed look identical from here. "not found" would send an operator hunting for a bug in the two cases where nothing is wrong, so each gets its own message — and the interactive one names the terminal that has the URL.

Checks

6,352 passed / 3 skipped, ruff and mypy clean. Four mutants killed: record written while a human watches, record never forgotten, liveness check skipped, file at 0644.

Not covered here: that launchd actually produces a non-tty stdout on your machine. That is step 4's first observation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6

…art (#756)

`keel serve` prints its URL once, with the session token in it. That is enough
when a human is watching and useless when nothing is: under launchd the line goes
to StandardOutPath, and the route back into your own console becomes grepping a
log for a token. Step 4 of #756 -- the four serve plists -- is unusable without
this, which is why it comes first.

THE TOKEN GOES TO DISK, AND THAT IS BOUNDED BY ONE RULE

`security.py` layer 3 rests on the token being ephemeral, and `serve` PRINTS the
claim every run: "the token is new every run and is never written to disk". A
lockfile makes that sentence false, so it is written under one condition:

    THE RECORD EXISTS ONLY WHEN THE URL IS NOT BEING SHOWN TO A HUMAN.

`sys.stdout.isatty()` decides. Attached to a terminal nothing is written, the
posture is byte-for-byte what it was, and the sentence is still printed because
it is still true. Detached, a DIFFERENT sentence is printed -- naming the file,
its mode, and `keel open` -- because telling an operator their live credential is
not on disk while writing it there is the class of false safety assurance
`_session_banner` exists to refuse.

The argument is not that persisting a token is fine. It is that on the detached
path the token is ALREADY in StandardOutPath at the daemon's umask, so the real
choice is between that log line and a 0600 file in a 0700 directory that is
deleted on shutdown and checked for staleness before it is offered. The second is
strictly less exposure, and it is the only one that makes unattended serve usable.

`security.py`'s layer 3 now says which of the two runs it is describing.

THE FILE IS A HINT, NEVER THE AUTHORITY

`forget` covers the clean stop. A SIGKILL or a power cut does not, so
`live_record` re-checks the pid with signal 0 before offering anything -- handing
over a URL that refuses the browser would read as keel being broken rather than
as keel being stopped. `pid <= 0` is refused before the call, because
`os.kill(0, 0)` signals the process GROUP and would report a bad record as live.

Written through a temporary and renamed, so no reader sees a half-written record.
Corrupt JSON reads as absent rather than raising: `keel open` must not traceback
at an operator whose server just died. One file per PORT, because #756 counts
four deployments and one shared file would make the last server to start the only
reachable one. The token is never in the filename -- a listing is readable by
anything that can list the directory, even when the file is not.

THREE ABSENCES, THREE SENTENCES

No server, a server started from a terminal, and a server that was killed look
identical from here. "not found" would send an operator hunting for a bug in the
two cases where nothing is wrong, so each gets its own message and the
interactive one names the terminal that has the URL.

6,352 passed / 3 skipped, ruff and mypy clean. Four mutants killed: the record
written while a human watches, the record never forgotten, the liveness check
skipped, and the file at 0644.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6
Comment thread keel/web/runtime.py
Comment on lines +102 to +110
json.dumps(
{
"pid": os.getpid(),
"host": host,
"port": int(port),
"token": token,
"started_ts": int(time.time()),
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants