Skip to content

feat(console): the refusal now says how to GET the address it asks you to paste - #810

Merged
eaitbrahim merged 1 commit into
mainfrom
fix/refused-view-instructions
Sep 17, 2026
Merged

eaitbrahim merged 1 commit into
mainfrom
fix/refused-view-instructions

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

What the operator saw

keel is running, and did not admit this browser

...the token is new every run and is never written to disk -- so if keel has been
restarted since this browser last worked, the address printed THIS time is the one
that admits it. Paste that address into the address bar here...

[ Paste the address keel printed, or just its token ]  [Reconnect]

Cause named well. Remedy named nowhere.

Two failures, and the second is the bad one

1. keel open is never mentioned. It is named in server.py:1125, in the launchd plist
("This process prints its URL... where nobody is reading. Do not grep the log for it. Run
keel's open command"), and in runtime.py's own module docstring — everywhere except the screen
the locked-out operator is looking at.

2. "never written to disk" is no longer true, and it is the sentence that closes the door.
Since #756, that holds only for a server attached to a TERMINAL. Detached — launchd, a pipe, a
container — runtime.py records the address in a 0600 file precisely so keel open can hand
it back
. A reader who believes the sentence concludes the token is unrecoverable and restarts
the server, which mints a NEW one. refusedView's own docstring already warns about that move:
"sends them to restart the thing that is working… and makes the situation worse."

Evidence it fails in practice: on 2026-09-16 two people who know this codebase were locked out by
a daemon restart. One went to keel open --help; the other asked a colleague.

The change

An instruction block above the paste field — the command produces the value the field
consumes, so meeting the field first means being asked for something you do not have:

To get this run's address, from your keel deployment directory:

keel open --port 8765

It opens the console, and prints the address so you can paste it below. Attached to a terminal
keel prints the address and keeps nothing; run as a service it records the address for that
command and deletes it on shutdown.

The field's label moves from "Paste the address keel printed" — past tense about something a
daemon wrote to a log the plist says not to grep — to "Paste the address from that command".

Boxed, monospaced, user-select: all, because its whole purpose is to be copied. The detail
paragraph beside it runs five sentences; a command appended as a sixth is a command nobody sees.

Two constraints worth noting

The port comes from window.location, the same source and stated reason as
timelineExportUrl and the deployment card: a fact about where this page is, not a claim from a
payload that just refused us. Nothing else about the deployment is named — the path in
particular. The port is the one fact the reader demonstrably already has (they reached this page
on it), so naming it discloses nothing to a request that was not admitted. That begins to matter
once #648 lets this console answer beyond loopback.

openCommandNode returns a node and joins nothing. render.js may use neither template
literals nor + — test_render_contains_no_arithmetic bans the operator outright rather than
try to tell string concatenation from money, and my first attempt tripped exactly that gate.
append takes several strings, so the port is placed beside the command instead of joined to it.
A default-port URL gives location.port as the empty string and takes the bare command, which
keel open defaults correctly.

Verification

Six tests, each naming the mutation it rejects, asserted on code with comments stripped so the
prose explaining the change cannot stand in for the change. Mutation-verified:

mutant result
drop the command 3 failed
hardcode the port 1 failed
revert the label 1 failed
tests/web                       -> 972 passed
ruff check keel tests packages  -> All checks passed!
mypy                            -> clean (467 files; pre-existing PyYAML note)
pytest                          -> 6673 passed, 3 skipped

Not changed: the server-side 403 detail text still carries the stale "never written to disk"
sentence for non-browser clients. Worth a follow-up; this PR is the HTML view only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD

…u to paste

The view named the cause well and the remedy not at all. On 2026-09-16 two people who
know this codebase were locked out by a daemon restart and neither recovered from the
screen: one went to `keel open --help`, the other asked a colleague.

Worse than an omission, the refusal's prose says the token "is never written to disk".
Since #756 that holds only for a server attached to a TERMINAL -- run detached, under
launchd as every console on that machine is, `runtime.py` records the address in a 0600
file precisely so `keel open` can hand it back. A reader who believes the sentence
concludes the token is unrecoverable and restarts the server, minting a NEW one --
the exact move `refusedView`'s own docstring warns against.

`keel open` was already named in `server.py`, in the launchd plist ("Do not grep the log
for it") and in `runtime.py`'s module docstring. Everywhere except the screen the
locked-out operator is looking at.

Added above the paste field, because the command PRODUCES the value the field consumes,
and as a boxed `user-select: all` code block rather than a sixth sentence in a paragraph
that already runs five. The port comes from `window.location` -- the same source and the
same reason as `timelineExportUrl` -- and nothing else about the deployment is named. The
path in particular: the port is the one fact the reader demonstrably already has, so
naming it discloses nothing to a request that was NOT admitted, which starts to matter
once #648 lets this console answer beyond loopback.

`openCommandNode` returns a NODE and joins nothing: this file may use neither template
literals nor `+`, since `test_render_contains_no_arithmetic` bans the operator outright
rather than try to tell string concatenation from money. The empty-string port of a
default-port URL takes the bare command, which `keel open` handles itself.

Six tests, each naming the mutation it rejects; verified by mutation that dropping the
command, hardcoding the port, and reverting the label each fail a different count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD
@eaitbrahim
eaitbrahim merged commit a1ee344 into main Sep 17, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the fix/refused-view-instructions branch September 17, 2026 21:14
eaitbrahim added a commit that referenced this pull request Sep 25, 2026
…regression) (#817)

#810 added `const note = el("p")` to refusedView beside the existing
`const note = el("p", "detail")`. A duplicate declaration in one scope
is a SyntaxError for the whole module, so render.js did not load and
every console built from main showed a blank shell. The first is now
`howNote`.

Nothing caught it because the repo has no JavaScript toolchain. A new
scan in test_client_assets fails any module that declares one name
twice in one block scope, with a self-test that it catches the #810
shape and passes sibling blocks and loop heads.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim added a commit that referenced this pull request Sep 25, 2026
…e switcher with no tokens (#818)

MINOR: #810 and #814/#816 add console features. No schema change since
0.16.0, so a deployment needs no `keel migrate` step beyond the usual
idempotent run.

What lands:

  #807 -- a quote-increment cache record predating quote_increment is a
  MISS, not an "unknown".
  #810 -- the refused view says how to get the address it asks you to
  paste; #817 fixes its duplicate `const note`, which blanked the console.
  #813 (#812) -- the cycle line ends `vetoed=N (rail, ...)`, so a rail
  veto no longer reads as a silent drop.
  #815 + #816 (#814) -- a console switcher beside the session chip.
  /api/config carries no session token; /switch/<port> redirects into the
  chosen console's own hand-off, local deployments only.

Eight workspace versions and the inter-package `==` pins move together,
plus the refreshed uv.lock the release workflow checks first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant