Skip to content

feat(setup): attest and promote from the browser, on the axis that turned out to be the right one - #469

Merged
eaitbrahim merged 1 commit into
mainfrom
feat/d4-judgement-forms
Aug 20, 2026
Merged

feat(setup): attest and promote from the browser, on the axis that turned out to be the right one#469
eaitbrahim merged 1 commit into
mainfrom
feat/d4-judgement-forms

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Refs #437 (D4), #436 (D3). Stacked on #467 — retarget to main once that merges.

The rule I wrote was wrong, and this fixes the rule rather than the goal

Two JUDGEMENT steps live in the paper stage — attesting an asset, promoting a rule. So "no judgement step may ever be an action" made nothing safer. It made a terminal-free paper deployment impossible, which is the entire point of the milestone.

step stage among the eleven?
assets_attested paper no
rule_promoted paper no
subscription_attested live no
withdrawals_attested live yes

The invariant moves to where it always belonged

Not one of the eleven actions in keel/capabilities.py may be reachable from keel/web/ — already scanned and pinned independently. StepKind was a poor proxy for it, tried twice ("MECHANICAL only", then "+ OPERATOR_INPUT") and wrong both times.

The test now asserts by identity: every action's step is checked against the eleven invocations, and withdrawals attest --enabled is named as the judgement step that consequently has no form. That is the line the code already drew — taste had nothing to do with it.

The step-kind rule becomes the secondary policy it should have been: a wizard may record what the operator supplies; it may never supply it. An action over a judgement step must declare inputs, every one required, with nothing pre-filled and no select opening on a valid option.

That last clause is not pedantry

pays_yield as a checkbox would open unticked, and unticked is no — the permissive answer, since a yield-bearing asset fails the screen (KB §28.4).

A form whose default answer is the compliant one attests on the operator's behalf. So it is a select that opens on a disabled choose… and is required: it cannot be submitted without someone answering. backing is the same shape.

Exposing an attestation is safe for a reason its own CLI command states:

an attestation cannot itself place an order or raise a cap, and the screen it feeds only ever ADMITS to a list that guards.py rail 1 still enforces per-trade

Promotion has no force field, and that is load-bearing

attempt_promotion's docstring is explicit:

FORCE carries no gate HERE … the O3 contract is the front-end's to keep, never the service's to assume

The console keeps that contract with a typed terminal confirmation. A browser cannot, so this front-end simply has no force path — and force-promote is itself one of the eleven. force=False is hard-wired and pinned by test. Promotion re-runs a backtest, so it runs as a job for the same reason a fetch does.

Verified end to end

303  BTC    303  ETH    303  PAXG          ← attested from the browser
blank source → recorded NOTHING

sqlite: ('BTC','native',0,'https://example.test/ruling','operator')
        ('ETH','native',0,...)  ('PAXG','native',0,...)

checklist: unattested: ADA, LINK, LTC, SOL, XLM
           Withdrawal capability attested → still shows `keel withdrawals attest --enabled`

Exactly the supplied values, nothing defaulted, and the one gated step still terminal-only.

4087 passed, 3 skipped (5 new). ruff check clean repo-wide, mypy clean.

What is still deliberately absent

  • Withdrawal attestation — one of the eleven. That is D3 — A GUI human gate, architecturally distinct from _is_interactive #436's browser-gate question, and I would leave it unanswered until someone needs a terminal-free live deployment.
  • Off-venue steps — nothing to record that would be true. A form saying "I turned off USDC Rewards" records what was asserted, not what is, which the operator runbook names as worse than an honest manual step.
  • confirm_cycle — unobservable by construction.

🤖 Generated with Claude Code

Base automatically changed from feat/d4-market-data-job to main August 20, 2026 22:22
…rned out to be the right one

Two JUDGEMENT steps live in the PAPER stage -- attesting an asset, promoting a rule -- so the rule
I wrote two changes ago ("no judgement step may ever be an action") did not make anything safer.
It made a terminal-free paper deployment impossible, which is the entire point of the milestone.
This fixes the rule, not the goal.

THE INVARIANT MOVES TO WHERE IT ALWAYS BELONGED: the capability registry. Not one of the eleven
actions in `keel/capabilities.py` may be reachable from `keel/web/`, and that was already scanned
and pinned independently. `StepKind` was a poor proxy for it, tried twice -- "MECHANICAL only",
then "MECHANICAL or OPERATOR_INPUT" -- and wrong both times. The test now asserts by IDENTITY:
every action's step is checked against the eleven invocations, and `withdrawals attest --enabled`
is named as the judgement step that consequently has no form. That is the line the code already
drew; taste had nothing to do with it.

The step-kind rule becomes the secondary policy it should always have been: **a wizard may record
what the operator supplies; it may never supply it.** An action over a judgement step must declare
inputs, every one required, with nothing pre-filled and no select opening on a valid option.

THAT LAST CLAUSE IS NOT PEDANTRY. `pays_yield` as a checkbox would open unticked, and unticked is
`no` -- the PERMISSIVE answer, since a yield-bearing asset fails the screen (KB §28.4). A form
whose default answer is the compliant one attests on the operator's behalf. So it is a select that
opens on a disabled "choose…" and is `required`: it cannot be submitted without someone answering.
`backing` is the same shape.

Exposing an attestation is safe for a reason its own CLI command states: "an attestation cannot
itself place an order or raise a cap, and the screen it feeds only ever ADMITS to a list that
`guards.py` rail 1 still enforces per-trade". It is not one of the eleven and the CLI imposes no
ceremony on it beyond the mandatory source.

PROMOTION HAS NO FORCE FIELD, and that is load-bearing rather than an omission.
`attempt_promotion`'s docstring is explicit: force "carries no gate HERE ... the O3 contract is the
front-end's to keep, never the service's to assume." The console keeps that contract with a typed
terminal confirmation. A browser cannot, so this front-end simply has no force path -- and
force-promote is itself one of the eleven. `force=False` is hard-wired and pinned by test.
Promotion re-runs a backtest, so it runs as a job for the same reason a fetch does.

The backing choices are literals to keep `keel.compliance.screen` off the CLI's import path; that
duplication is pinned against `KNOWN_BACKINGS`, because a backing kind added there and missing
here would be silently un-attestable from the browser.

VERIFIED END TO END against a live server: three assets attested from the browser and recorded
with exactly the supplied sector, backing, yield answer, source and attester; a submission with a
blank source recorded NOTHING; the checklist then names the assets still outstanding; and
`withdrawals attest --enabled` still shows its CLI command rather than a form.

4087 passed, 3 skipped (5 new). ruff clean repo-wide; mypy clean over keel + packages.

Stacked on #467 -- it builds directly on that PR's `Action`/`ActionInput` and job runner.

Refs #437, #436, #18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eaitbrahim
eaitbrahim force-pushed the feat/d4-judgement-forms branch from e820c87 to 4c9971c Compare August 20, 2026 22:25
@eaitbrahim
eaitbrahim merged commit 57de5bc into main Aug 20, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat/d4-judgement-forms branch August 20, 2026 22:25
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