feat(setup): a credential form, and the line a wizard must not cross to have one - #465
Merged
Conversation
…to have one After setup creates a config, a database and a rule library, the next thing a first run needs is a market-data credential -- `keel fetch` authenticates even in paper mode, where no order can be placed. A desktop user has no terminal to type one into, so the browser has to accept it. THAT REQUIRED MOVING A LINE I DREW MYSELF, and the move is the substance of this change. The rule was "only MECHANICAL steps may be actions", with the reasoning that a JUDGEMENT step is the operator's -- "a wizard may collect and record them but must never decide them". But collecting and recording IS a write, so the rule as written forbade the very thing D4 asks for. It was too strong in the wrong dimension. `StepKind` gains `OPERATOR_INPUT`, split out of `JUDGEMENT`, because the two are different things: * A JUDGEMENT is a DECISION only a human may make -- a Shariah classification, a promotion. A form that recorded one would be making a compliance ruling on the operator's behalf, and no amount of "but they clicked it" makes that the same as their having decided it. * An OPERATOR_INPUT is a FACT only the operator possesses -- an API key. A wizard can record one and could not possibly invent one, so a form is safe here in a way it is not there. Collapsing them would have forced one of two bad outcomes: either the browser could record an attestation, or it could never accept a credential and a desktop user could never configure keel at all. The pin is now stronger rather than looser: actions must cover only MECHANICAL or OPERATOR_INPUT steps, no JUDGEMENT or OFF_VENUE step may ever be one, AND an action must declare inputs exactly when its step is OPERATOR_INPUT -- so a mechanical action cannot quietly start accepting operator data, nor an operator-input one quietly stop requiring it. WHAT A SECRET IS NOT ALLOWED TO DO. It is never rendered back into the page, not even on a re-render after failure: pre-filling a password field puts the secret in the page source, where it survives a screenshot, a view-source and anything that saves the page. A failed submission must be retyped, and that is the correct cost. The redirect carries the step key and nothing else -- a value in a Location is a secret in browser history, in the Referer of anything the page later loads, and in any proxy log in between, which is the whole reason the form is a POST. `autocomplete="off"` keeps a password manager from offering to store an exchange API key as a website login. And only the fields an action DECLARES are forwarded: the form is attacker-shaped input the moment anyone can craft a POST, so an action never receives a key it has no name for. Both fields are required and neither has a default. An action that could fill in a field the operator left blank is one that could record something they never supplied. The confirmation is read back through the same resolver a real caller uses, so a `.env` that shadows what was just stored is reported now rather than at the first fetch that used the other key. The checklist reports WHERE the credential comes from for the same reason. VERIFIED END TO END against a live server and the real macOS Keychain: config, database, rules and credentials all 303; the checklist then reads "done · operator_input · stored in the OS keychain"; the secret appears in no page, no redirect and no log; and reading it back through `keel_core.secrets` returns it from the keychain, matching what was submitted. 4069 passed, 3 skipped (7 new). ruff clean repo-wide; mypy clean over keel + packages. Refs #437, #436, #18. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #437 (D4), #436 (D3). Milestone: Desktop distribution (#18).
After setup creates a config, a database and a rule library, the next thing a first run needs is a market-data credential —
keel fetchauthenticates even in paper mode, where no order can be placed. A desktop user has no terminal to type one into, so the browser has to accept it.That required moving a line I drew myself
The rule was "only
MECHANICALsteps may be actions", reasoning that aJUDGEMENTstep is the operator's — "a wizard may collect and record them but must never decide them".But collecting and recording is a write. The rule as written forbade the very thing D4 asks for. It was too strong in the wrong dimension.
StepKindgainsOPERATOR_INPUT, split out ofJUDGEMENT, because the two are different things:JUDGEMENTOPERATOR_INPUTA form that recorded a judgement would be making a compliance ruling on the operator's behalf, and no amount of "but they clicked it" makes that the same as their having decided it. A wizard can record an API key and could not possibly invent one.
Collapsing the two would have forced one of two bad outcomes: either the browser could record an attestation, or it could never accept a credential — and a desktop user could never configure keel at all.
The pin is stronger, not looser. Actions must cover only
MECHANICALorOPERATOR_INPUTsteps; noJUDGEMENTorOFF_VENUEstep may ever be one; and an action must declare inputs exactly when its step isOPERATOR_INPUT— so a mechanical action cannot quietly start accepting operator data, nor an operator-input one quietly stop requiring it.What a secret is not allowed to do
Locationcarries the step key and nothing else. A value there is a secret in browser history, in theRefererof anything the page later loads, and in any proxy log in between — the whole reason the form is a POST.autocomplete="off", so a password manager does not offer to store an exchange API key as a website login.Both fields are required and neither has a default: an action that could fill in a field the operator left blank is one that could record something they never supplied.
The confirmation is read back through the same resolver a real caller uses, so a
.envthat shadows what was just stored is reported now rather than at the first fetch that used the other key.Verified end to end
Against a live server and the real macOS Keychain:
4069 passed, 3 skipped (7 new).
ruff checkclean repo-wide,mypyclean overkeel+packages.Next
Market data — the last mechanical step before a paper deployment actually does something. It needs a background job with progress, since a first fetch across the allowlist runs for minutes and a request that blocks that long is not a button.
🤖 Generated with Claude Code