Skip to content

Trading gates: user_decision evidence, readiness that needs holding evidence, and a signed live-go - #295

Merged
seathatflowsinourveins merged 6 commits into
mainfrom
claude/checker-hardening-20260925
Oct 4, 2026
Merged

seathatflowsinourveins merged 6 commits into
mainfrom
claude/checker-hardening-20260925

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Scope

  • What this PR changes: it hardens scripts/trading_gates.py before any live go. The 2026-09-25 readiness review's step 23 named three items, and this PR delivers all three:
    • a user_decision evidence class;
    • rung readiness that counts an established gate only while its evidence holds;
    • an enforced authorship control for live-go: a detached SSH signature verified with ssh-keygen -Y verify.
  • Base commit: 39e18ed5 (head ce05fbb8)
  • Lane: lane:trading
  • Owned paths touched:
    • scripts/trading_gates.py
    • tests/test_trading_gates.py
    • catalogs/us-equities/gates-20260922.json: a new top-level authorship block, a dated sentence in the live-go note, and a rewritten top-level scope that states the readiness predicate (an established gate counts only while its evidence holds) and the exception for the user-authored live-go flip, as the new decision record describes. No status, flip condition or evidence class changes.
    • catalogs/us-equities/README.md
    • docs/decisions/2026-09-25-live-go-authorship-control.md (new)
    • manifests/evidence.json: re-registration only, in the last commit, per docs/lanes.md.

This PR does not write or flip live-go. It creates no docs/decisions/live-go.md, signature, allowed_signers entry or key. live-go stays status: user_decision, evidence_class: none. It changes no other gate's status, condition or evidence rules. It does not touch PR #183 or #191. It does not change any engine module: runner.py, safety.py, transport.py, order-contract/order_contract.py, and everything else that adaptive-paper/source-hashes.json covers, are byte-identical to main. It makes no broker or market-data calls.

SOTA sources

  • Signature verification, by installing OpenSSH's own tool rather than reimplementing it: openssh/openssh-portable, tag V_9_6_P1 (commit b24f772e), ssh-keygen.1.
    • -Y verify (lines 765-790) reads the message on standard input, with -n namespace, -s signature, -I identity and -f allowed signers. "Successful verification by an authorized signer is signalled by returning a zero exit status". The checker therefore counts only exit 0.
    • ALLOWED SIGNERS (from line 1225) defines the file format and the namespaces= and valid-before= options that the setup uses.
    • Lines 758-764 advise naming custom namespaces NAMESPACE@YOUR.DOMAIN, hence live-go@native-agent-stack.
  • Invocation pattern, following git's SSH signature verification: git/git, tag v2.43.0, gpg-interface.c, verify_ssh_signed_buffer (lines 447-580).
    • Git refuses verification when no allowed signers file is configured (lines 469-472).
    • It runs ssh-keygen -Y verify -n <namespace> -f <allowed signers> -I <principal> -s <signature> with the payload on standard input (lines 557-575).
    • This checker differs in two ways: it pins the principal in the catalog rather than calling -Y find-principals, and it passes no -Overify-time, so a valid-before expiry applies at check time.
  • The readiness predicate and the user_decision class change this repository's own checker contract: the gates document's scope and the 2026-09-25 readiness review's step 23. No external implementation applies to these two predicates.
  • The same sources are recorded in the decision record's Sources section.

What changes

  1. Evidence class user_decision. It is valid only for a gate owned by user-decision whose flip_condition is null. Validation refuses it on any other gate. The other classes keep their rules.

  2. Readiness needs holding evidence. A required gate counts toward rung_ready only while it is recorded established and its condition holds now. Otherwise it is listed in blocking and keeps its rung and every later rung not ready. This covers a missing or failing receipt and, for live-go, a signature that does not verify. Before this change, pending read only the recorded status (scripts/trading_gates.py:293 at 0074a0c), so simulation V1 could show status: failed alongside rung_ready.live: true. The error such a gate already produced is unchanged, and optional gates still never block.

  3. The live-go authorship control. The gates document's top-level authorship block names live-go's control:

    • method: ssh-keygen -Y verify
    • signature: docs/decisions/live-go.md.sig
    • allowed_signers file: docs/decisions/live-go.allowed_signers
    • principal: live-go-signer
    • namespace: live-go@native-agent-stack

    live-go holds only when its document is non-empty and ssh-keygen -Y verify -f <allowed_signers> -I <principal> -n <namespace> -s <signature> accepts its exact bytes. It fails closed in every other case:

    • the document, signature or allowed_signers file is absent or empty;
    • a path resolves outside the tree;
    • ssh-keygen is missing;
    • ssh-keygen times out after 30 s;
    • ssh-keygen exits non-zero.

    In each of those cases live-go cannot count as established. It stays in blocking.live, and an established live-go is reported as an error. The control is hard to remove or bypass:

    • The checker refuses a catalog whose live-go gate names no control, so deleting the block alone cannot make live-go presence-only.
    • condition_holds never lets live-go hold on presence alone, even for a caller that passes no control.
    • Recorded established, live-go must carry user_decision.
    • Only a gate owned by user-decision with a null flip condition may have a control.

    The checker runs ssh-keygen with tree-relative paths from the tree root, so no host path reaches its messages, and it writes nothing. --check now also prints authorship.live-go.{verified, detail}, an additive output key.

Your one-time setup (also in catalogs/us-equities/README.md, "Live-go authorship control")

Once, before the first go:

  1. Make a signing key that agent sessions cannot use. A hardware-backed key is best, because each signature then needs your touch: ssh-keygen -t ed25519-sk -C live-go-signer -f ~/.ssh/live-go_ed25519_sk, where your platform supports FIDO2. Otherwise use a passphrase-protected ssh-keygen -t ed25519 -C live-go-signer -f ~/.ssh/live-go_ed25519, and keep it out of any ssh-agent that agent sessions can reach.
  2. Register the public key as the only line of the allowed_signers file:
    printf 'live-go-signer namespaces="live-go@native-agent-stack" %s\n' \
      "$(cut -d' ' -f1,2 ~/.ssh/live-go_ed25519_sk.pub)" > docs/decisions/live-go.allowed_signers
    Optionally, append ,valid-before="YYYYMMDD" to the options to give a go an expiry. After that date the verification fails.

For each go:

  1. Write docs/decisions/live-go.md with LF line endings. The repository's .gitattributes is * text=auto eol=lf, and the signature covers the committed bytes.
  2. Remove any old signature first, because ssh-keygen -Y sign stops at an overwrite prompt:
    rm -f docs/decisions/live-go.md.sig
    Then sign:
    ssh-keygen -Y sign -f ~/.ssh/live-go_ed25519_sk -n live-go@native-agent-stack docs/decisions/live-go.md
  3. Verify:
    ssh-keygen -Y verify -f docs/decisions/live-go.allowed_signers -I live-go-signer -n live-go@native-agent-stack -s docs/decisions/live-go.md.sig < docs/decisions/live-go.md
    It prints Good "live-go@native-agent-stack" signature for live-go-signer .... --check already shows authorship.live-go.verified: true while live-go is still user_decision.
  4. Set live-go to status: established and evidence_class: user_decision. Then python3 scripts/trading_gates.py --check must exit 0 with status: passed and errors: [].
  5. Commit the four files yourself.

An optional complement, and your change to make, is a ruleset or code-owner review requirement on docs/decisions/live-go* and the gates file.

Evidence-class table

Claim Evidence class Command / receipt
user_decision is accepted only on user-decision gates with a null flip condition synthetic UserDecisionEvidenceTests (3 tests)
An established gate whose condition fails blocks its rung and every later rung; optional gates never block synthetic ReadinessRequiresHoldingEvidenceTests (4 tests), including the repository's own rows recorded established in an empty tree
live-go fails closed on a missing or empty file, an out-of-tree symlink, no ssh-keygen, a timeout, an OSError or a non-zero exit, and never holds on presence alone; the checker writes nothing synthetic LiveGoAuthorshipTests (7 tests; subprocess.run patched to fail if it is called on the missing-file paths)
Real ssh-keygen -Y verify accepts the matching signature and refuses 12 mismatches: an edited document, an unlisted key, the file namespace, another namespace, another principal (in the file and in the catalog), another namespace (in the catalog), a namespace-restricted key, an expired key, a file with no key, another document's signature, and a malformed signature local_integration (OpenSSH_9.6p1, throwaway ed25519 keys in temporary directories) LiveGoSignatureTests (2 tests)
The repository catalog names live-go's control with the expected method and paths, and a checker run on the repository leaves the three live-go files exactly as they were (absent today) local_integration RepositoryLiveGoControlTests (1 test)
Your setup commit would not trip this repository's scanners local_integration (throwaway key, scratch directory, deleted afterwards) A throwaway live-go.allowed_signers line, live-go.md.sig and live-go.md pass gitleaks dir --config .gitleaks.toml (no leaks) and python3 scripts/validate.py --scan-file (status: passed)
Both required CI jobs ran the real-signature tests (on the earlier head eeaca88, whose checker and test bytes equal this head's) native_proven (GitHub Actions, run 36193588517 and run 36193588443) ubuntu-24.04 validate on merge 1fa2894: 5626 tests, skipped=648. main at c303d8e: 5609 tests, skipped=648. That is +17 tests, exactly this PR's, with 0 new skips. macos-15 validate-macos verbose log artifact: all 17 new tests ok, including both LiveGoSignatureTests
The new tests are not vacuous: they fail against the pre-change checker local_integration This PR's test file run against git show origin/main:scripts/trading_gates.py in a scratch tree: FAILED (failures=37, errors=28)
The README steps work end to end on the real catalog local_integration (scratch copy of the checker, the catalog and its 13 existing receipts; throwaway key) Before the flip: verified: true, live-go in blocking.live. After the flip: exit 0, errors: [], live-go out of blocking.live. After an edit: exit 1, live-go back in blocking.live
The repository ladder's output is unchanged apart from the new key local_integration trading_gates.py --check: exit 0, status: passed, the same blocking.live (1x/2x/4x, ibkr-local-acceptance, live-go), authorship.live-go.verified: false ("receipt missing")

Review dispositions

  • Codex review P1, scripts/trading_gates.py:237, "Pin the live-go signer outside mutable repository state": rejected for this PR, with this evidence.

    1. The stage brief specifies this design: verify with ssh-keygen -Y verify against an allowed_signers file whose path the catalog names. This PR implements that design.
    2. The threat the readiness review recorded was a mistaken agent flip. In its words, "An agent commit could therefore flip live-go by mistake", because a placeholder live-go.md plus any evidence class made rung_ready.live true. This PR closes that path. Without a verifying signature, live-go fails closed, as LiveGoAuthorshipTests and LiveGoSignatureTests show. A forgery now takes four deliberate, visible edits: a new key, a rewritten live-go.allowed_signers, a signature and the flip. Each one breaks the standing rule never to write or flip live-go.
    3. Whoever can merge can also change anything the checker could consult. Main requires 0 approvals (.github/main-ruleset.json: required_approving_review_count: 0, bypass_actors: []). Agents act under the owner's identity. The checker, its tests and the CI workflow that would enforce an external pin are all in the tree. A pin outside the tree therefore turns a forgery from a data edit into an enforcement-code edit, which review sees more readily. It does not make a forgery impossible.
    4. A trust root outside the agents' reach does exist: the owner's GitHub SSH signing keys (GET /users/seathatflowsinourveins/ssh_signing_keys, HTTP 200, 0 keys registered, measured 2026-09-25). The gh token on the workstation carries only gist, read:org, repo and workflow (measured with gh auth status). It lacks admin:ssh_signing_key, so agents on that host cannot register a key there without the owner's interactive re-authorization. Binding live-go to that list needs two decisions that are yours:
      • register a dedicated signing key on the account;
      • accept a network dependency in the checker, or in a new required CI step, which then fails closed whenever GitHub cannot be reached.

    The decision record lists this as an alternative, with the overturn condition "the user deciding to bind the signer to the owner's GitHub signing keys". The residual, that anyone with write access can replace the in-tree allowed_signers file, is stated in the decision record, the catalog note and the README.

Local commands run (raw via rtk proxy; head ce05fbb unless noted; scripts/trading_gates.py and tests/test_trading_gates.py are byte-identical at eeaca88 and ce05fbb)

$ python3 -m unittest tests.test_trading_gates            -> Ran 43 tests, OK (26 before this PR)
$ python3 -m unittest tests.test_alpaca_history_fixture tests.test_adaptive_market_research tests.test_adaptive_paper_runner.AchievedLeverageGateTraceTests
                                                          -> Ran 93 tests, OK (skipped=15: "requires reviewed isolated Alpaca runtime")
$ python3 -m unittest   (at the earlier head 72af4006 on c9d74eab; the rebase onto 8df847b4 brought only #281's data)
                                                          -> Ran 5608 tests, FAILED (failures=1, skipped=629). The one failure,
                                                             tests.test_pre_commit_gate, hit the host's guarded-gitleaks per-user lock
                                                             held by another session ("another scan holds the per-user lock");
                                                             rerun alone: Ran 3 tests, OK
$ python3 scripts/validate.py                             -> exit 0, status passed, hashed_files 5622
$ python3 scripts/validate_catalogs.py                    -> exit 0
$ python3 scripts/evidence_manifest.py --check            -> exit 0, status passed
$ python3 scripts/trading_gates.py --check                -> exit 0, status passed, errors []
$ python3 scripts/host_receipts.py validate               -> exit 0, receipts 106
$ python3 scripts/component_matrix.py --check             -> exit 0
$ python3 scripts/new_host_grand_list.py --check          -> exit 0
$ uvx --offline ruff check --select F,E9,B scripts/trading_gates.py tests/test_trading_gates.py -> All checks passed
$ gitleaks git . --log-opts=origin/main..HEAD --config .gitleaks.toml --max-target-megabytes 2 --redact --no-banner   (guarded wrapper)
                                                          -> 2 commits scanned (~55 KB), no leaks found, exit 0. Every touched file
                                                             is under the 2 MB cap (largest: manifests/evidence.json, 1,484,018 bytes),
                                                             so nothing was excluded by size

The manifest entries were written with scripts/host_receipts.py register_file. Each was then recomputed independently: the sha256 and byte count of all five files match.

Decision record

docs/decisions/2026-09-25-live-go-authorship-control.md names the evidence, the alternatives (a distinct-reviewer ruleset, signed commits, GPG, Sigstore/gitsign, minisign/signify) and what would overturn the choice.

Coordinator choices, each with the condition that would overturn it

  • A signature rather than a ruleset, as the stage brief directs. The repository has one maintainer, and agents act under that identity. Overturned by a second reviewer account, which should then be compared on the same attempted agent flip.
  • A top-level authorship block keyed by gate id, rather than a new per-gate field. GATE_FIELDS and every existing row stay unchanged, and concurrent gate-row PRs meet fewer conflicts. Overturned if a second authorship-controlled gate makes a per-row field clearer.
  • The checker requires live-go to name a control, and an established live-go to carry user_decision. This is the converse of item 1 for gates with a control, and step 28 of the readiness plan asks for it. Overturned if you prefer to record another class.
  • The names: the principal live-go-signer, the namespace live-go@native-agent-stack, and the paths docs/decisions/live-go.md.sig and docs/decisions/live-go.allowed_signers. The checker reads all of them from the catalog, so you may rename them there before setup.
  • Only live-go requires a signature. user_decision evidence on another gate owned by user-decision with a null flip condition keeps the presence rule, as item 1 specifies; no such gate exists today. Overturned when a second gate of that shape is added: it should then name a control too.
  • A host without ssh-keygen fails closed, and on such a host the real-signature tests skip. Overturned if a required CI runner lacks ssh-keygen: live-go could then never verify there. Both required runners have it today: the signature tests ran and passed on ubuntu-24.04 and on macos-15 (see the evidence table).

Limits and follow-ups

  • The allowed_signers file lives in the tree, so anyone with write access can replace it. The replacement is visible in that file's diff and history, but it is not prevented. A key that needs a physical touch keeps agent sessions from signing with the registered key. It does not stop that key from being replaced.
  • The checker verifies who signed live-go.md, not what it says. The readiness review's optional items are not in this PR: a format lint for live-go.md (step 23 item 4) and a native-fault source_matches condition (item 5).
  • docs/grand-catalog-handbook.md:347-368 still says no checker can evaluate live-go. It is a shared document, left to its owner; editing it would make this PR lane:shared.
  • Gating Order-throughput harness: user-gated live capacity mode (never runs without the user's live key and go file) #191's live mode on this checker's output is a separate PR and your decision. This PR does not touch it.

Checklist

  • No GitHub Actions or workflows changed.
  • No secrets are printed, logged or committed. The test keys are throwaway, generated in temporary directories and deleted.
  • No paid hosting, subscription or billing surface.
  • Peer-owned untracked files and worktrees were preserved.

🤖 Generated with Claude Code

Custody check, 2026-10-04

On a trial merge of this head with main 14048b84, which conflicts only in the registry: python3 -m unittest tests.test_trading_gates ran 43 tests, OK; python3 scripts/trading_gates.py --check reported status passed, exit 0. A GPT-6.1 Sol cross-family read returned ACCEPT; its one P2 was this bullet's missing scope rewrite. This PR adds a control that makes a live go harder; it enables no live mode, endpoint or key.

@seathatflowsinourveins seathatflowsinourveins added the lane:trading Trading lane: sim, paper and live north star label Sep 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eeaca88a39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/trading_gates.py
…vidence, and a signed live-go

- scripts/trading_gates.py:
  - Adds the evidence class user_decision. It is valid only for a gate owned
    by user-decision whose flip_condition is null.
  - rung_ready and blocking now count a required gate only while it is
    recorded established and its condition holds. Simulation V1 of the
    2026-09-25 readiness review had shown status failed alongside
    rung_ready.live true.
  - A top-level authorship block names a detached SSH signature per gate,
    checked with `ssh-keygen -Y verify`. The live-go gate must name one, and
    it holds only when that signature verifies. It fails closed in each of
    these cases:
    - the document, signature or allowed_signers file is absent or empty;
    - a path resolves outside the tree;
    - ssh-keygen is missing, times out or exits non-zero.
  - Recorded established, live-go must carry user_decision.
  - The checker writes nothing.
- catalogs/us-equities/gates-20260922.json:
  - Adds live-go's authorship control: docs/decisions/live-go.md.sig,
    docs/decisions/live-go.allowed_signers, principal live-go-signer,
    namespace live-go@native-agent-stack.
  - Adds a dated live-go note.
  - No status, flip condition or evidence class changes.
- tests/test_trading_gates.py: 17 new tests. The real-signature tests use
  throwaway ed25519 keys in temporary directories. The live-go fixture in
  the readiness test now carries its control.
- catalogs/us-equities/README.md: the user's one-time setup and per-go
  steps.
- docs/decisions/2026-09-25-live-go-authorship-control.md: the evidence,
  the alternatives and the conditions that would overturn this choice.

No key, signature, allowed_signers entry or live-go.md was created. live-go
stays user_decision.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- README per-go steps: re-register the gates file in manifests/evidence.json
  (scripts/validate.py fails on an unregistered edit), run validate.py and
  evidence_manifest.py --check, commit the manifest with the go, rebase per
  the hot-file protocol, and open the PR with the required SOTA sources
  section. The valid-before expiry and revert path carry the same steps.
- gates-20260922.json: the top-level scope now defines readiness as
  established AND holding now, and names live-go (no flip condition, flipped
  only by the user) as the exception to the flip-candidate rule; the
  authorship note lists the symlink-loop case.
- trading_gates.py: resolve(strict=True) in authorship_holds and
  source_matches_holds; a symlink loop (RuntimeError on Python 3.12 and
  earlier, OSError from 3.13) now fails closed with JSON output and no host
  path instead of a traceback. Tests cover both paths and main()'s output.
- Decision record: pin OpenSSH V_9_6_P1 as tag object b24f772e, commit
  8241b9c0 (and git v2.43.0 as tag object c089584a, commit 564d0252); state
  the symlink-loop case and the untested ed25519-sk boundary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Re-registered on origin/main 564a16a (after #278) with
scripts/host_receipts.py register_file (docs/lanes.md hot-file protocol):
catalogs/us-equities/README.md, catalogs/us-equities/gates-20260922.json,
scripts/trading_gates.py and tests/test_trading_gates.py updated; the decision
record docs/decisions/2026-09-25-live-go-authorship-control.md added.
component_matrix.py --write and new_host_grand_list.py --write left their
reports unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scout and others added 3 commits October 4, 2026 16:03
…egistry plus the owned rows)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore the final hot-file commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (hot-file protocol: every hot-file edit in the last commit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Claude session native-agent-stack-5f: landing at head 6cc3ee64bc1672ac05ac35af7cbf082206faad8d. The command center (wsl-architecture-design) gave its ACK at b583ac04c1a8, which reaches this head only through recorded mechanical refreshes whose owned non-registry patch-ids are equal (its carry rule); under the user's 2026-10-04 decision it owns merges that touch the hot registry. The read-only cross-family review (GPT-6.1 Sol max, the packaged Codex SDK worker) returned ACCEPT at b583ac04c1a8; this head is reached from it through recorded carry edges (equal owned patch-ids, or a cross-family delta read returning ACCEPT at the edge's target), with no P1 item.

Observed main 6c5f5bb73869d01c934c97591724b5728d761465. coordination/merge_tree_landing_check.py (sha256 a11649fcf2e5844c…) exit 0:

main 6c5f5bb73869d01c934c97591724b5728d761465 head 6cc3ee64bc1672ac05ac35af7cbf082206faad8d base 14048b840425c2569e0df60a6596e94e601da15b merged-tree 1d1ca6866db456c6997d59bb8bc8cd8c3e27cd53 merge-tree-exit 0
ok   1: clean three-way merge
ok   2: merged-vs-main paths 6, outside PR-owned 0 []
ok   3: main drift 33 paths, overlap with PR-owned inputs (registry excluded) []
ok   4: registry foreign rows equal True, order preserved True, PR-owned rows 5, unowned top-level keys differing []
ok   5: merged files[] sorted by path with no duplicates: True (9928 rows)
LANDABLE
condition 5: merged files[] sorted and unique (tree 1d1ca6866db456c6997d59bb8bc8cd8c3e27cd53)

Required checks at this head: 8 pass . Unresolved review threads: 0.

@seathatflowsinourveins
seathatflowsinourveins merged commit 6334825 into main Oct 4, 2026
25 checks passed
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Claude session native-agent-stack-5f: post-merge observation. Landed as 63348259aef76a5b3d7919cba7913e403ded0dbd (parent 6c5f5bb73869d01c934c97591724b5728d761465), tree 1d1ca6866db456c6997d59bb8bc8cd8c3e27cd53. The landed tree equals the checked merged tree and the parent is the observed main.

@seathatflowsinourveins
seathatflowsinourveins deleted the claude/checker-hardening-20260925 branch October 4, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:trading Trading lane: sim, paper and live north star

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant