feat(coinbase): refuse a derivative-capable account, and say what that does not prove (#666) - #692
Merged
Merged
Conversation
…t does not prove (#666) Stage 1, written against what a probe of the live account actually returned rather than against a guess. THE PROBE, 2026-09-02, two read-only calls: get_transaction_summary -> margin_rate present: True, value: NULL get_portfolios -> DEFAULT 'Default', CONSUMER 'My portfolio' `margin_rate` is present-and-NULL. It is in the response SCHEMA for every account, so its presence signals nothing and a check keyed on presence would refuse every deployment -- the exact failure this issue was filed to avoid. A non-null value is plausibly a signal, and could not be verified: there is no margin-enabled account to compare against, so that refusal branch would ship untested. A compliance gate whose refusal path has never fired against a true positive is the gate that eventually fires on the compliant case and gets disabled in anger. It is surfaced as a WARNING on `get_fee_summary`, which already fetches that response, so the visibility costs no extra request. WHAT IS UNAMBIGUOUS is the portfolio list. DEFAULT and CONSUMER are the spot portfolios; INTX is the international/perpetuals one, and its presence is derivative capability on the account. `verify_cash_account` refuses on it and is called from `_build_broker` beside Alpaca's, so every command that builds a broker inherits it. One `get_portfolios` read per build. PASSING MEANS NO CONTRADICTION FOUND, NEVER PROOF, and the docstring says so because a future reader taking a pass as evidence has the guarantee backwards. Coinbase exposes no cash-versus-margin field for spot -- every margin, borrow, leverage and liquidation field in its SDK lives in the futures, perpetuals or derivative-order types -- so nothing here can issue a cash posture. #691 carries the operator attestation that can, on rail 17's pattern.⚠️ IT PASSES ON AN UNREADABLE RESPONSE, THE OPPOSITE OF THE ALPACA CHECK, and the asymmetry is a property of the venues rather than an inconsistency. Alpaca fails CLOSED because `multiplier` IS the classification: a readable answer is definitive and silence is a distinct third state worth refusing on. Here the check can only refute, so silence proves nothing a readable response would not also have failed to prove -- and failing closed would refuse a compliant deployment on a network blip while establishing nothing. `docs/fiqh-basis.md`'s open question is rewritten to the residual that remains, and `test_the_venue_boundary_premise_is_stated_not_hidden` FAILED when this landed, which is what it was written for. It has now been re-pointed twice and its docstring records both moves: first from a proxy that stopped tracking what it stood for, now from a premise that genuinely changed. Tests: `tests/broker_coinbase/test_adapter.py` (+8) and `tests/commands/test_coinbase_posture_wiring.py` (2, new -- the call site, because three PRs this week shipped a helper whose call site was unpinned). 6 mutants, 6 killed, including one that made the unreadable case fail closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
CI caught what a narrower local check could not. `Transport` is the adapter's STATEMENT of what it needs from a client, and `verify_cash_account` reached `get_portfolios` without declaring it there -- so mypy saw a call to an attribute the protocol does not have, and the test fakes were under no obligation to provide it. MY VERIFICATION WAS THE BUG. `[tool.mypy]` sets `files = ["packages", "keel", "tests"]`, so bare `mypy` checks 389 source files; I had been running `mypy keel/`, which checks 122 and skips every broker package -- including the one this change is entirely inside. Bare `mypy` now passes on all 389. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
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.
Closes #666 (Stage 1). Stage 2 — the operator attestation — is filed as #691.
Written against what a probe of the live account actually returned, not against a guess.
The probe
margin_rateis present-and-null. It's in the response schema for every account, so its presence signals nothing — a check keyed on presence would refuse every deployment, which is the exact failure this issue was filed to avoid.A non-null value is plausibly a signal, and could not be verified — there's no margin-enabled account to compare against, so that refusal branch would ship untested. A compliance gate whose refusal path has never fired against a true positive is the gate that eventually fires on the compliant case and gets disabled in anger. It's surfaced as a warning on
get_fee_summary, which already fetches that response, so visibility costs no extra request.What is unambiguous
The portfolio list.
DEFAULTandCONSUMERare the spot portfolios;INTXis the international/perpetuals one, and its presence is derivative capability on the account.verify_cash_accountrefuses on it, called from_build_brokerbeside Alpaca's — oneget_portfoliosread per build.Passing means "no contradiction found", never proof
The docstring says so, and a test asserts it does, because a reader taking a pass as evidence has the guarantee backwards. Coinbase exposes no cash-versus-margin field for spot — every margin, borrow, leverage and liquidation field in its SDK lives in the futures, perpetuals or derivative-order types. Nothing here can issue a cash posture. #691 carries the attestation that can, on rail 17's pattern.
A property of the venues, not an inconsistency. Alpaca fails closed because
multiplieris the classification: a readable answer is definitive, so silence is a distinct third state worth refusing on.Here the check can only refute, so silence proves nothing a readable response would also have failed to prove — and failing closed would refuse a compliant deployment on a network blip while establishing nothing. A mutation making it fail closed is one of the six killed.
The pin that fired
docs/fiqh-basis.md's open question is rewritten, andtest_the_venue_boundary_premise_is_stated_not_hiddenfailed when this landed — which is what #672 wrote it for. It's now been re-pointed twice, and its docstring records both moves: first from a proxy that stopped tracking what it stood for, now from a premise that genuinely changed.Verification
Tests:
tests/broker_coinbase/test_adapter.py(+8),tests/commands/test_coinbase_posture_wiring.py(2, new — the call site, because three PRs this week shipped a helper whose call site was unpinned and whose removal left every other test green).6 mutants, 6 killed: INTX no longer refusing, case-sensitive matching, the unreadable case failing closed, the
margin_ratewarning removed, the warning firing on null, and the build seam skipping the check.Full suite 5268 passed / 3 skipped; ruff and mypy clean. Written test-first.