Skip to content

Console: a reloaded console keeps its HALT after the remote lifetime (#985) - #1001

Merged
eaitbrahim merged 3 commits into
mainfrom
fix/985-halt-after-lifetime
Oct 13, 2026
Merged

eaitbrahim merged 3 commits into
mainfrom
fix/985-halt-after-lifetime

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #985

What changes

Since #983 (PR #987) the server takes a halt carrying the session cookie and the halt token past the 12 h remote session lifetime on keel serve --external-host, but /api/pulse, the only endpoint that serves the token, keeps the lifetime. So a console reloaded after the lifetime drew no HALT button.

This follows the design the owner approved on the issue:

Tests (TDD: written first, failed because halt was None)

  • With the cookie, an expired Pulse carries the halt token and the resume command, for both a running and a halted switch. The served token halts. The body contains no gates token, CSRF token or session token.
  • With no cookie or a wrong cookie: halt: null and no token in the body.
  • Every other /api/* GET (13 routes plus the CSV export, with a count asserted): halt: null.
  • A POST to /api/pulse after expiry: halt: null.
  • The served token releases neither Tier 1 action in either header, and the kill switch stays engaged.
  • A fresh remote Pulse is unchanged: 200 with data.halt_token, envelope halt: null.
  • Client structure: in paint, the halt arm comes before the refused arm, which comes before the stopped view. Each arm appears exactly once, and main.js contains no "Session expired" literal.

Mutation checks (each on a saved copy, with a check that the source changed, then restored): dropping the cookie check, the method check, the route check, the halt= argument or refusal_document's pass-through, serving the gates token, a dead client arm, and a branch on the title. All of them were killed.

Verification

  • Full pytest: 8442 passed, 3 skipped. mypy, ruff and ruff format are clean. node --check is clean on main.js, render.js, api.js and actions.js.
  • Real Chromium (Playwright) against a temp deployment on port 8791 with an external host and a lifetime set to end 45 s after start:
    • Before the lifetime ended, Pulse rendered as usual with one HALT button.
    • After it ended, a reload served the shell from the service worker. The Pulse page showed the HALT button above the "Session expired" card, with no reconnect form.
    • Clicking HALT and confirming showed the line "HALTED — resume with keel resume". kill_switch read true from the database (opened read-only).
    • A request without the cookie got halt: null.

Known limit, unchanged by this PR: a browser with no cached shell is refused the page itself after the lifetime, so the refusal still names keel kill.

🤖 Generated with Claude Code

…985)

Since #983 the server admits a halt carrying the session cookie and the
halt token past the 12 h remote session lifetime, but /api/pulse - the
only endpoint serving the token - keeps the lifetime, so a console
reloaded after it drew no HALT button.

Owner-approved design: the expired /api/pulse refusal (GET/HEAD only)
carries #958's {halt_token, resume_command} when the request carries
this session's cookie (KeelHandler._expired_pulse_halt, keyed on
ApiRoute.halt_on_failure). Without the cookie it carries nothing extra.
It is still a 403 "Session expired" with data null: no release, no
setup write, no other read re-opened, no other endpoint serves the
halt token, and the gates token is served nowhere (#968).

Client: paint's data-null branch takes the HALT arm (pulseFailedView)
before the 403 arm, branching on the presence of `halt`, never on the
refusal's title. Docstrings that said a reloaded console cannot get the
token are corrected; the #634 guard pin allows the `} else ` prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread tests/web/test_server.py Outdated
Comment thread keel/web/api.py
…halt's second caller (#985)

The release POSTs passed by vacuum: the expired session refused them whatever token they carried, so a mutant serving gates_token survived. Assert the served token is the halt token and not the gates token, and that each release is refused by the expiry gate. pulse_halt's docstring now names _expired_pulse_halt as its second caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread keel/web/payload.py
Comment thread keel/web/server.py
Comment thread tests/web/test_server.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread keel/web/server.py
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.

Console: keep the HALT control on Pulse after the remote session lifetime expires (client half of #983)

1 participant