Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions keel/commands/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import webbrowser
from typing import Any

import click

Expand Down Expand Up @@ -57,13 +58,19 @@ def serve_cmd(ctx: click.Context, host: str, port: int, open_browser: bool) -> N
readable by anyone who can reach the port, with a cleartext token as the only obstacle.
"""
obj = ctx.obj or {}
# Resolved ONCE, here, and carried on the config in both its forms. `build_info()` shells out
# to git twice, and `/api/config` is polled by a service worker (#538) -- an endpoint that
# forks a subprocess to answer "which build is this" would make the cheapest question on the
# server the most expensive one.
build = _build_info()
cfg = ServeConfig(
host=host,
port=port,
token=new_session_token(),
db_path=obj.get("db_path") or default_db_path(),
config_path=obj.get("config_path") or default_config_path(),
build=_build_line(),
build=_build_line(build),
build_info=build,
)

if open_browser:
Expand All @@ -78,12 +85,24 @@ def serve_cmd(ctx: click.Context, host: str, port: int, open_browser: bool) -> N
ctx.exit(serve(cfg, echo=click.echo))


def _build_line() -> str:
"""The build identity in the page footer, so a screenshot of the UI says which build produced
it. Best-effort: a footer is not worth failing a server start over."""
def _build_info() -> Any:
"""The running build, resolved once, or `None`.

Best-effort: a build identity is not worth failing a server start over, and `None` is a state
the consumers already handle -- the footer renders empty and `/api/config` reports the version
absent rather than inventing one. Split out of `_build_line` when `/api/config` (#534) needed
the same object as STRUCTURE rather than as a sentence, so the two can never describe different
builds: parsing `describe()`'s output back into fields would be a display string being read as
data."""
try:
from keel.version import build_info

return build_info().describe()
return build_info()
except Exception: # pragma: no cover - metadata absent in odd environments
return ""
return None


def _build_line(build: Any) -> str:
"""The build identity in the page footer, so a screenshot of the UI says which build produced
it."""
return "" if build is None else str(build.describe())
22 changes: 18 additions & 4 deletions keel/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@
macOS app launched from Finder has no controlling terminal at all, and the GUI human gate (#436)
needs somewhere to live that is architecturally distinct from `_is_interactive`.

**This package has no write surface, and that is structural rather than a matter of discipline.**
The request handler implements `do_GET` and `do_HEAD` and nothing else, so every other method is
refused by `BaseHTTPRequestHandler` before any keel code runs. Write actions land in D3, behind a
gate of their own; until then there is no code path here that could grow one by accident.
**This package's write surface is a closed set, and that is structural rather than a matter of
discipline.**

The sentence that used to be here -- "the request handler implements `do_GET` and `do_HEAD` and
nothing else" -- stopped being true at #437 and is corrected rather than deleted, because
`server.do_POST` still points a reader at this file for the rule it must not break. The guarantee
now is stronger than the one it replaced, and it is the one anyone actually cares about:

* `do_POST` exists, and it routes ONLY through `keel.commands.setup.ACTIONS` -- idempotent,
non-destructive steps -- so a first-run user on a machine with no terminal can create a
deployment. "No POST at all" was a clean property that was also satisfied by a server which
could not set anything up.
* **Not one of the eleven capability-increasing actions in `keel/capabilities.py` is reachable
from this package**, asserted by a test that scans this source rather than by inspection. The
server cannot arm, release or spend. Attesting, promoting, releasing a halt and arming autonomy
remain CLI-only, behind the TTY gate; D3 (#436) is where a browser gate for those would go.
* The JSON API (#534, `keel/web/api.py`) is **reads only**. Every route in its table answers a GET
and 404s a POST, and it did not widen `API_PREFIX`'s existing `X-Keel-Client` gate by one byte.
"""
Loading