Skip to content

feat(ats): freeze the Spec 1 broker connector and approval contracts - #160

Merged
AetherAI3 merged 5 commits into
mainfrom
feat/ats-trading-contract-freeze
Sep 22, 2026
Merged

AetherAI3 merged 5 commits into
mainfrom
feat/ats-trading-contract-freeze

Conversation

@AetherAI3

@AetherAI3 AetherAI3 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Gate 1.0 of the trading integrations finale — the shared schema freeze that Spec 1 §17 and Spec 2 PR 2.1 both put first. It lands the shapes and nothing else: no connector write path is enabled.

Why this shape

Both specs' primary invariant is that authority lives in exactly one place. The cheapest way to keep that true is to make the unsafe states unrepresentable rather than merely forbidden in review. So these contracts are closed documents with real validators, not bare TypeScript interfaces — an unknown key is a refusal, and a model-originated proposal is treated as attacker-controlled input throughout.

What is frozen

Eight schemas under src/core/ats_contracts/: execution-state, connector-capability, account-binding, delegated-trading-grant, equity-order-intent, order-review-receipt, operator-approval, execution-receipt.

Invariants the shapes enforce, each with a test:

  • Implementation support is not permission. A capability snapshot pins grants_execution_authority: false; any other value fails to parse.
  • Effective mode never exceeds requested mode on the ladder offline < observe < review_only < paper < approve < auto. Halt states may follow any request; a differing effective mode must carry a reason.
  • An empty symbol allowlist permits nothing. There is no wildcard token, so an allowlist cannot quietly become one after a half-failed load.
  • confirmation is pinned to per_order. Robinhood can permit unattended placement; that mode is not expressible here.
  • Raw account numbers cannot leave the connector core. Opaque refs need a namespace prefix, a masked label with 5+ consecutive digits is refused, and redactBindingForExport() is the only exported projection.
  • Preview, approval and commit must agree on adapter, endpoint schema digest, execution environment and binding generation. verifyApprovalChain() returns a terminal verdict — there is deliberately no re-preview or reroute path, because that path is how an approval minted against a paper preview would end up authorizing a live order.
  • Fill facts only when broker-confirmed, and never more than was ordered.
  • ambiguous is a first-class outcome requiring a reconciliation state, so a commit that may have started is never retried.

Two judgement calls worth your review

1. Canonicalization is an honest subset, not RFC 8785. The specs say RFC 8785. I reused the existing src/core/device_runtime/canonical_json.ts rather than adding a second encoder, and it is stricter: it refuses non-integer and non-finite numbers outright. On integers it agrees byte-for-byte with 8785 and with Python's json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=True). docs/CONTRACTS.md states the difference plainly rather than claiming full 8785. Consequence: every monetary field is integer minor units and every quantity a whole share — floats are how a preview and a commit come to disagree about a number that looked equal.

2. effective_mode is the union of both specs' lists. Spec 2 §7.2 gives offline|observe|paper|approve|auto; Spec 1 §11.8 gives observe|review_only|orders_paused|emergency_locked. They describe one projection from two vantage points, so this freezes the union. Splitting them would let a runtime-authored orders_paused fail to parse on the connector side — a halt that does not deserialize is a halt that does not stop anything.

Test plan

  • npm run typecheck clean
  • Full suite: 2616 tests, 2606 pass, 0 fail, 10 skipped
  • npm run verify:production → {"ok":true,"packedFiles":852,"packedBytes":4539531}
  • 33 new tests covering every invariant above, plus prototype pollution, non-UTC timestamps, fractional quantities, and approval-chain mutation (10 fields, each must break the chain)
  • test/fixtures/ats_contracts_golden.json — one validated document per schema with its canonical digest, following the existing bridge_conformance.json drift-detector pattern. The ATSv2 Python suite mirrors it, so canonicalization divergence fails a test instead of a trade.
  • A test greps the modules for node:fs / node:net / node:http / node:child_process / fetch( / process.env — this is what makes "no write path enabled" checkable rather than asserted.

Correction to an earlier revision of this description

An earlier version of this body claimed 5 pre-existing test failures and that the package was 433 KB over its size budget. Both claims were wrong and are withdrawn. They came from my own broken build: build is tsc -p tsconfig.json && node dist/scripts/copy-skill-assets.js, and I had been running bare npx tsc, which silently skipped the asset copy. That produced a dist with no dist/src/skills/builtin/, which is what failed the four skill tests — including in the "clean main baseline" worktree I used to attribute them, so the attribution was wrong too.

The size claim was a second, independent error: I summed raw dist/src bytes on disk, but MAX_UNPACKED_BYTES is compared against npm's unpackedSize for the packed set. Those differ sharply because sourcemaps are not packed at all — 0 .map files ship. Real figures on this branch:

bytes
npm unpackedSize (what the gate checks) 4,539,531
MAX_UNPACKED_BYTES 5,000,000
headroom 460,469
tarball 1,158,260
this PR's contribution to the pack 86,546

So there is no size breach, this PR does not approach one, and the earlier suggestion to strip .js.map was doubly unnecessary — they were never in the package. Thanks to the Spec 2 lane for pushing back on the number rather than taking it.

Scope and coordination

Deliberately not in this PR:

  • strategyCount() at src/commands/ats_agent.ts:139 returns every scanned file including rejected/needs_conversion, so setup can report "6 strategies" with 0 compiled — the dishonest-readiness case Spec 2 §2.1 names. It feeds the zero-strategy check at :313, the persisted count at :327 and the journal count at :391. Confirmed real; it belongs to Spec 2 PR 2.3, and that lane has taken it.
  • Spec 2's runtime / data-profile / strategy / journal contracts — a parallel lane (feat(ats): Spec 2 lane — frozen contracts, headless runtime supervisor, honest setup #161), stacked on this branch and importing this base.

Next gates consume these shapes: 1.1 headless connector core, 1.5 local operator gateway, 1.6 delegated paper hardening.

Merge order: #161 targets this branch, so GitHub Actions does not run on it (ci.yml triggers on pull_request with branches: [main]). Land this first; #161 then retargets to main and gets the full matrix before approval.

Gate 1.0 of the trading integrations finale: the cross-repository shapes for
local broker connectors, account onboarding and the approval bridge. No
connector write path is enabled — src/core/ats_contracts/ performs no I/O,
holds no credential and places no order, and a test asserts that by scanning
the modules for node:fs/node:net/node:http/node:child_process/fetch/process.env.

Eight schemas frozen, each a CLOSED document (an unknown key is a refusal):
execution-state, connector-capability, account-binding,
delegated-trading-grant, equity-order-intent, order-review-receipt,
operator-approval, execution-receipt.

The invariants are enforced by the shapes rather than described in prose:

- A capability snapshot pins grants_execution_authority:false. Implementation
  support is not permission.
- Effective mode can never exceed requested mode on the ladder
  offline<observe<review_only<paper<approve<auto; the halt states may follow
  any request, and a differing effective mode must carry a reason.
- An empty symbol allowlist permits nothing. There is no wildcard token, so an
  allowlist cannot degrade into one on a half-failed load.
- confirmation is pinned to per_order; standing or session approval is not
  expressible even though the provider offers it.
- Raw account numbers cannot leave the connector core: opaque refs require a
  namespace prefix, a masked label with 5+ consecutive digits is refused, and
  redactBindingForExport() is the only exported projection.
- Preview, approval and commit must name the same adapter, endpoint schema
  digest, execution environment and account binding generation.
  verifyApprovalChain() returns a terminal verdict and there is deliberately no
  re-preview or reroute path to reach for.
- Fill facts are only expressible when broker-confirmed, and a receipt cannot
  fill more than it ordered.
- An ambiguous commit is first-class and requires a reconciliation state, so a
  commit that may have started is never retried.

Canonicalization reuses the existing device-runtime encoder rather than adding
a second one. The specs say RFC 8785; what is implemented is a stricter subset
that refuses non-integer and non-finite numbers, and docs/CONTRACTS.md states
that difference plainly rather than overclaiming. Every monetary field is an
integer count of minor units and every quantity a whole share, so a preview and
a commit cannot disagree about a number that looked equal across languages.

test/fixtures/ats_contracts_golden.json carries one validated example per
schema with its canonical digest, following the bridge_conformance.json
drift-detector pattern; the ATSv2 Python suite mirrors it so a canonicalization
divergence fails a test instead of a trade.
Addresses the review on #160. Four blockers, none cosmetic.

1. THE APPROVAL CHAIN WAS NOT FULLY BOUND.

verifyApprovalChain() trusted an intentDigest supplied by its caller, so it
proved only that three documents agreed about a number the caller chose — a
forged intent plus a matching digest satisfied every comparison. It now derives
both the intent and review digests itself and takes no digest argument, and it
checks approval.review_digest against the recomputed review, which catches a
tampered review that kept its review_id.

ConnectorBindingRef carried only binding_generation, which cannot tell two
accounts apart: both are generation 1 the day they are linked. It now carries
provider_id and a stable opaque account_binding_id, and connectorBindingMatches
derives its comparison from the field list so adding a field cannot leave the
check silently testing the old set. BrokerAccountBindingV1 gained the matching
account_binding_id.

The chain also refuses a refused or expired review and a spent or expired
approval, rather than validating a structurally perfect chain over a dead one.

New verifyCommitAuthority() is the gate immediately before a broker commit: it
proves the order is aimed at the account the local binding names, under a grant
that still permits it, in a mode that still allows submission. Kill and pause
are re-checked there rather than inherited from whatever the review said
minutes earlier.

The mutation test now classifies EVERY approval field as cross-bound or
recorded-only and asserts each behaves as classified. A field missing from that
map fails the test, so adding one to the contract forces a decision instead of
landing unchecked.

2. THE CANONICALIZATION WAS NOT AN RFC 8785 SUBSET.

The previous module re-exported the device-runtime encoder and called the
result "jcs-integer-subset/1". That encoder escapes every non-ASCII character
(ensure_ascii=True), while JCS requires non-control Unicode emitted literally
and encoded as UTF-8 — different bytes, and a different digest, for any
document containing a non-ASCII character. The name implied a narrowing when
the difference was the string encoding.

ats_contracts now has a real JCS implementation: literal Unicode, keys sorted
by UTF-16 code unit, ECMAScript number-to-string, -0 normalized, lone
surrogates refused. Profile renamed to rfc8785/1. device_runtime's encoder is
untouched and must stay that way — it is pinned to the Cloud's Python
ensure_ascii and changing it would invalidate device signatures.

Float rejection moves to where it belongs: JCS serializes floats fine, so the
encoder accepts them and the schema validators keep refusing them. Integer
minor units remain the contract rule.

The fixture gains encoder vectors for Unicode, astral characters, property
ordering, -0, control escapes and large integers, plus lone-surrogate rejects.
test/fixtures/ats_contracts_golden_verify.py is an independent Python
implementation that reproduces all 18 checks byte-for-byte; it is the mirror
ATSv2 lifts. It is not wired into npm test because CI has no guaranteed Python
and a conditional skip would give false assurance.

Worth knowing for anyone writing the Python side: sorted() is WRONG. Python
compares code points, RFC 8785 requires UTF-16 code units, and they disagree
when an astral character meets a BMP character at or above U+E000. The fixture
contains a vector that fails loudly on exactly that and passes everywhere else.

3. A MALFORMED ABUSE FLAG READ AS NOT ABUSIVE.

abuse_flagged used `raw.abuse_flagged === true`, turning a missing key, a null
or the string "true" into false — silently downgrading an abuse flag in the one
direction it must never fail in. Now a strict boolean.

4. PRIMITIVE VALIDATION.

timestamp() re-derives the calendar fields from the parsed instant and compares
them back, because Date.parse rolls 2026-02-30 over to 2 March. An expiry that
quietly moves is not an expiry.

CONTROL now rejects every C0 character including tab, newline and carriage
return, matching what its comment always claimed. Fields validated by text()
are single-line by construction and reach logs and support bundles, where an
embedded newline lets one record forge a second. multilineText() is added for
genuine prose (journal notes, compiler diagnostics): it permits newline and
tab and REFUSES carriage return rather than normalizing it, because these
strings are digested and folding CRLF would let identical-looking text hash two
different ways.

The opaque-reference comment no longer overclaims. A prefix cannot prove an
account number is absent from the body; acct_000123456789 satisfies the shape.
Non-reversibility has to be produced upstream by the connector core minting
these identifiers, and the comment now says so.
@AetherAI3

Copy link
Copy Markdown
Owner Author

All four required corrections are in 955118c. Summary of what changed and what I had to decide along the way.

1. Approval chain

  • verifyApprovalChain() no longer takes intentDigest. It computes digestOf(intent) and digestOf(review) itself. The old signature proved only that three documents agreed about a number the caller supplied; a forged intent with a matching digest passed every comparison. There is now no digest parameter to pass.
  • It checks approval.review_digest against the recomputed review, which catches a tampered review that kept its review_id — there's a test that mutates one unrelated field (evidence_age_ms) and confirms the chain breaks.
  • ConnectorBindingRef gains provider_id and a stable opaque account_binding_id; BrokerAccountBindingV1 gains the matching account_binding_id. There's a test with two bindings both at generation 1 proving they can no longer be confused.
  • connectorBindingMatches() now derives its comparison from the field list rather than an explicit && chain, so adding a field to the ref cannot leave the check silently testing the old set.
  • The chain refuses a refused review, an expired review, an expired intent, and a consumed or expired approval.
  • New verifyCommitAuthority() takes {now, grant, usage, binding, intent, review, approval, resultingPositionNotionalMinor} and proves the order is aimed at the account the local binding names, under a grant that still permits it, in a mode that still allows submission. Kill and pause are re-checked there, not inherited from what the review said minutes earlier.
  • Mutation testing is now exhaustive in a way that stays exhaustive: every approval field is classified cross-bound or recorded-only, and a field missing from that map fails the test. Adding a field to the contract forces a decision rather than landing unchecked. Same treatment for all six ConnectorBindingRef fields.

2. Canonicalization — you were right, and the name was the worst part

src/core/ats_contracts/canonical.ts is now a real JCS implementation: literal Unicode, keys sorted by UTF-16 code unit, ECMAScript number-to-string, -0 normalized, lone surrogates refused. Profile renamed jcs-integer-subset/1 → rfc8785/1.

device_runtime/canonical_json.ts is untouched and must stay that way — it's pinned to the Cloud's Python ensure_ascii=True and changing it would invalidate device signatures. Two encoders now exist deliberately, each pinned to a different counterpart; docs/CONTRACTS.md says so explicitly so nobody "unifies" them later.

Float rejection moved as you specified: the encoder accepts floats (JCS serializes them fine), the schema validators keep refusing them. A test asserts both halves so neither drifts into the other.

Cross-language verification, done before freezing as you required. test/fixtures/ats_contracts_golden_verify.py is an independent Python implementation — it imports nothing from the TS side:

OK: 18 checks reproduced byte-for-byte by an independent Python implementation.

New vectors: Unicode, astral character, property ordering, -0, control escapes, large integers, nested/empty, plus two lone-surrogate rejects.

One finding worth flagging for the ATSv2 side. Python's sorted() is wrong for JCS. It compares code points; RFC 8785 requires UTF-16 code units. They disagree whenever an astral character meets a BMP character at or above U+E000, because the astral form begins with high surrogate 0xD800, numerically below U+FFFF. Demonstrated:

UTF-16 (correct):  {"\U00010000":1,"\uffff":2}
sorted()  (wrong): {"\uffff":2,"\U00010000":1}

The fixture carries a vector that fails loudly on exactly this and passes everywhere else, so a Python mirror that gets it wrong finds out at test time rather than at reconciliation time.

3. Abuse flag

raw.abuse_flagged === true → strict bool(). Tests cover missing key, null, 1, "true", "yes" — every one of which previously became false, silently downgrading an abuse flag in the one direction it must never fail in.

4. Primitives

  • timestamp() re-derives calendar fields from the parsed instant and compares back. 2026-02-30, 2026-09-31 and 2026-02-29 are refused instead of rolling over; 2028-02-29 is accepted.
  • CONTROL now rejects every C0 character including tab, newline and CR, matching what its comment always claimed.
  • Added multilineText() for genuine prose. It permits newline and tab and refuses CR rather than normalizing it — these strings are digested, so folding CRLF would let identical-looking text hash two different ways. (Requested by the Spec 2 lane for journal notes and compiler diagnostics, which are legitimately multi-line.)
  • The opaque-reference comment no longer overclaims: a prefix cannot prove an account number is absent from the body, acct_000123456789 satisfies the shape, and non-reversibility has to be produced upstream by the connector core minting these identifiers. Said plainly in both the code and docs/CONTRACTS.md.

Verification

  • Full suite: 2629 tests, 2619 pass, 0 fail, 10 skipped
  • npm run verify:production → {"ok":true,"packedFiles":852,"packedBytes":4556505} (443,495 under the gate)
  • Contract tests: 33 → 46
  • 0 control bytes across all touched files, no Bin in the diffstat

One unrelated observation. test/preview.test.ts → "only a terminal failed state with dead recorded processes permits identity-bound replacement" failed once in a full run and passed on re-run, and passes 14/14 in isolation. It's about process identity and PID liveness, so it looks timing-sensitive under --test-isolation=none rather than broken. Not touched by this PR; flagging it rather than quietly re-running until green.

Caught by the Spec 2 lane reviewing the freeze: every contract document in the
fixture was pure ASCII, and the old ensure_ascii encoder and a correct JCS
encoder produce byte-identical output on ASCII. So all eight contract digests
would still have matched after a regression to \u-escaping — the schema layer
pinned nothing about the encoding change this PR exists to make.

Measured before and after, by re-digesting every fixture entry under the old
escaping rule and counting which recorded digests disagree:

  before: canonical 3/8 catch it, contract 0/8
  after:  canonical 3/8 catch it, contract 3/8

The canonical vectors were already covering the encoder, so the fixture was not
blind — but a fixture where only one of two layers can detect the bug is one
edit away from being blind, and regenerating contract vectors under a regressed
encoder would have produced a self-consistent, silently wrong set.

Two contract documents now carry non-ASCII in fields that would realistically
hold it: an execution-state `effective_reason` with an em dash and a euro sign,
and an account-binding `masked_label` with a middle dot and bullets. That also
exercises text() accepting legitimate non-ASCII while still refusing control
characters, which nothing covered before.

Added a test that fails if someone tidies the non-ASCII back out. It does not
merely assert the characters are present — it re-digests a covered document
under the old escaping rule and requires the recorded digest to disagree, so
the coverage is proven rather than assumed.

Python mirror re-verified: 18 checks still reproduced byte-for-byte.
@AetherAI3

Copy link
Copy Markdown
Owner Author

Follow-up in b34df59 — the golden fixture could not fully detect the bug this PR exists to fix.

Raised by the Spec 2 lane reviewing the freeze. I measured it rather than taking it at face value: I re-digested every fixture entry under the old ensure_ascii rule and counted which recorded digests actually disagree. A vector whose digest doesn't change cannot catch the regression.

before after
canonical_vectors that catch it 3 / 8 3 / 8
contract_vectors that catch it 0 / 8 3 / 8

The encoder vectors were never blind — café, the astral 𝄞, and the UTF-16 key-ordering pair would each have failed loudly. But all eight contract documents were pure ASCII, and the old and new encoders are byte-identical on ASCII. So the schema layer pinned nothing about the encoding.

That's not just missing redundancy. Regenerating contract vectors under a regressed encoder would have produced a self-consistent, silently wrong set, with only the canonical vectors objecting. A fixture where one of two layers can detect the bug is one edit from being blind.

Fixed by putting non-ASCII in two fields that would realistically carry it — an execution-state effective_reason with an em dash and euro sign, and an account-binding masked_label with a middle dot and bullets. That also became the only coverage of text() accepting legitimate non-ASCII while still refusing control characters.

Added a test that fails if the non-ASCII is ever tidied back out. It doesn't assert "these characters are present" — that would pass even if the encoder stopped caring. It re-digests a covered document under the old escaping rule and requires the recorded digest to disagree, so the coverage is proven on every run rather than assumed.

Re-verified after the change:

  • Full suite: 2630 tests, 2620 pass, 0 fail, 10 skipped
  • npm run verify:production → {"ok":true,"packedFiles":852,"packedBytes":4556505}
  • Python mirror: OK: 18 checks reproduced byte-for-byte by an independent Python implementation.
  • All 12 touched files: ctrl=0 crlf=false (added a CRLF check after the Spec 2 lane hit a scripted edit that silently rewrote two files to CRLF and turned a 102-line diff into 1392)

…sion

The Spec 2 lane found a self-lowering floor in its own guard — a threshold
derived from the list it was checking, so it dropped as the list shrank and
stayed green to one. Checking mine for the same shape turned up the same defect
by a different mechanism.

My floors were constants, so they could not lower themselves. But the contract
floor was `>= 1` against actual coverage of 3, which is the same failure with
extra steps: a constant set far below reality never rises to meet it. Measured
rather than reasoned about — tidy the non-ASCII out of two of the three covered
documents and regenerate their digests honestly, and coverage falls 3 -> 1 while
the guard stays green.

That lands in the worst available state. One catching vector that nobody knows
is the only one is strictly worse than zero, because zero is unambiguous and
one looks like coverage.

Fixed:

- MIN_CANONICAL_COVERAGE and MIN_CONTRACT_COVERAGE are named constants at the
  top of the file, independent of the fixture, both set to the coverage that
  actually exists. Raising them is a deliberate edit; lowering one should be
  argued for in review.
- The digest disagreement is now asserted for EVERY covered document, not just
  contractCovered[0]. Checking only the first would let the other two be
  regenerated under a regressed encoder with the test still green, and the
  failure message names the schema that stopped pinning the encoding.

Re-ran all four scenarios against the corrected guard:

  baseline                                -> passes, coverage 3
  honest tidy-up (strip AND regenerate)   -> fails the contract floor
  erosion 3 -> 1 (regenerated honestly)   -> fails the contract floor
  regenerated under a regressed encoder   -> fails the digest check, by schema
  control: mutate with stale digests      -> fails for the wrong reason, proves nothing

The control is worth keeping in mind: it disagrees because any document change
breaks a stale digest, not because the guard works. That is the third instance
today of a check that was green while proving nothing, so it is recorded here
rather than left as folklore.

Full suite 2630 tests, 2620 pass, 0 fail, 10 skipped. verify:production ok at
4,556,505. Python mirror still reproduces 18 checks byte-for-byte.
@AetherAI3

Copy link
Copy Markdown
Owner Author

96452ed — the encoder-coverage guard added in b34df59 tolerated silent erosion. Found by checking my own work against a defect the Spec 2 lane found in theirs.

They hit a self-lowering floor: a threshold derived from the list it guarded, so it dropped as the list shrank and stayed green all the way to one. My floors were constants, so they couldn't lower themselves, and I nearly stopped at "different structure, not affected."

Then I ran it. The contract floor was >= 1 against actual coverage of 3. Tidy the non-ASCII out of two of the three covered documents, regenerate their digests honestly, and coverage falls 3 → 1 with the guard still green.

Same failure, extra steps. Theirs lowered itself to meet a shrinking list; mine was set so far below reality it never had to move. A constant floor is only safe if it equals the coverage that actually exists — otherwise it's a derived floor that someone derived once, badly, and froze.

It also lands in the worst available state: one catching vector that nobody knows is the only one is strictly worse than zero, because zero is unambiguous and one looks like coverage.

Fixed:

  • MIN_CANONICAL_COVERAGE / MIN_CONTRACT_COVERAGE — named constants at the top of the file, independent of the fixture, both set to actual coverage.
  • The digest disagreement is asserted for every covered document, not just contractCovered[0]. Checking only the first would let the other two be regenerated under a regressed encoder with the test green. The failure names the schema that stopped pinning the encoding.

All four scenarios re-run against the corrected guard:

scenario result
baseline passes, coverage 3
honest tidy-up (strip and regenerate) fails contract floor
erosion 3 → 1, regenerated honestly fails contract floor
regenerated under a regressed encoder fails digest check, by schema name
control: mutate with stale digests fails for the wrong reason — proves nothing

That control is worth keeping: it disagrees because any document change breaks a stale digest, not because the guard works.

The pattern, now four instances across the two lanes. Every one was a check that was green while proving nothing, and the tell was identical each time — the check's own correctness depended on something the check did not verify:

  1. a floor derived from the list it guarded
  2. a verification that mutated a document but left the digest stale
  3. a presence check shadowing the digest assertion it preceded (mine, unexercised until someone asked)
  4. a constant floor frozen far below actual coverage (mine)

What worked every time wasn't more assertions — it was running the scenario the check claims to catch, and confirming it fails for the stated reason. That last clause is what caught #2.

Verification: full suite 2630 tests, 2620 pass, 0 fail, 10 skipped · verify:production ok at 4,556,505 · Python mirror 18/18 byte-for-byte · all 12 touched files ctrl=0 crlf=0.

@AetherAI3
AetherAI3 merged commit b18fc6a into main Sep 22, 2026
9 checks passed
@AetherAI3
AetherAI3 deleted the feat/ats-trading-contract-freeze branch September 22, 2026 21:59
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