Skip to content

The browser's status page reports autonomy OFF while it trades, and an expired rail-17 attestation as fresh #548

Description

@eaitbrahim

Found while building #536. Shipped, user-facing, and two of the five are binary safety states rather than cosmetic.

Root cause

keel/web/render.py's render_status reads report fields through getattr(obj, "name", default). Where the attribute does not exist, the default is returned silently — a frozen dataclass would have raised AttributeError at the first render, but the default suppresses it. Five attributes it asks for are not on the dataclasses.

1. Autonomy always prints "off" — render.py:351

autonomy_text = "on" if getattr(autonomy, "enabled", False) else "off"

AutonomyStatus (keel/commands/status.py:44-51) has live, autonomous, autonomous_until, updated_ts, profile_readable. There is no enabled.

So the expression is getattr(..., "enabled", False)False"off", unconditionally, for every deployment, always.

A deployment placing orders unattended reads autonomy: off in the browser. This repository's own CLI states the stakes in its wording — keel/commands/status.py:481:

autonomy_line = "autonomy: ON -- orders placed WITHOUT asking" if a.live else "autonomy: off"

The CLI reads a.live directly and is correct. The browser is the only wrong surface.

2. An expired rail-17 attestation renders as fresh — render.py:375

expired = bool(getattr(attestation, "expired", False))

WithdrawalAttestationStatus (status.py:93-105) carries state: str # "attested" | "suspended" | "expired" | "unattested", plus enabled and attested_at. There is no expired.

Always False. An expired attestation — the state that halts live entries — displays as fresh, in green. The page reports health while entries are being refused, which is the failure mode most likely to send an operator looking in the wrong place.

3–5. Three columns that are always blank

line asks for actual field effect
render.py:392 pos.entry_fill entry_price every open position's entry price renders --
render.py:419 rule.name RuleSummary has id, kind, status, product_id, params — no name the live-rule column is always blank
render.py:~ subscription.status, .attested_ts stored_status / effective_status; no attested timestamp on the row both columns blank

Cosmetic next to 1 and 2, but the same cause.

Why it survived

Nothing fails. There is no exception, no empty page, no log line. getattr with a default converts a rename or a typo into a plausible-looking value, and every one of these renders as a reasonable reading — "off", "fresh", "--", "". A page reporting a quiet, non-autonomous, compliant deployment is exactly what an operator expects to see, so there is nothing to notice.

The fix, and the part worth arguing about

Reading the attributes directly is the one-line fix and leaves the trap: the next rename re-creates it silently.

Delete the getattr fallbacks entirely. These are frozen dataclasses in the same repository, not external data — a missing attribute is a bug, and AttributeError at render time is the correct, loud outcome. The defaults exist to make rendering total, but a page that renders is not a page that is right, and totality bought at the cost of correctness is a bad trade for a trading console.

Then pin it: a test constructing a fully-populated report and asserting the rendered page shows autonomy ON and an expired attestation as expired. Neither is currently possible to observe.

Relationship to the rewrite

#536's client reads the API (keel/web/api.py), which uses the correct field names, so the new status view does not inherit any of this — its docstring records each divergence from render.py and why.

render.py is deleted at #540, so all five disappear then. That is not a reason to wait. Until the rewrite lands, every operator running keel serve on a live, autonomous deployment reads that autonomy is off — and if you want / correct before #540, this is a small independent fix.

Related: #542 (the same shape — pct() in the browser, correct in the CLI).

Acceptance

  • render_status reads report attributes directly; no getattr fallback in the module.
  • A populated-report test asserts autonomy renders ON when live is true.
  • A populated-report test asserts an expired attestation renders as expired, not fresh.
  • Entry price, rule name and subscription columns carry their values.
  • A renamed report field breaks a test rather than rendering a plausible default.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingrailsUn-overridable safety rail / guard (Compliance & rails)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions